Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

MonacoEditorReactComp caches text and won't update it even if new text is set in userConfig #752

Open
azhakhan opened this issue Sep 24, 2024 · 8 comments

Comments

@azhakhan
Copy link

i use monaco-languageclient to render python code editor

i'm using MonacoEditorReactComp and configured it as:

  • useConfigureMonacoWorkers is copied straight from the docs
  • in CreateUserConfig i configure LSP (websocket url), text (which is code pulled form the server), uri (full path to the file), and getKeybindingsServiceOverride
  • onTextChanged is passed to MonacoEditorReactComp and it handles text changes in parent component
  • in PythonEditor (parent component), i have 2 key pieces:
    • code - the original code pulled from the server
    • updatedCode - a updated version of the code (updated via handleTextChanged based on changes from MonacoEditorReactComp) to be used by parent component

here is how it looks in the code:

import '@codingame/monaco-vscode-python-default-extension';
import { UserConfig } from 'monaco-editor-wrapper';
import type { TextChanges } from '@typefox/monaco-editor-react';
import { MonacoEditorReactComp } from '@typefox/monaco-editor-react';
import { useWorkerFactory } from 'monaco-editor-wrapper/workerFactory';
import getKeybindingsServiceOverride from '@codingame/monaco-vscode-keybindings-service-override';


const useConfigureMonacoWorkers = () => {
  useWorkerFactory({
    ignoreMapping: true,
    workerLoaders: {
      editorWorkerService: () =>
        new Worker(
          new URL('monaco-editor/esm/vs/editor/editor.worker.js', import.meta.url),
          { type: 'module' },
        ),
    },
  });
};

const CreateUserConfig = ({ text, uri }: { text: string; uri: string }): UserConfig => {
  return {
    languageClientConfig: {
      languageId: 'python',
      options: {
        $type: 'WebSocketUrl',
        url: LSP_URL,
        startOptions: {
          onCall: () => {},
          reportStatus: false,
        },
        stopOptions: {
          onCall: () => {},
          reportStatus: false,
        },
      },
    },
    wrapperConfig: {
      serviceConfig: {
        userServices: {
          ...getKeybindingsServiceOverride(),
        },
        debugLogging: false,
      },
      editorAppConfig: {
        $type: 'extended',
        codeResources: {
          main: { text, uri },
        },
      },
    },
  };
};

interface EditorProps {
  value: string;
  uri: string;
  onTextChanged: (textChanges: TextChanges) => void;
}

const Editor: FC<EditorProps> = ({ value, uri, onTextChanged }) => {
  const config = CreateUserConfig({ text: value, uri });

  return (
    <Stack style={{ height: '100%' }} key={value} pt={4}>
      <MonacoEditorReactComp
        key={value}
        userConfig={config}
        style={{ height: '100%' }}
        onTextChanged={onTextChanged}
      />
    </Stack>
  );
};

export const PythonEditor = () => {
  // pull code saved in file from the server
  const { code, refetch, isFetching, isLoading, isRefetching } = useFile({ ... });
  const { cwd } = useGetPage({ ... });
  // handle updates
  const [updatedCode, setUpdatedCode] = useState<string>('');
  // Create a ref to store the updatedCode
  const updatedCodeRef = useRef(updatedCode);
  updatedCodeRef.current = updatedCode;

  // sync updatedCode with server copy when a new version received 
  useEffect(() => {
    if (code) {
      setUpdatedCode(code);
    }
  }, [code]);
  
  ...

  const handleTextChanged = (textChanges: TextChanges) => {
    const text = textChanges.main;
    setUpdatedCode(text);
  };

  useConfigureMonacoWorkers();

  return (
    <Stack h="full" bg="white" spacing="0" w="full">
        {isFetching || isLoading || isRefetching || !cwd ? (
          <Skeleton startColor="gray.100" endColor="gray.400" h="full" />
        ) : (
          <Editor
            key={code}
            value={code}
            uri={`${cwd}/workspace/${appName}/${pageName}/${fileName}`}
            onTextChanged={handleTextChanged}
          />
        )}
      </Tabs>
    </Stack>
  );
};

