Skip to content

Commit 67506e4

Browse files
authored
Merge pull request #1099 from rust-lang/monaco-and-other-fonts
Create our own Monaco integration and unify fonts
2 parents d5c5816 + ae509b3 commit 67506e4

16 files changed

+195
-305
lines changed

ui/frontend/.eslintrc.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,7 +89,9 @@ module.exports = {
8989
'compileActions.ts',
9090
'configureStore.ts',
9191
'editor/AceEditor.tsx',
92+
'editor/MonacoEditorCore.tsx',
9293
'editor/SimpleEditor.tsx',
94+
'editor/rust_monaco_def.ts',
9395
'hooks.ts',
9496
'observer.ts',
9597
'prism-shim.ts',

ui/frontend/.prettierignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,9 @@ node_modules
2929
!compileActions.ts
3030
!configureStore.ts
3131
!editor/AceEditor.tsx
32+
!editor/MonacoEditorCore.tsx
3233
!editor/SimpleEditor.tsx
34+
!editor/rust_monaco_def.ts
3335
!hooks.ts
3436
!observer.ts
3537
!prism-shim.ts

ui/frontend/HelpExample.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import * as actions from './actions';
66
import { useAppDispatch } from './hooks';
77

88
import * as styles from './HelpExample.module.css';
9+
import prismOverrides from './prismjs-overrides.css';
910
import prismTheme from 'prismjs/themes/prism-okaidia.css';
1011

1112
export interface HelpExampleProps {
@@ -23,6 +24,7 @@ const HelpExample: React.FC<HelpExampleProps> = ({ code }) => {
2324
</button>
2425
<root.div>
2526
<link href={prismTheme} rel="stylesheet" />
27+
<link href={prismOverrides} rel="stylesheet" />
2628

2729
<Prism language="rust">{code}</Prism>
2830
</root.div>

ui/frontend/configureStore.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ const editorDarkThemes = {
1414
theme: 'github_dark',
1515
},
1616
monaco: {
17-
theme: 'vscode-dark-plus',
17+
theme: 'vs-dark',
1818
},
1919
},
2020
};

ui/frontend/declarations.d.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,11 @@ declare module 'prismjs/themes/*.css' {
88
export default content;
99
}
1010

11+
declare module '*prismjs-overrides.css' {
12+
const content: string;
13+
export default content;
14+
}
15+
1116
declare module '*.svg' {
1217
const content: string;
1318
export default content;
Lines changed: 150 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -1,54 +1,165 @@
1-
import React from 'react';
2-
import { CommonEditorProps } from '../types';
3-
import MonacoEditor, { EditorDidMount, EditorWillMount } from 'react-monaco-editor';
1+
import * as monaco from 'monaco-editor';
2+
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
3+
44
import { useAppSelector } from '../hooks';
5-
import { config, grammar, themeVsDarkPlus } from './rust_monaco_def';
5+
import { offerCrateAutocompleteOnUse } from '../selectors';
6+
import { CommonEditorProps } from '../types';
7+
import { themeVsDarkPlus } from './rust_monaco_def';
68

79
import * as styles from './Editor.module.css';
810

9-
const MODE_ID = 'rust';
11+
function useEditorProp<T>(
12+
editor: monaco.editor.IStandaloneCodeEditor | null,
13+
prop: T,
14+
whenPresent: (
15+
editor: monaco.editor.IStandaloneCodeEditor,
16+
model: monaco.editor.ITextModel,
17+
prop: T,
18+
) => void | (() => void),
19+
) {
20+
useEffect(() => {
21+
if (!editor) {
22+
return;
23+
}
1024

11-
const initMonaco: EditorWillMount = (monaco) => {
12-
monaco.editor.defineTheme('vscode-dark-plus', themeVsDarkPlus);
13-
monaco.languages.register({
14-
id: MODE_ID,
15-
});
25+
const model = editor.getModel();
26+
if (!model) {
27+
return;
28+
}
29+
30+
return whenPresent(editor, model, prop);
31+
}, [editor, prop, whenPresent]);
32+
}
33+
34+
const MonacoEditorCore: React.FC<CommonEditorProps> = (props) => {
35+
const [editor, setEditor] = useState<monaco.editor.IStandaloneCodeEditor | null>(null);
36+
const theme = useAppSelector((s) => s.configuration.monaco.theme);
37+
const completionProvider = useRef<monaco.IDisposable | null>(null);
38+
const autocompleteOnUse = useAppSelector(offerCrateAutocompleteOnUse);
39+
40+
// Replace `initialCode` and `initialTheme` with an "effect event"
41+
// when those stabilize.
42+
//
43+
// https://react.dev/learn/separating-events-from-effects#declaring-an-effect-event
44+
const initialCode = useRef(props.code);
45+
const initialTheme = useRef(theme);
1646

17-
monaco.languages.onLanguage(MODE_ID, async () => {
18-
monaco.languages.setLanguageConfiguration(MODE_ID, config);
19-
monaco.languages.setMonarchTokensProvider(MODE_ID, grammar);
47+
// One-time setup
48+
useEffect(() => {
49+
monaco.editor.defineTheme('vscode-dark-plus', themeVsDarkPlus);
50+
}, []);
51+
52+
// Construct the editor
53+
const child = useCallback((node: HTMLDivElement | null) => {
54+
if (!node) {
55+
return;
56+
}
57+
58+
const nodeStyle = window.getComputedStyle(node);
59+
60+
const editor = monaco.editor.create(node, {
61+
language: 'rust',
62+
value: initialCode.current,
63+
theme: initialTheme.current,
64+
fontSize: parseInt(nodeStyle.fontSize, 10),
65+
fontFamily: nodeStyle.fontFamily,
66+
automaticLayout: true,
67+
'semanticHighlighting.enabled': true,
68+
});
69+
setEditor(editor);
70+
71+
editor.focus();
72+
}, []);
73+
74+
useEditorProp(editor, props.onEditCode, (_editor, model, onEditCode) => {
75+
model.onDidChangeContent(() => {
76+
onEditCode(model.getValue());
77+
});
2078
});
21-
};
2279

23-
const initEditor = (execute: () => any): EditorDidMount => (editor, monaco) => {
24-
editor.focus();
25-
editor.addCommand(monaco.KeyMod.CtrlCmd | monaco.KeyCode.Enter, () => {
26-
execute();
80+
useEditorProp(editor, props.execute, (editor, _model, execute) => {
81+
editor.addCommand(monaco.KeyMod.CtrlCmd | monaco.KeyCode.Enter, () => {
82+
execute();
83+
});
84+
// Ace's Vim mode runs code with :w, so let's do the same
85+
editor.addCommand(monaco.KeyMod.CtrlCmd | monaco.KeyCode.KeyS, () => {
86+
execute();
87+
});
2788
});
28-
// Ace's Vim mode runs code with :w, so let's do the same
29-
editor.addCommand(monaco.KeyMod.CtrlCmd | monaco.KeyCode.KeyS, () => {
30-
execute();
89+
90+
useEditorProp(editor, props.code, (editor, model, code) => {
91+
// Short-circuit if nothing interesting to change.
92+
if (code === model.getValue()) {
93+
return;
94+
}
95+
96+
editor.executeEdits('redux', [
97+
{
98+
text: code,
99+
range: model.getFullModelRange(),
100+
},
101+
]);
31102
});
32-
};
33103

34-
const MonacoEditorCore: React.FC<CommonEditorProps> = props => {
35-
const theme = useAppSelector((s) => s.configuration.monaco.theme);
104+
useEditorProp(editor, theme, (editor, _model, theme) => {
105+
editor.updateOptions({ theme });
106+
});
36107

37-
return (
38-
<MonacoEditor
39-
language={MODE_ID}
40-
theme={theme}
41-
className={styles.monaco}
42-
value={props.code}
43-
onChange={props.onEditCode}
44-
editorWillMount={initMonaco}
45-
editorDidMount={initEditor(props.execute)}
46-
options={{
47-
automaticLayout: true,
48-
'semanticHighlighting.enabled': true,
49-
}}
50-
/>
108+
const autocompleteProps = useMemo(
109+
() => ({ autocompleteOnUse, crates: props.crates }),
110+
[autocompleteOnUse, props.crates],
51111
);
52-
}
112+
113+
useEditorProp(editor, autocompleteProps, (_editor, _model, { autocompleteOnUse, crates }) => {
114+
completionProvider.current = monaco.languages.registerCompletionItemProvider('rust', {
115+
triggerCharacters: [' '],
116+
117+
provideCompletionItems(model, position, _context, _token) {
118+
const word = model.getWordUntilPosition(position);
119+
120+
function wordBefore(
121+
word: monaco.editor.IWordAtPosition,
122+
): monaco.editor.IWordAtPosition | null {
123+
const prevPos = { lineNumber: position.lineNumber, column: word.startColumn - 1 };
124+
return model.getWordAtPosition(prevPos);
125+
}
126+
127+
const preWord = wordBefore(word);
128+
const prePreWord = preWord && wordBefore(preWord);
129+
130+
const oldStyle = prePreWord?.word === 'extern' && preWord?.word === 'crate';
131+
const newStyle = autocompleteOnUse && preWord?.word === 'use';
132+
133+
const triggerPrefix = oldStyle || newStyle;
134+
135+
if (!triggerPrefix) {
136+
return { suggestions: [] };
137+
}
138+
139+
const range = {
140+
startLineNumber: position.lineNumber,
141+
endLineNumber: position.lineNumber,
142+
startColumn: word.startColumn,
143+
endColumn: word.endColumn,
144+
};
145+
146+
const suggestions = crates.map(({ name, version, id }) => ({
147+
kind: monaco.languages.CompletionItemKind.Module,
148+
label: `${name} (${version})`,
149+
insertText: `${id}; // ${version}`,
150+
range,
151+
}));
152+
153+
return { suggestions };
154+
},
155+
});
156+
157+
return () => {
158+
completionProvider.current?.dispose();
159+
};
160+
});
161+
162+
return <div className={styles.monaco} ref={child} />;
163+
};
53164

54165
export default MonacoEditorCore;

0 commit comments

Comments
 (0)