From c0f639c097643cfceb044983be897f2d09711cf2 Mon Sep 17 00:00:00 2001 From: hkalbasi Date: Tue, 14 Jun 2022 19:35:37 +0430 Subject: [PATCH] Add rust-analyzer-wasm --- ui/frontend/ConfigElement.tsx | 16 ++ ui/frontend/ConfigMenu.tsx | 13 +- ui/frontend/Header.tsx | 36 ++- ui/frontend/editor/MonacoEditorCore.tsx | 2 + ui/frontend/editor/rust_monaco_def.ts | 10 +- ui/frontend/generate-crate-src.sh | 21 ++ .../intellisense/ConfigMenu.module.css | 23 ++ ui/frontend/intellisense/ConfigMenu.tsx | 69 +++++ ui/frontend/intellisense/config.ts | 38 +++ ui/frontend/intellisense/crates.ts | 47 ++++ ui/frontend/intellisense/index.ts | 245 ++++++++++++++++++ ui/frontend/intellisense/ra-worker.js | 30 +++ 12 files changed, 545 insertions(+), 5 deletions(-) create mode 100644 ui/frontend/generate-crate-src.sh create mode 100644 ui/frontend/intellisense/ConfigMenu.module.css create mode 100644 ui/frontend/intellisense/ConfigMenu.tsx create mode 100644 ui/frontend/intellisense/config.ts create mode 100644 ui/frontend/intellisense/crates.ts create mode 100644 ui/frontend/intellisense/index.ts create mode 100644 ui/frontend/intellisense/ra-worker.js diff --git a/ui/frontend/ConfigElement.tsx b/ui/frontend/ConfigElement.tsx index 3872f7e71..85a783986 100644 --- a/ui/frontend/ConfigElement.tsx +++ b/ui/frontend/ConfigElement.tsx @@ -4,6 +4,22 @@ import MenuItem from './MenuItem'; import styles from './ConfigElement.module.css'; +interface ButtonProps extends ConfigElementProps { + id: string; + label: string; + onClick: () => any; +} + +export const Button: React.SFC = + ({ label, onClick, ...rest }) => ( + +
+ +
+
+ ); + + interface EitherProps extends ConfigElementProps { id: string; a: string; diff --git a/ui/frontend/ConfigMenu.tsx b/ui/frontend/ConfigMenu.tsx index fc457f5b5..1a1dcb32d 100644 --- a/ui/frontend/ConfigMenu.tsx +++ b/ui/frontend/ConfigMenu.tsx @@ -3,7 +3,7 @@ import React, { Fragment, useCallback } from 'react'; import { useDispatch, useSelector } from 'react-redux'; -import { Either as EitherConfig, Select as SelectConfig } from './ConfigElement'; +import { Either as EitherConfig, Select as SelectConfig, Button as ButtonConfig } from './ConfigElement'; import MenuGroup from './MenuGroup'; import * as actions from './actions'; @@ -16,6 +16,8 @@ import { PairCharacters, ProcessAssembly, } from './types'; +import { isEnable, enableIntellisense } from './intellisense/config'; +import { configDialog } from './intellisense/ConfigMenu'; interface ConfigMenuProps { close: () => void; @@ -94,6 +96,15 @@ const ConfigMenu: React.FC = () => { > {MONACO_THEMES.map(t => )} + {isEnable() + ? + : } )} diff --git a/ui/frontend/Header.tsx b/ui/frontend/Header.tsx index b77f5b7d3..a04330697 100644 --- a/ui/frontend/Header.tsx +++ b/ui/frontend/Header.tsx @@ -1,4 +1,4 @@ -import React, { useCallback } from 'react'; +import React, { useCallback, useState } from 'react'; import { useSelector } from 'react-redux'; import AdvancedOptionsMenu from './AdvancedOptionsMenu'; @@ -17,6 +17,9 @@ import * as selectors from './selectors'; import { useAppDispatch } from './configureStore'; import styles from './Header.module.css'; +import { enableIntellisense, getIntellisenseConfig } from './intellisense/config'; +import { State } from './reducers'; +import { Editor } from './types'; const Header: React.FC = () => (
@@ -33,6 +36,11 @@ const Header: React.FC = () => ( + + + + + @@ -129,7 +137,31 @@ const AdvancedOptionsMenuButton: React.FC = () => { return ; } -const ShareButton: React.FC = () => { +const IntellisenseButton: React.FC = () => { + const [dismissed, setDismiss] = useState(false); + const dispatch = useAppDispatch(); + const editorStyle = useSelector((state: State) => state.configuration.editor); + if (dismissed || !getIntellisenseConfig().suggest) { + return <>; + } + const enable = () => { + if (editorStyle != Editor.Monaco) { + dispatch(actions.changeEditor(Editor.Monaco)); + } + enableIntellisense(); + }; + const dismiss: React.MouseEventHandler = (e) => { + e.stopPropagation(); + setDismiss(true); + }; + return ( + + Enable Rust-analyzer + + ); +}; + +const ShareButton: React.SFC = () => { const dispatch = useAppDispatch(); const gistSave = useCallback(() => dispatch(actions.performGistSave()), [dispatch]); diff --git a/ui/frontend/editor/MonacoEditorCore.tsx b/ui/frontend/editor/MonacoEditorCore.tsx index cb20014d3..e2064a29b 100644 --- a/ui/frontend/editor/MonacoEditorCore.tsx +++ b/ui/frontend/editor/MonacoEditorCore.tsx @@ -6,6 +6,7 @@ import State from '../state'; import { config, grammar, themeVsDarkPlus } from './rust_monaco_def'; import styles from './Editor.module.css'; +import { enableOnMonaco } from '../intellisense'; const MODE_ID = 'rust'; @@ -26,6 +27,7 @@ const initEditor = (execute: () => any): EditorDidMount => (editor, monaco) => { editor.addCommand(monaco.KeyMod.CtrlCmd | monaco.KeyCode.Enter, () => { execute(); }); + enableOnMonaco(editor, monaco); // Ace's Vim mode runs code with :w, so let's do the same editor.addCommand(monaco.KeyMod.CtrlCmd | monaco.KeyCode.KeyS, () => { execute(); diff --git a/ui/frontend/editor/rust_monaco_def.ts b/ui/frontend/editor/rust_monaco_def.ts index 8b7b4be31..a91d83fc9 100644 --- a/ui/frontend/editor/rust_monaco_def.ts +++ b/ui/frontend/editor/rust_monaco_def.ts @@ -39,7 +39,7 @@ export const grammar: languages.IMonarchLanguage = { 'as', 'break', 'const', 'crate', 'enum', 'extern', 'false', 'fn', 'impl', 'in', 'let', 'mod', 'move', 'mut', 'pub', 'ref', 'return', 'self', 'Self', 'static', 'struct', 'super', 'trait', 'true', 'type', 'unsafe', 'use', 'where', - 'macro_rules', + 'macro_rules', 'async', 'await', ], controlFlowKeywords: [ @@ -80,6 +80,7 @@ export const grammar: languages.IMonarchLanguage = { '@keywords': { cases: { 'fn': { token: 'keyword', next: '@func_decl' }, + 'const': { token: 'keyword', next: '@const_decl' }, '@default': 'keyword', }, }, @@ -157,7 +158,12 @@ export const grammar: languages.IMonarchLanguage = { func_decl: [ [ - /[a-z_$][\w$]*/, 'support.function', '@pop', + /[a-zA-Z_$][\w$]*/, 'support.function', '@pop', + ], + ], + const_decl: [ + [ + /[a-zA-Z_$][\w$]*/, 'variable.constant', '@pop', ], ], }, diff --git a/ui/frontend/generate-crate-src.sh b/ui/frontend/generate-crate-src.sh new file mode 100644 index 000000000..e051d5a43 --- /dev/null +++ b/ui/frontend/generate-crate-src.sh @@ -0,0 +1,21 @@ +rm -rf build/assets/crate-src/ +mkdir build/assets/crate-src/ +cd build/assets/crate-src/ +syn-file-expand-cli -c unix -c "not(test)" -c "not(no_global_oom_handling)" --loopify $(rustc --print sysroot)/lib/rustlib/src/rust/library/core/src/lib.rs > core_stable_1_58.rs +syn-file-expand-cli -c unix -c "not(test)" -c "not(no_global_oom_handling)" --loopify $(rustc --print sysroot)/lib/rustlib/src/rust/library/alloc/src/lib.rs > alloc_stable_1_58.rs +syn-file-expand-cli -c unix -c "not(test)" -c "not(no_global_oom_handling)" --loopify $(rustc --print sysroot)/lib/rustlib/src/rust/library/std/src/lib.rs > std_stable_1_58.rs +# Switch to beta +syn-file-expand-cli -c unix -c "not(test)" -c "not(no_global_oom_handling)" --loopify $(rustc --print sysroot)/lib/rustlib/src/rust/library/core/src/lib.rs > core_beta_1_59.rs +syn-file-expand-cli -c unix -c "not(test)" -c "not(no_global_oom_handling)" --loopify $(rustc --print sysroot)/lib/rustlib/src/rust/library/alloc/src/lib.rs > alloc_beta_1_59.rs +syn-file-expand-cli -c unix -c "not(test)" -c "not(no_global_oom_handling)" --loopify $(rustc --print sysroot)/lib/rustlib/src/rust/library/std/src/lib.rs > std_beta_1_59.rs +# Switch to nightly +syn-file-expand-cli -c unix -c "not(test)" -c "not(no_global_oom_handling)" --loopify $(rustc --print sysroot)/lib/rustlib/src/rust/library/core/src/lib.rs > core_nightly_2022_03_07.rs +syn-file-expand-cli -c unix -c "not(test)" -c "not(no_global_oom_handling)" --loopify $(rustc --print sysroot)/lib/rustlib/src/rust/library/alloc/src/lib.rs > alloc_nightly_2022_03_07.rs +syn-file-expand-cli -c unix -c "not(test)" -c "not(no_global_oom_handling)" --loopify $(rustc --print sysroot)/lib/rustlib/src/rust/library/std/src/lib.rs > std_nightly_2022_03_07.rs +echo "{" > index.json +echo '"stdlib": [' >> index.json +echo '"stable_1_58",' >> index.json +echo '"beta_1_59",' >> index.json +echo '"nightly_2022_03_07"' >> index.json +echo ']' >> index.json +echo '}' >> index.json diff --git a/ui/frontend/intellisense/ConfigMenu.module.css b/ui/frontend/intellisense/ConfigMenu.module.css new file mode 100644 index 000000000..fd280807c --- /dev/null +++ b/ui/frontend/intellisense/ConfigMenu.module.css @@ -0,0 +1,23 @@ +.root { + position: fixed; + top: 0; + left: 0; + height: 100vh; + width: 100vw; + background-color: gray; + opacity: .7; + z-index: 5000; +} + +.main { + position: fixed; + top: 10vh; + left: 10vw; + height: 80vh; + width: 80vw; + background-color: black; + color: white; + border-radius: 2rem; + padding: 2rem; + z-index: 5001; +} \ No newline at end of file diff --git a/ui/frontend/intellisense/ConfigMenu.tsx b/ui/frontend/intellisense/ConfigMenu.tsx new file mode 100644 index 000000000..648508d34 --- /dev/null +++ b/ui/frontend/intellisense/ConfigMenu.tsx @@ -0,0 +1,69 @@ +import React, { useEffect, useState } from 'react'; +import ReactDOM from 'react-dom'; +import { getIntellisenseConfig, setConfig } from './config'; +import styles from './ConfigMenu.module.css'; +import { availableVersions, selectVersion } from './crates'; + +const ConfigMenu: React.FC<{ onDone: (result?: string | undefined) => void }> = ({ onDone }) => { + const { enable, diagnostic } = getIntellisenseConfig(); + const [nextEnable, setEnable] = useState(enable); + const [nextDiag, setDiag] = useState(diagnostic); + const [nextStdlib, setNextStdlib] = useState(undefined); + const [stdlibVersions, setStdlibVersions] = useState(undefined); + useEffect(() => { + (async () => { + const x = await availableVersions('stdlib'); + setStdlibVersions(x); + setNextStdlib(x.selected); + })(); + }, []); + let stdlibVersionsElement = <>Loading...; + if (stdlibVersions) { + stdlibVersionsElement = ( + <> + setNextStdlib(stdlibVersions.selected)}> + {stdlibVersions.selected} (downloaded) + + {stdlibVersions.others.map((x: string) => setNextStdlib(x)}> + {' '}{x} + )} + + ) + } + return ( +
+
+
+ Enabled: setEnable(!nextEnable)} /> +
+ {nextEnable && <> + Diagnostics: setDiag(!nextDiag)} /> +
+ Standard library version: {stdlibVersionsElement} + } +
+ +
+
onDone()} /> +
+ ); +}; + +export const configDialog = (): Promise => { + const div = document.createElement('div'); + document.body.appendChild(div); + return new Promise((res) => { + ReactDOM.render( { + document.body.removeChild(div); + res(result); + }} />, div); + }); +}; diff --git a/ui/frontend/intellisense/config.ts b/ui/frontend/intellisense/config.ts new file mode 100644 index 000000000..1509425e2 --- /dev/null +++ b/ui/frontend/intellisense/config.ts @@ -0,0 +1,38 @@ +const storageKey = 'intellisence'; + +type Config = { + enable: boolean, + suggest: boolean, + diagnostic: boolean, +}; + +export const setConfig = (x: Config) => { + window.localStorage.setItem(storageKey, JSON.stringify(x)); +}; + +export const getIntellisenseConfig = (): Config => { + const v = window.localStorage.getItem(storageKey); + if (!v) { + setConfig({ + enable: false, + suggest: true, + diagnostic: false, + }); + return getIntellisenseConfig(); + } + return JSON.parse(v); +}; + +export const isEnable = (): boolean => { + return getIntellisenseConfig().enable; +}; + +export const enableIntellisense = () => { + setConfig({ + ...getIntellisenseConfig(), + enable: true, + suggest: false, + }); + window.location.reload(); +}; + diff --git a/ui/frontend/intellisense/crates.ts b/ui/frontend/intellisense/crates.ts new file mode 100644 index 000000000..717525d4d --- /dev/null +++ b/ui/frontend/intellisense/crates.ts @@ -0,0 +1,47 @@ + +export const downloadCodeOfCrate = async (crateName: string, version: string) => { + return (await fetch(`/assets/crate-src/${crateName}_${version}.rs`)).text(); +}; + +export const availableCrates = async (): Promise<{ [crate: string]: string[] }> => { + return (await fetch('/assets/crate-src/index.json')).json(); +}; + +const storageKey = 'intellisense-crates'; + +const selectedCrates = (): { [crate: string]: string | undefined } => { + const x = localStorage.getItem(storageKey); + if (!x) { + localStorage.setItem(storageKey, '{}'); + return {}; + } + return JSON.parse(x); +}; + +export const availableVersions = async (crateName: string): Promise<{ + selected: string | undefined, + others: string[], +}> => { + const v = (await availableCrates())[crateName]; + const selected = selectedCrates()[crateName]; + return { + selected, + others: v.filter((x) => x !== selected), + }; +}; + +export const selectVersion = (crateName: string, version: string) => { + const c = selectedCrates(); + c[crateName] = version; + localStorage.setItem(storageKey, JSON.stringify(c)); +}; + +export const selectedVersion = async (crateName: string): Promise => { + const current = selectedCrates()[crateName]; + if (selectedCrates()[crateName]) { + return current; + } + const r = (await availableCrates())[crateName][0]; + selectVersion(crateName, r); + return r; +}; diff --git a/ui/frontend/intellisense/index.ts b/ui/frontend/intellisense/index.ts new file mode 100644 index 000000000..d453c9804 --- /dev/null +++ b/ui/frontend/intellisense/index.ts @@ -0,0 +1,245 @@ +import { semanticTokensLegend } from '../editor/rust_monaco_def'; +import * as Monaco from 'monaco-editor'; +import { downloadCodeOfCrate, selectedVersion } from './crates'; +import { getIntellisenseConfig, isEnable } from './config'; + +const modeId = 'rust'; + +// Create an RA Web worker +const createRA = async () => { + const worker = new Worker(new URL('./ra-worker.js', import.meta.url)); + const pendingResolve = {}; + + let id = 1; + let ready; + + const callWorker = async (which, ...args) => { + return new Promise((resolve, _) => { + pendingResolve[id] = resolve; + worker.postMessage({ + which, + args, + id, + }); + id += 1; + }); + } + + const proxyHandler = { + get: (target, prop, _receiver) => { + if (prop == 'then') { + return Reflect.get(target, prop, _receiver); + } + return async (...args) => { + return callWorker(prop, ...args); + } + }, + } + + worker.onmessage = (e) => { + if (e.data.id == 'ra-worker-ready') { + ready(new Proxy({}, proxyHandler)); + return; + } + const pending = pendingResolve[e.data.id]; + if (pending) { + pending(e.data.result); + delete pendingResolve[e.data.id]; + } + } + + return new Promise((resolve, _) => { + ready = resolve; + }); +} + +const registerRA = (monaco: typeof Monaco, state: any) => { + monaco.languages.registerHoverProvider(modeId, { + provideHover: (_, pos) => state.hover(pos.lineNumber, pos.column), + }); + monaco.languages.registerCodeLensProvider(modeId, { + async provideCodeLenses(m) { + const code_lenses = await state.code_lenses(); + const lenses = code_lenses.map(({ range, command }) => { + const position = { + column: range.startColumn, + lineNumber: range.startLineNumber, + }; + + const references = command.positions.map((pos) => ({ range: pos, uri: m.uri })); + return { + range, + command: { + id: command.id, + title: command.title, + arguments: [ + m.uri, + position, + references, + ], + }, + }; + }); + + return { lenses, dispose() { /* do nothing */ } }; + }, + }); + monaco.languages.registerReferenceProvider(modeId, { + async provideReferences(m, pos, { includeDeclaration }) { + const references = await state.references(pos.lineNumber, pos.column, includeDeclaration); + if (references) { + return references.map(({ range }) => ({ uri: m.uri, range })); + } + }, + }); + monaco.languages.registerInlayHintsProvider(modeId, { + async provideInlayHints() { + const hints = await state.inlay_hints(); + console.log('finished inlay'); + return hints.map((hint: any) => { + if (hint.hint_type == 1) { + return { + kind: 1, + position: { column: hint.range.endColumn, lineNumber: hint.range.endLineNumber }, + text: `: ${hint.label}`, + }; + } + if (hint.hint_type == 2) { + return { + kind: 2, + position: { column: hint.range.startColumn, lineNumber: hint.range.startLineNumber }, + text: `${hint.label}:`, + whitespaceAfter: true, + }; + } + }) + }, + }); + monaco.languages.registerDocumentHighlightProvider(modeId, { + async provideDocumentHighlights(_, pos) { + return await state.references(pos.lineNumber, pos.column, true); + }, + }); + monaco.languages.registerRenameProvider(modeId, { + async provideRenameEdits(m, pos, newName) { + const edits = await state.rename(pos.lineNumber, pos.column, newName); + if (edits) { + return { + edits: edits.map(edit => ({ + resource: m.uri, + edit, + })), + }; + } + }, + async resolveRenameLocation(_, pos) { + return state.prepare_rename(pos.lineNumber, pos.column); + }, + }); + monaco.languages.registerCompletionItemProvider(modeId, { + triggerCharacters: ['.', ':', '='], + async provideCompletionItems(_m, pos) { + const suggestions = await state.completions(pos.lineNumber, pos.column); + if (suggestions) { + return { suggestions }; + } + }, + }); + monaco.languages.registerSignatureHelpProvider(modeId, { + signatureHelpTriggerCharacters: ['(', ','], + async provideSignatureHelp(_m, pos) { + const value = await state.signature_help(pos.lineNumber, pos.column); + if (!value) return null; + return { + value, + dispose() { /* do nothing */ }, + }; + }, + }); + monaco.languages.registerDefinitionProvider(modeId, { + async provideDefinition(m, pos) { + const list = await state.definition(pos.lineNumber, pos.column); + if (list) { + return list.map(def => ({ ...def, uri: m.uri })); + } + }, + }); + monaco.languages.registerTypeDefinitionProvider(modeId, { + async provideTypeDefinition(m, pos) { + const list = await state.type_definition(pos.lineNumber, pos.column); + if (list) { + return list.map(def => ({ ...def, uri: m.uri })); + } + }, + }); + monaco.languages.registerImplementationProvider(modeId, { + async provideImplementation(m, pos) { + const list = await state.goto_implementation(pos.lineNumber, pos.column); + if (list) { + return list.map(def => ({ ...def, uri: m.uri })); + } + }, + }); + monaco.languages.registerDocumentSymbolProvider(modeId, { + async provideDocumentSymbols() { + return await state.document_symbols(); + }, + }); + monaco.languages.registerOnTypeFormattingEditProvider(modeId, { + autoFormatTriggerCharacters: ['.', '='], + async provideOnTypeFormattingEdits(_, pos, ch) { + return await state.type_formatting(pos.lineNumber, pos.column, ch); + }, + }); + monaco.languages.registerFoldingRangeProvider(modeId, { + async provideFoldingRanges() { + return await state.folding_ranges(); + }, + }); + monaco.languages.registerDocumentSemanticTokensProvider(modeId, { + getLegend() { + return semanticTokensLegend; + }, + async provideDocumentSemanticTokens() { + const data = await state.semantic_tokens(); + console.log(data); + return { data }; + }, + releaseDocumentSemanticTokens() { /* do nothing */ }, + }); +}; + +export const enableOnMonaco = (editor: Monaco.editor.IStandaloneCodeEditor, monaco: typeof Monaco) => { + if (!isEnable()) { + return; + } + const model = editor.getModel(); + let state = null; + + async function update() { + const res = await state.update(model.getValue()); + if (getIntellisenseConfig().diagnostic) { + monaco.editor.setModelMarkers(model, modeId, res.diagnostics); + } + } + const initRA = async () => { + state = await createRA(); + await state.init(model.getValue()); + registerRA(monaco, state); + await update(); + model.onDidChangeContent(update); + await new Promise((res) => setTimeout(res, 500)); + const libstdVersion = await selectedVersion('stdlib'); + await state.update_crate_code('std', await downloadCodeOfCrate('std', libstdVersion)); + await state.update_crate_code('core', await downloadCodeOfCrate('core', libstdVersion)); + await state.update_crate_code('alloc', await downloadCodeOfCrate('alloc', libstdVersion)); + console.log('std lib will be loaded on next update'); + await state.semantic_tokens(); + // HACK: reload semantic data + const pos = editor.getPosition(); + model.setValue(model.getValue()); + editor.setPosition(pos); + }; + initRA(); +}; + diff --git a/ui/frontend/intellisense/ra-worker.js b/ui/frontend/intellisense/ra-worker.js new file mode 100644 index 000000000..9e35cc048 --- /dev/null +++ b/ui/frontend/intellisense/ra-worker.js @@ -0,0 +1,30 @@ +import init, { initThreadPool, WorldState } from './pkg/wasm_demo.js'; + +const start = async () => { + await init(); + + // Thread pool initialization with the given number of threads + // (pass `navigator.hardwareConcurrency` if you want to use all cores). + await initThreadPool(navigator.hardwareConcurrency) + + const state = new WorldState(); + + onmessage = (e) => { + const { which, args, id } = e.data; + const result = state[which](...args); + + postMessage({ + id: id, + result: result, + }); + }; +}; + +start().then(() => { + postMessage({ + id: 'ra-worker-ready', + }) +}) + + +