besides @TypeFox and @CodinGame packages, im using chakra ui, react query, and jotai.

editor works if only editor is used.

however, if the code is updated by some other components while MonacoEditorReactComp has its own changes (unsaved or even saved), even if useFile pulls a new version of the code and passes it to MonacoEditorReactComp, MonacoEditorReactComp will not update its text content (text).

i can confirm that Editor receives a new code with value={code} and that config is contains the latest code in config.wrapperConfig.editorAppConfig.codeResources.main.text.

i tried to trigger a new render of Editor by passing new code as new key, but no luck.

it is as if MonacoEditorReactComp is cached and won’t update it even if new config received.

how can i resolve this?

thanks!

@kaisalmen
Copy link
Collaborator

Hi @azhakhan are you aware of this https://github.com/TypeFox/monaco-languageclient/blob/main/packages/examples/src/python/client/reactPython.tsx#L47-L49 This way you can get hold of the wrapper and do whatever you want afterwards. When you want to update editor text you can do this via wrapper. The react component is basically just a shell around the monaco-editor-wrapper.

@azhakhan
Copy link
Author

hey @kaisalmen, thanks for the response!

i went over the methods in MonacoEditorLanguageClientWrapper and the one that seemed most relevant for my case was updateCodeResources, so i updated my code as:

			<MonacoEditorReactComp
				key={value}
				userConfig={config}
				style={{ height: '100%' }}
				onTextChanged={onTextChanged}
				onLoad={(editorWrapper: MonacoEditorLanguageClientWrapper) => {
					editorWrapper.updateCodeResources({
						main: { text: value, uri },
						original: { text: value, uri },
					});
				}}
			/>

my assumption here is that i need to reset main and original to the newly received code value.

however, the issue persists.

i also tried it with async and initAndStart

			<MonacoEditorReactComp
				key={`${value}_${uri}`} // unique key based on both value and uri
				userConfig={config}
				style={{ height: '100%' }}
				onTextChanged={onTextChanged}
				onLoad={async (editorWrapper: MonacoEditorLanguageClientWrapper) => {
					try {
						await editorWrapper.initAndStart(
							config,
							document.getElementById('editorContainer'),
						);
						editorWrapper.updateCodeResources({
							main: { text: value, uri },
							original: { text: value, uri },
						});
					} catch (error) {
						console.error('Failed to initialize editor wrapper:', error);
					}
				}}
			/>

but still getting caching issue.

am i missing something?

@kaisalmen
Copy link
Collaborator

kaisalmen commented Sep 25, 2024

@azhakhan The idea is that you store the wrapper that is passed in onLoad in your app. You don't have to call initAndStart. If you want to update the text (from your app) you just need to call updateCodeResources. onLoad let's you know the content of the editor was changed (e.g. everytime you typed something in the editor).

The latest version of the react-component (not yet released, I will publish a pre-release today or tomorrow and let you know) will perform a full re-init if an updated config is passed.

I hope this helps.

@azhakhan
Copy link
Author

hey @kaisalmen, thanks for the pointer!

i’ve made the following changes:

  • created wrapperRef to store reference to MonacoEditorLanguageClientWrapper
  • assign wrapperRef.current = editorWrapper when onLoad is called
  • update wrapperRef.current.updateCodeResources when value changes using useEffect
  • added isEditorLoaded flag to ensure wrapperRef.current updated only after editor is loaded
