Skip to content

Commit

Permalink
Merge pull request #1099 from rust-lang/monaco-and-other-fonts
Browse files Browse the repository at this point in the history
Create our own Monaco integration and unify fonts
  • Loading branch information
shepmaster authored Oct 2, 2024
2 parents d5c5816 + ae509b3 commit 67506e4
Show file tree
Hide file tree
Showing 16 changed files with 195 additions and 305 deletions.
2 changes: 2 additions & 0 deletions ui/frontend/.eslintrc.js
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,9 @@ module.exports = {
'compileActions.ts',
'configureStore.ts',
'editor/AceEditor.tsx',
'editor/MonacoEditorCore.tsx',
'editor/SimpleEditor.tsx',
'editor/rust_monaco_def.ts',
'hooks.ts',
'observer.ts',
'prism-shim.ts',
Expand Down
2 changes: 2 additions & 0 deletions ui/frontend/.prettierignore
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,9 @@ node_modules
!compileActions.ts
!configureStore.ts
!editor/AceEditor.tsx
!editor/MonacoEditorCore.tsx
!editor/SimpleEditor.tsx
!editor/rust_monaco_def.ts
!hooks.ts
!observer.ts
!prism-shim.ts
Expand Down
2 changes: 2 additions & 0 deletions ui/frontend/HelpExample.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import * as actions from './actions';
import { useAppDispatch } from './hooks';

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

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

<Prism language="rust">{code}</Prism>
</root.div>
Expand Down
2 changes: 1 addition & 1 deletion ui/frontend/configureStore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ const editorDarkThemes = {
theme: 'github_dark',
},
monaco: {
theme: 'vscode-dark-plus',
theme: 'vs-dark',
},
},
};
Expand Down
5 changes: 5 additions & 0 deletions ui/frontend/declarations.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,11 @@ declare module 'prismjs/themes/*.css' {
export default content;
}

declare module '*prismjs-overrides.css' {
const content: string;
export default content;
}

