Skip to content

Commit

Permalink
Fix interferring monaco editor instances (#702)
Browse files Browse the repository at this point in the history
* Fix interferring monaco editor instances

* terser

* defaults

* types
  • Loading branch information
Janpot authored Jul 28, 2022
1 parent f3e4b95 commit 7bdbf23
Show file tree
Hide file tree
Showing 4 changed files with 181 additions and 40 deletions.
19 changes: 10 additions & 9 deletions packages/toolpad-app/src/components/JsonEditor.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,17 +3,17 @@
*/

import * as React from 'react';
import MonacoEditor, { MonacoEditorHandle, MonacoEditorProps } from './MonacoEditor';
import type * as monaco from 'monaco-editor';
import MonacoEditor, { MonacoEditorProps } from './MonacoEditor';

export interface JsonEditorProps extends Omit<MonacoEditorProps, 'language'> {
export interface JsonEditorProps
extends Omit<MonacoEditorProps, 'language' | 'diagnostics' | 'extraLibs' | 'compilerOptions'> {
schemaUri?: string;
}

export default function JsonEditor({ schemaUri, ...props }: JsonEditorProps) {
const editorRef = React.useRef<MonacoEditorHandle>(null);

React.useEffect(() => {
editorRef.current?.monaco.languages.json.jsonDefaults.setDiagnosticsOptions({
const diagnostics = React.useMemo<monaco.languages.json.DiagnosticsOptions>(
() => ({
validate: true,
schemaRequest: 'error',
enableSchemaRequest: true,
Expand All @@ -25,8 +25,9 @@ export default function JsonEditor({ schemaUri, ...props }: JsonEditorProps) {
},
]
: [],
});
}, [schemaUri]);
}),
[schemaUri],
);

return <MonacoEditor ref={editorRef} language="json" {...props} />;
return <MonacoEditor language="json" diagnostics={diagnostics} {...props} />;
}
160 changes: 148 additions & 12 deletions packages/toolpad-app/src/components/MonacoEditor.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,22 @@ import { styled, SxProps } from '@mui/material';
import clsx from 'clsx';
import cuid from 'cuid';
import invariant from 'invariant';
import {
conf as jsonBasicConf,
language as jsonBasicLanguage,
} from 'monaco-editor/esm/vs/basic-languages/javascript/javascript';
import {
conf as typescriptBasicConf,
language as typescriptBasicLanguage,
} from 'monaco-editor/esm/vs/basic-languages/typescript/typescript';
import monacoEditorTheme from '../monacoEditorTheme';
import muiTheme from '../theme';

export interface ExtraLib {
content: string;
filePath?: string;
}

function getExtension(language: string): string {
switch (language) {
case 'typescript':
Expand Down Expand Up @@ -58,6 +71,29 @@ window.MonacoEnvironment = {
},
} as monaco.Environment;

function registerLanguage(
langId: string,
language: monaco.languages.IMonarchLanguage,
conf: monaco.languages.LanguageConfiguration,
) {
monaco.languages.register({ id: langId });
monaco.languages.registerTokensProviderFactory(langId, {
create: async (): Promise<monaco.languages.IMonarchLanguage> => language,
});
monaco.languages.onLanguage(langId, async () => {
monaco.languages.setLanguageConfiguration(langId, conf);
});
}

/**
* Monaco language services are singletons, we can't set language options per editor instance.
* We're working around this limitiation by only considering diagnostics for the focused editor.
* Unfocused editors will be configured with a syntax-coloring-only language which are registered below.
* See https://github.com/microsoft/monaco-editor/issues/1105
*/
registerLanguage('jsonBasic', jsonBasicLanguage, jsonBasicConf);
registerLanguage('typescriptBasic', typescriptBasicLanguage, typescriptBasicConf);

monaco.languages.typescript.typescriptDefaults.setCompilerOptions({
target: monaco.languages.typescript.ScriptTarget.Latest,
allowNonTsExtensions: true,
Expand All @@ -71,10 +107,35 @@ monaco.languages.typescript.typescriptDefaults.setCompilerOptions({
typeRoots: ['node_modules/@types'],
});

monaco.languages.typescript.typescriptDefaults.setDiagnosticsOptions({
const JSON_DEFAULT_DIAGNOSTICS_OPTIONS: monaco.languages.json.DiagnosticsOptions = {};

monaco.languages.json.jsonDefaults.setDiagnosticsOptions(JSON_DEFAULT_DIAGNOSTICS_OPTIONS);

const TYPESCRIPT_DEFAULT_DIAGNOSTICS_OPTIONS: monaco.languages.typescript.DiagnosticsOptions = {
noSemanticValidation: false,
noSyntaxValidation: false,
});
};

monaco.languages.typescript.typescriptDefaults.setDiagnosticsOptions(
TYPESCRIPT_DEFAULT_DIAGNOSTICS_OPTIONS,
);

const TYPESCRIPT_DEFAULT_COMPILER_OPTIONS: monaco.languages.typescript.CompilerOptions = {
target: monaco.languages.typescript.ScriptTarget.Latest,
allowNonTsExtensions: true,
moduleResolution: monaco.languages.typescript.ModuleResolutionKind.NodeJs,
module: monaco.languages.typescript.ModuleKind.CommonJS,
noEmit: true,
esModuleInterop: true,
jsx: monaco.languages.typescript.JsxEmit.React,
reactNamespace: 'React',
allowJs: true,
typeRoots: ['node_modules/@types'],
};

monaco.languages.typescript.typescriptDefaults.setCompilerOptions(
TYPESCRIPT_DEFAULT_COMPILER_OPTIONS,
);

const classes = {
monacoHost: 'Toolpad_MonacoEditorMonacoHost',
Expand Down Expand Up @@ -128,25 +189,49 @@ export interface MonacoEditorHandle {

type EditorOptions = monaco.editor.IEditorOptions & monaco.editor.IGlobalEditorOptions;

export interface MonacoEditorProps {
interface MonacoEditorBaseProps {
value?: string;
onChange?: (newValue: string) => void;
disabled?: boolean;
sx?: SxProps;
autoFocus?: boolean;
language?: string;
onFocus?: () => void;
onBlur?: () => void;
options?: EditorOptions;
className?: string;
}

export type MonacoEditorProps = MonacoEditorBaseProps &
(
| {
language?: 'typescript' | undefined;
diagnostics?: monaco.languages.typescript.DiagnosticsOptions;
compilerOptions?: monaco.languages.typescript.CompilerOptions | undefined;
extraLibs?: ExtraLib[];
}
| {
language: 'json';
diagnostics?: monaco.languages.json.DiagnosticsOptions;
compilerOptions?: undefined;
extraLibs?: undefined;
}
| {
language: 'css';
diagnostics?: undefined;
compilerOptions?: undefined;
extraLibs?: undefined;
}
);

export default React.forwardRef<MonacoEditorHandle, MonacoEditorProps>(function MonacoEditor(
{
value,
onChange,
sx,
language = 'typescript',
diagnostics,
compilerOptions,
extraLibs,
onFocus,
onBlur,
className,
Expand All @@ -159,6 +244,50 @@ export default React.forwardRef<MonacoEditorHandle, MonacoEditorProps>(function
const rootRef = React.useRef<HTMLDivElement>(null);
const instanceRef = React.useRef<monaco.editor.IStandaloneCodeEditor | null>(null);

const [isFocused, setIsFocused] = React.useState(false);

React.useEffect(() => {
/**
* Update the language and diagnostics of the currently focused editor. Non-focused editors
* will get a syntax-coloring-only version of the language.
* This is our workaround for having different diagnostics options per editor instance.
* See https://github.com/microsoft/monaco-editor/issues/1105
*/
const model = instanceRef.current?.getModel();
if (!model) {
return;
}

if (language === 'json') {
if (isFocused) {
monaco.editor.setModelLanguage(model, 'json');
monaco.languages.json.jsonDefaults.setDiagnosticsOptions({
...JSON_DEFAULT_DIAGNOSTICS_OPTIONS,
...(diagnostics as monaco.languages.json.DiagnosticsOptions),
});
} else {
monaco.editor.setModelLanguage(model, 'jsonBasic');
}
} else if (language === 'typescript') {
if (isFocused) {
monaco.editor.setModelLanguage(model, 'typescript');
monaco.languages.typescript.typescriptDefaults.setDiagnosticsOptions({
...TYPESCRIPT_DEFAULT_DIAGNOSTICS_OPTIONS,
...(diagnostics as monaco.languages.typescript.DiagnosticsOptions),
});
monaco.languages.typescript.typescriptDefaults.setCompilerOptions({
...TYPESCRIPT_DEFAULT_COMPILER_OPTIONS,
...compilerOptions,
});
monaco.languages.typescript.typescriptDefaults.setExtraLibs(extraLibs || []);
} else {
monaco.editor.setModelLanguage(model, 'typescriptBasic');
}
} else {
monaco.editor.setModelLanguage(model, language);
}
}, [isFocused, language, diagnostics, extraLibs, compilerOptions]);

React.useEffect(() => {
invariant(rootRef.current, 'Ref not attached');

Expand All @@ -167,36 +296,38 @@ export default React.forwardRef<MonacoEditorHandle, MonacoEditorProps>(function
...options,
};

if (instanceRef.current) {
let instance = instanceRef.current;

if (instance) {
if (extraOptions) {
instanceRef.current.updateOptions(extraOptions);
instance.updateOptions(extraOptions);
}

const model = instanceRef.current.getModel();
const model = instance.getModel();
if (typeof value === 'string' && model) {
const actualValue = model.getValue();

if (value !== actualValue) {
// Used to restore cursor position
const state = instanceRef.current.saveViewState();
const state = instance.saveViewState();

instanceRef.current.executeEdits(null, [
instance.executeEdits(null, [
{
range: model.getFullModelRange(),
text: value,
},
]);

if (state) {
instanceRef.current.restoreViewState(state);
instance.restoreViewState(state);
}
}
}
} else {
const pathUri = monaco.Uri.parse(`./scripts/${cuid()}${getExtension(language)}`);
const model = monaco.editor.createModel(value || '', language, pathUri);

instanceRef.current = monaco.editor.create(rootRef.current, {
instance = monaco.editor.create(rootRef.current, {
model,
language,
minimap: { enabled: false },
Expand All @@ -210,8 +341,13 @@ export default React.forwardRef<MonacoEditorHandle, MonacoEditorProps>(function
...extraOptions,
});

instanceRef.current = instance;

instance.onDidFocusEditorWidget(() => setIsFocused(true));
instance.onDidBlurEditorWidget(() => setIsFocused(false));

if (autoFocus && !disabled) {
instanceRef.current.focus();
instance.focus();
}
}
}, [language, value, options, disabled, autoFocus]);
Expand Down
29 changes: 10 additions & 19 deletions packages/toolpad-app/src/components/TypescriptEditor.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,31 +3,22 @@
*/

import * as React from 'react';
import MonacoEditor, { MonacoEditorHandle, MonacoEditorProps } from './MonacoEditor';
import type * as monaco from 'monaco-editor';
import MonacoEditor, { MonacoEditorProps } from './MonacoEditor';

export interface TypescriptEditorProps extends Omit<MonacoEditorProps, 'language'> {
export interface TypescriptEditorProps extends Omit<MonacoEditorProps, 'language' | 'diagnostics'> {
value: string;
onChange: (newValue: string) => void;
extraLibs?: { content: string; filePath?: string }[];
functionBody?: boolean;
}

export default function TypescriptEditor({
extraLibs,
functionBody,
...props
}: TypescriptEditorProps) {
const editorRef = React.useRef<MonacoEditorHandle>(null);

React.useEffect(() => {
editorRef.current?.monaco.languages.typescript.typescriptDefaults.setExtraLibs(extraLibs || []);
}, [extraLibs]);

React.useEffect(() => {
editorRef.current?.monaco.languages.typescript.typescriptDefaults.setDiagnosticsOptions({
export default function TypescriptEditor({ functionBody, ...props }: TypescriptEditorProps) {
const diagnostics = React.useMemo<monaco.languages.typescript.DiagnosticsOptions>(
() => ({
diagnosticCodesToIgnore: functionBody ? [1108] : [],
});
}, [functionBody]);
}),
[functionBody],
);

return <MonacoEditor ref={editorRef} language="typescript" {...props} />;
return <MonacoEditor language="typescript" diagnostics={diagnostics} {...props} />;
}
13 changes: 13 additions & 0 deletions packages/toolpad-app/typings/monaco-editor.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
declare module 'monaco-editor/esm/vs/basic-languages/javascript/javascript' {
import type * as monaco from 'monaco-editor';

export const conf: monaco.languages.LanguageConfiguration;
export const language: monaco.languages.IMonarchLanguage;
}

declare module 'monaco-editor/esm/vs/basic-languages/typescript/typescript' {
import type * as monaco from 'monaco-editor';

export const conf: monaco.languages.LanguageConfiguration;
export const language: monaco.languages.IMonarchLanguage;
}

0 comments on commit 7bdbf23

Please sign in to comment.