const Editor: FC<EditorProps> = ({ value, uri, onTextChanged }) => {
	const wrapperRef = useRef<MonacoEditorLanguageClientWrapper | null>(null);
	const [isEditorLoaded, setIsEditorLoaded] = useState(false);
	const [config, setConfig] = useState(() => CreateUserConfig({ text: value, uri }));

	useEffect(() => {
		setConfig(CreateUserConfig({ text: value, uri }));
	}, [value, uri]);

	useEffect(() => {
		if (isEditorLoaded && wrapperRef.current) {
			console.log('WRAPPERREF.CURRENT', wrapperRef.current);
			wrapperRef.current.updateCodeResources({
				main: { text: value, uri },
				original: { text: value, uri },
			});
		}
	}, [isEditorLoaded, value, uri]);

	const handleLoad = useCallback(async (editorWrapper: MonacoEditorLanguageClientWrapper) => {
		console.log('EDITORWRAPPER', editorWrapper);
		wrapperRef.current = editorWrapper;
		setIsEditorLoaded(true);
	}, []);

	return (
		<Stack style={{ height: '100%' }} key={`${uri}-editor`} pt={4}>
			<MonacoEditorReactComp
				key={`${uri}-editor`}
				userConfig={config}
				style={{ height: '100%' }}
				onTextChanged={onTextChanged}
				onLoad={handleLoad}
			/>
		</Stack>
	);
};

i can confirm that editorWrapper and wrapperRef.current have the same values

image

i also confirmed that wrapperRef.current has the right data in editorApp.config.codeResources.main.text

image

and the right MonacoEditorLanguageClientWrapper (id 10 in this case) is mounted to the Editor

image

however, the old value is still shown in the monaco editor (see options in Select, only shown instead of 3)

image

not sure what else to try here.

i'll wait for the latest version of the react-component and try again.

thanks again for helping me with this!

@azhakhan
Copy link
Author