declare module '*.svg' {
const content: string;
export default content;
Expand Down
189 changes: 150 additions & 39 deletions ui/frontend/editor/MonacoEditorCore.tsx
Original file line number Diff line number Diff line change
@@ -1,54 +1,165 @@
import React from 'react';
import { CommonEditorProps } from '../types';
import MonacoEditor, { EditorDidMount, EditorWillMount } from 'react-monaco-editor';
import * as monaco from 'monaco-editor';
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';

import { useAppSelector } from '../hooks';
import { config, grammar, themeVsDarkPlus } from './rust_monaco_def';
import { offerCrateAutocompleteOnUse } from '../selectors';
import { CommonEditorProps } from '../types';
import { themeVsDarkPlus } from './rust_monaco_def';

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

const MODE_ID = 'rust';
function useEditorProp<T>(
editor: monaco.editor.IStandaloneCodeEditor | null,
prop: T,
whenPresent: (
editor: monaco.editor.IStandaloneCodeEditor,
model: monaco.editor.ITextModel,
prop: T,
) => void | (() => void),
) {
useEffect(() => {
if (!editor) {
return;
}

const initMonaco: EditorWillMount = (monaco) => {
monaco.editor.defineTheme('vscode-dark-plus', themeVsDarkPlus);
monaco.languages.register({
id: MODE_ID,
});
const model = editor.getModel();
if (!model) {
return;
}

return whenPresent(editor, model, prop);
}, [editor, prop, whenPresent]);
}

const MonacoEditorCore: React.FC<CommonEditorProps> = (props) => {
const [editor, setEditor] = useState<monaco.editor.IStandaloneCodeEditor | null>(null);
const theme = useAppSelector((s) => s.configuration.monaco.theme);
const completionProvider = useRef<monaco.IDisposable | null>(null);
const autocompleteOnUse = useAppSelector(offerCrateAutocompleteOnUse);

// Replace `initialCode` and `initialTheme` with an "effect event"
// when those stabilize.
//
// https://react.dev/learn/separating-events-from-effects#declaring-an-effect-event
const initialCode = useRef(props.code);
const initialTheme = useRef(theme);

monaco.languages.onLanguage(MODE_ID, async () => {
monaco.languages.setLanguageConfiguration(MODE_ID, config);
monaco.languages.setMonarchTokensProvider(MODE_ID, grammar);
// One-time setup
useEffect(() => {
monaco.editor.defineTheme('vscode-dark-plus', themeVsDarkPlus);
}, []);

// Construct the editor
const child = useCallback((node: HTMLDivElement | null) => {
if (!node) {
return;
}

const nodeStyle = window.getComputedStyle(node);

const editor = monaco.editor.create(node, {
language: 'rust',
value: initialCode.current,
theme: initialTheme.current,
fontSize: parseInt(nodeStyle.fontSize, 10),
fontFamily: nodeStyle.fontFamily,
automaticLayout: true,
'semanticHighlighting.enabled': true,
});
setEditor(editor);

editor.focus();
}, []);

useEditorProp(editor, props.onEditCode, (_editor, model, onEditCode) => {
model.onDidChangeContent(() => {
onEditCode(model.getValue());
});
});
};

const initEditor = (execute: () => any): EditorDidMount => (editor, monaco) => {
editor.focus();
editor.addCommand(monaco.KeyMod.CtrlCmd | monaco.KeyCode.Enter, () => {
execute();
useEditorProp(editor, props.execute, (editor, _model, execute) => {
editor.addCommand(monaco.KeyMod.CtrlCmd | monaco.KeyCode.Enter, () => {
execute();
});
// Ace's Vim mode runs code with :w, so let's do the same
editor.addCommand(monaco.KeyMod.CtrlCmd | monaco.KeyCode.KeyS, () => {
execute();
});
});
// Ace's Vim mode runs code with :w, so let's do the same
editor.addCommand(monaco.KeyMod.CtrlCmd | monaco.KeyCode.KeyS, () => {
execute();

useEditorProp(editor, props.code, (editor, model, code) => {
// Short-circuit if nothing interesting to change.
if (code === model.getValue()) {
return;
}

editor.executeEdits('redux', [
{
text: code,
range: model.getFullModelRange(),
},
]);
});
};

const MonacoEditorCore: React.FC<CommonEditorProps> = props => {
const theme = useAppSelector((s) => s.configuration.monaco.theme);
useEditorProp(editor, theme, (editor, _model, theme) => {
editor.updateOptions({ theme });
});

return (
<MonacoEditor
language={MODE_ID}
theme={theme}
className={styles.monaco}
value={props.code}
onChange={props.onEditCode}
editorWillMount={initMonaco}
editorDidMount={initEditor(props.execute)}
options={{
automaticLayout: true,
'semanticHighlighting.enabled': true,
}}
/>
const autocompleteProps = useMemo(
() => ({ autocompleteOnUse, crates: props.crates }),
[autocompleteOnUse, props.crates],
);
}

useEditorProp(editor, autocompleteProps, (_editor, _model, { autocompleteOnUse, crates }) => {
completionProvider.current = monaco.languages.registerCompletionItemProvider('rust', {
triggerCharacters: [' '],

provideCompletionItems(model, position, _context, _token) {
const word = model.getWordUntilPosition(position);

function wordBefore(
word: monaco.editor.IWordAtPosition,
): monaco.editor.IWordAtPosition | null {
const prevPos = { lineNumber: position.lineNumber, column: word.startColumn - 1 };
return model.getWordAtPosition(prevPos);
}

const preWord = wordBefore(word);
const prePreWord = preWord && wordBefore(preWord);

const oldStyle = prePreWord?.word === 'extern' && preWord?.word === 'crate';
const newStyle = autocompleteOnUse && preWord?.word === 'use';

const triggerPrefix = oldStyle || newStyle;

if (!triggerPrefix) {
return { suggestions: [] };
}

const range = {
startLineNumber: position.lineNumber,
endLineNumber: position.lineNumber,
startColumn: word.startColumn,
endColumn: word.endColumn,
};

const suggestions = crates.map(({ name, version, id }) => ({
kind: monaco.languages.CompletionItemKind.Module,
label: `${name} (${version})`,
insertText: `${id}; // ${version}`,
range,
}));

return { suggestions };
},
});

return () => {
completionProvider.current?.dispose();
};
});

return <div className={styles.monaco} ref={child} />;
};

export default MonacoEditorCore;
Loading

0 comments on commit 67506e4

Please sign in to comment.