added a button to manually call updateCodeResources and it updates the content of the editor

	const handleClick = () => {
		wrapperRef.current.updateCodeResources({
			main: { text: 'updated code', uri },
			original: { text: 'updated code', uri },
		});
	};

	return (
		<Stack style={{ height: '100%' }} key={`${uri}-editor`} pt={4}>
			<Button onClick={handleClick} variant="outline">
				click me
			</Button>
			<MonacoEditorReactComp
				key={`${uri}-editor`} // ensure key is unique to forcefully re-render when URI changes
				userConfig={config}
				style={{ height: '100%' }}
				onTextChanged={onTextChanged}
				onLoad={handleLoad}
			/>
		</Stack>
image

but the same updateCodeResources in useEffect does not work

@kaisalmen
Copy link
Collaborator

@azhakhan
Copy link
Author

azhakhan commented Oct 2, 2024

hey @kaisalmen, thanks for the update!

unfortunately im still running into the same issues

i tried updating to 6.0.0-next.1 version. i’ve updated the following in package.json:

    "@typefox/monaco-editor-react": "6.0.0-next.1",
    "monaco-editor-wrapper": "6.0.0-next.1",
    "monaco-languageclient": "9.0.0-next.1",
    "vscode-languageclient": "~9.0.1",
    "@codingame/monaco-vscode-keybindings-service-override": "^9.0.3",
    "@codingame/monaco-vscode-python-default-extension": "^9.0.3",

i’m using python-lsp-server for lsp, so here is how i setup config:

export const createUserConfig = (
  workspaceRoot: string,
  code: string,
  codeUri: string,
): WrapperConfig => {
  const url = createUrl({ url: LSP_URL });
  const webSocket = new WebSocket(url);
  const iWebSocket = toSocket(webSocket);
  const reader = new WebSocketMessageReader(iWebSocket);
  const writer = new WebSocketMessageWriter(iWebSocket);

  return {
    languageClientConfigs: {
      python: {
        languageId: 'python',
        name: 'Python Language Server',
        connection: {
          options: {
            $type: 'WebSocketDirect',
            webSocket,
          },
          messageTransports: { reader, writer },
        },
        clientOptions: {
          documentSelector: ['python'],
          workspaceFolder: {
            index: 0,
            name: 'workspace',
            uri: vscode.Uri.parse(workspaceRoot),
          },
        },
      },
    },

    // logLevel: LogLevel.Info,
    serviceConfig: {
      userServices: {
        ...getEditorServiceOverride(useOpenEditorStub),
        ...getKeybindingsServiceOverride(),
      },
    },
    editorAppConfig: {
      $type: 'extended',
      codeResources: {
        main: {
          text: code,
          uri: codeUri,
        },
        original: {
          text: code,
          uri: codeUri,
        },
      },
      userConfiguration: {
        json: JSON.stringify({
          'workbench.colorTheme': 'Default Light Modern',
          'editor.guides.bracketPairsHorizontal': 'active',
          'editor.wordBasedSuggestions': 'off',
          'editor.experimental.asyncTokenization': true,
        }),
      },
      useDiffEditor: false,
      monacoWorkerFactory: configureMonacoWorkers,
    },
  };
};

to be honest, i don’t fully understand what messageTransports: { reader, writer } is for, but it’s not breaking anything, so i kept it

and here is how i initiate MonacoEditorReactComp

export const FunctionEditor = () => {
  const [fileName, setSelectedFile] = useState('main.py');
  const { code, refetch, isFetching, isLoading, isRefetching } = useFile();
  const { cwd } = useGetPage();
  
  const workspaceRoot = `${cwd}/workspace/${appName}/${pageName}`;
  const codeUri = `${workspaceRoot}/${fileName}`;

  const wrapperRef = useRef<MonacoEditorLanguageClientWrapper | null>(null);
  const [tabIndex, setTabIndex] = useState(0);

  const codeRef = useRef(code);
  codeRef.current = code;

  useEffect(() => {
    if (wrapperRef.current) {
      wrapperRef.current.updateCodeResources({
        main: { text: code, uri: codeUri },
        original: { text: code, uri: codeUri },
      });
    }
  }, [code, codeUri]);

...

  const handleSave = useCallback(() => {
    const codeValue = wrapperRef.current?.getTextContents()?.text || '';
    savePythonMutation.mutate({ pageName, appName, fileName, code: codeValue });
  }, [appName, pageName, fileName, savePythonMutation]);

...

  const memoizedWrapperConfig = useMemo(
    () => createUserConfig(workspaceRoot, code, codeUri),
    [workspaceRoot, code, codeUri],
  );

  const handleLoad = useCallback(async (editorWrapper: MonacoEditorLanguageClientWrapper) => {
    wrapperRef.current = editorWrapper;
  }, []);

  return (
    <Stack h="full" bg="white" spacing="0" w="full">
      <Tabs>
        <TabList mt={2} px={4}>
          <Tab>main.py</Tab>
          <Tab>functions.py</Tab>
          <Spacer />
        </TabList>
        <TabPanels h="calc(100% - 50px)" w="full">
          
          <MonacoEditorReactComp
            style={{ height: '100%' }}
            wrapperConfig={memoizedWrapperConfig}
            onLoad={handleLoad}
          />
          
        </TabPanels>
      </Tabs>
    </Stack>
  );
};

i decided to ditch onTextChanged for edits tracking and instead access updated code directly via wrapperRef.current?.getTextContents()?.text

but im still running into the same issues as before:

when we the code is updated from outside of monaco and updates are passed to the editor via updateCodeResources, those changes are not taking place. i confirmed that updateCodeResources is called from within useEffect, but it seems like something else is overwriting the content of the editor from within MonacoEditorReactComp

before when i had onTextChanged implemented, i saw how right after updateCodeResources was called, onTextChanged was also called but with an old code.

also, because i need to edit 2 files (main.py and functions.py), i need 2 monaco editors. but if i keep code and uri in createUserConfig, a new web socket is opened every time i switch between editors. but if i initiate wrapperConfig without code and uri and instead set them via updateCodeResources after MonacoEditorReactComp is initiated, no code is displayed in the editor

@kaisalmen
Copy link
Collaborator

@azhakhan can you share a reproducible example in a repo or can you augment a react example in this repo so your problem can be reproduced? Thank you.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants