From ec5fa85ad0a1ded34a7a46fe38654531008ee391 Mon Sep 17 00:00:00 2001 From: Sock <87631534+platonicsock@users.noreply.github.com> Date: Mon, 3 Aug 2026 13:26:50 -0400 Subject: [PATCH] [anneal] Add playground frontend gherrit-pr-id: Gbja7ynjo5n4gf6rek56jhkaclymvo22y --- .../ui/frontend/.babelrc | 17 + .../ui/frontend/.browserslistrc | 9 + .../ui/frontend/.gitignore | 3 + .../ui/frontend/.prettierignore | 35 + .../ui/frontend/.prettierrc.json | 11 + .../ui/frontend/.stylelintignore | 1 + .../ui/frontend/.stylelintrc.json | 20 + .../ui/frontend/AdvancedOptionsMenu.tsx | 84 + .../ui/frontend/BuildMenu.module.css | 4 + .../ui/frontend/BuildMenu.tsx | 93 + .../ui/frontend/ButtonMenuItem.module.css | 8 + .../ui/frontend/ButtonMenuItem.tsx | 23 + .../ui/frontend/ButtonSet.module.css | 124 + .../ui/frontend/ButtonSet.tsx | 81 + .../ui/frontend/ChannelMenu.module.css | 3 + .../ui/frontend/ChannelMenu.tsx | 69 + .../ui/frontend/ConfigElement.module.css | 73 + .../ui/frontend/ConfigElement.tsx | 90 + .../ui/frontend/ConfigMenu.tsx | 153 + .../ui/frontend/Header.module.css | 12 + .../ui/frontend/Header.tsx | 197 + .../ui/frontend/Help.module.css | 64 + .../ui/frontend/Help.tsx | 365 + .../ui/frontend/HelpExample.module.css | 21 + .../ui/frontend/HelpExample.tsx | 35 + .../ui/frontend/Icon.module.css | 4 + .../ui/frontend/Icon.tsx | 70 + .../ui/frontend/Loader.module.css | 23 + .../ui/frontend/Loader.tsx | 13 + .../ui/frontend/MenuAside.module.css | 4 + .../ui/frontend/MenuAside.tsx | 11 + .../ui/frontend/MenuGroup.module.css | 22 + .../ui/frontend/MenuGroup.tsx | 19 + .../ui/frontend/MenuItem.module.css | 7 + .../ui/frontend/MenuItem.tsx | 9 + .../ui/frontend/ModeMenu.tsx | 47 + .../ui/frontend/Notifications.module.css | 41 + .../ui/frontend/Notifications.tsx | 129 + .../ui/frontend/Output.module.css | 53 + .../ui/frontend/Output.tsx | 148 + .../ui/frontend/Output/Assembly.tsx | 24 + .../ui/frontend/Output/Execute.module.css | 3 + .../ui/frontend/Output/Execute.tsx | 43 + .../ui/frontend/Output/Gist.module.css | 40 + .../ui/frontend/Output/Gist.tsx | 101 + .../ui/frontend/Output/Header.module.css | 21 + .../ui/frontend/Output/Header.tsx | 13 + .../ui/frontend/Output/LlvmIr.tsx | 24 + .../ui/frontend/Output/Loader.tsx | 13 + .../ui/frontend/Output/OutputPrism.module.css | 3 + .../ui/frontend/Output/OutputPrism.tsx | 18 + .../ui/frontend/Output/PaneWithCode.tsx | 19 + .../ui/frontend/Output/PaneWithMir.tsx | 20 + .../ui/frontend/Output/Section.module.css | 3 + .../ui/frontend/Output/Section.tsx | 22 + .../ui/frontend/Output/SimplePane.tsx | 57 + .../ui/frontend/Output/WarnAboutNoSymbols.tsx | 35 + .../ui/frontend/PageSwitcher.tsx | 18 + .../ui/frontend/Playground.module.css | 101 + .../ui/frontend/Playground.tsx | 259 + .../ui/frontend/PopButton.module.css | 35 + .../ui/frontend/PopButton.tsx | 103 + .../ui/frontend/Prism.tsx | 31 + .../ui/frontend/Router.tsx | 109 + .../ui/frontend/SelectOne.tsx | 26 + .../ui/frontend/SelectableMenuItem.module.css | 42 + .../ui/frontend/SelectableMenuItem.tsx | 29 + .../frontend/SimpleButtonMenuItem.module.css | 7 + .../ui/frontend/SimpleButtonMenuItem.tsx | 25 + .../ui/frontend/Stdin.module.css | 46 + .../ui/frontend/Stdin.tsx | 138 + .../ui/frontend/ToolsMenu.tsx | 85 + .../ui/frontend/actions.ts | 188 + .../rust-anneal-playground/ui/frontend/api.ts | 55 + .../frontend/assets/integer32-logo-dark.svg | 2 + .../frontend/assets/integer32-logo-light.svg | 2 + .../ui/frontend/compileActions.ts | 52 + .../ui/frontend/configureStore.ts | 84 + .../ui/frontend/declarations.d.ts | 34 + .../ui/frontend/editor/AceEditor.tsx | 62 + .../ui/frontend/editor/AceEditorCore.tsx | 301 + .../ui/frontend/editor/Editor.module.css | 24 + .../ui/frontend/editor/Editor.tsx | 47 + .../ui/frontend/editor/MonacoEditor.tsx | 13 + .../ui/frontend/editor/MonacoEditorCore.tsx | 210 + .../ui/frontend/editor/SimpleEditor.tsx | 116 + .../ui/frontend/editor/rust_monaco_def.ts | 10 + .../ui/frontend/editor/useLatest.ts | 19 + .../ui/frontend/eslint.config.mjs | 62 + .../ui/frontend/highlighting.ts | 213 + .../ui/frontend/hooks.ts | 8 + .../ui/frontend/index.ejs | 16 + .../ui/frontend/index.module.css | 231 + .../ui/frontend/index.tsx | 115 + .../ui/frontend/local_storage.spec.ts | 50 + .../ui/frontend/local_storage.ts | 176 + .../ui/frontend/observer.ts | 54 + .../ui/frontend/package.json | 115 + .../ui/frontend/pnpm-lock.yaml | 8990 +++++++++++++++++ .../ui/frontend/pnpm-workspace.yaml | 4 + .../ui/frontend/postcss.config.js | 8 + .../ui/frontend/prism-shim.ts | 6 + .../ui/frontend/prismjs-overrides.css | 5 + .../ui/frontend/reducers/browser.ts | 23 + .../ui/frontend/reducers/client.ts | 60 + .../ui/frontend/reducers/code.ts | 99 + .../ui/frontend/reducers/configuration.ts | 172 + .../ui/frontend/reducers/crates.ts | 36 + .../ui/frontend/reducers/featureFlags.ts | 61 + .../frontend/reducers/globalConfiguration.ts | 47 + .../ui/frontend/reducers/index.ts | 37 + .../ui/frontend/reducers/notifications.ts | 54 + .../ui/frontend/reducers/output/assembly.ts | 43 + .../ui/frontend/reducers/output/clippy.ts | 65 + .../ui/frontend/reducers/output/execute.ts | 287 + .../ui/frontend/reducers/output/format.ts | 64 + .../ui/frontend/reducers/output/gist.ts | 123 + .../ui/frontend/reducers/output/hir.ts | 43 + .../ui/frontend/reducers/output/index.ts | 31 + .../ui/frontend/reducers/output/llvmIr.ts | 43 + .../reducers/output/macroExpansion.ts | 64 + .../ui/frontend/reducers/output/meta.ts | 92 + .../ui/frontend/reducers/output/mir.ts | 43 + .../ui/frontend/reducers/output/miri.ts | 66 + .../ui/frontend/reducers/output/wasm.ts | 43 + .../ui/frontend/reducers/page.ts | 22 + .../ui/frontend/reducers/position.ts | 22 + .../ui/frontend/reducers/selection.ts | 21 + .../ui/frontend/reducers/versions.ts | 37 + .../ui/frontend/reducers/websocket.ts | 77 + .../ui/frontend/robots.txt | 2 + .../ui/frontend/selectors/gist.ts | 120 + .../ui/frontend/selectors/index.spec.ts | 136 + .../ui/frontend/selectors/index.ts | 498 + .../ui/frontend/session_storage.ts | 57 + .../ui/frontend/shared.module.css | 47 + .../ui/frontend/storage.ts | 77 + .../ui/frontend/tsconfig.json | 33 + .../ui/frontend/tsfmt.json | 4 + .../ui/frontend/types.ts | 198 + .../ui/frontend/uss-router/Link.tsx | 41 + .../ui/frontend/uss-router/Router.tsx | 19 + .../ui/frontend/uss-router/index.ts | 94 + .../ui/frontend/webpack.config.js | 178 + .../ui/frontend/websocketActions.ts | 55 + .../ui/frontend/websocketMiddleware.ts | 191 + 146 files changed, 18505 insertions(+) create mode 100644 anneal/v1/playground/rust-anneal-playground/ui/frontend/.babelrc create mode 100644 anneal/v1/playground/rust-anneal-playground/ui/frontend/.browserslistrc create mode 100644 anneal/v1/playground/rust-anneal-playground/ui/frontend/.gitignore create mode 100644 anneal/v1/playground/rust-anneal-playground/ui/frontend/.prettierignore create mode 100644 anneal/v1/playground/rust-anneal-playground/ui/frontend/.prettierrc.json create mode 100644 anneal/v1/playground/rust-anneal-playground/ui/frontend/.stylelintignore create mode 100644 anneal/v1/playground/rust-anneal-playground/ui/frontend/.stylelintrc.json create mode 100644 anneal/v1/playground/rust-anneal-playground/ui/frontend/AdvancedOptionsMenu.tsx create mode 100644 anneal/v1/playground/rust-anneal-playground/ui/frontend/BuildMenu.module.css create mode 100644 anneal/v1/playground/rust-anneal-playground/ui/frontend/BuildMenu.tsx create mode 100644 anneal/v1/playground/rust-anneal-playground/ui/frontend/ButtonMenuItem.module.css create mode 100644 anneal/v1/playground/rust-anneal-playground/ui/frontend/ButtonMenuItem.tsx create mode 100644 anneal/v1/playground/rust-anneal-playground/ui/frontend/ButtonSet.module.css create mode 100644 anneal/v1/playground/rust-anneal-playground/ui/frontend/ButtonSet.tsx create mode 100644 anneal/v1/playground/rust-anneal-playground/ui/frontend/ChannelMenu.module.css create mode 100644 anneal/v1/playground/rust-anneal-playground/ui/frontend/ChannelMenu.tsx create mode 100644 anneal/v1/playground/rust-anneal-playground/ui/frontend/ConfigElement.module.css create mode 100644 anneal/v1/playground/rust-anneal-playground/ui/frontend/ConfigElement.tsx create mode 100644 anneal/v1/playground/rust-anneal-playground/ui/frontend/ConfigMenu.tsx create mode 100644 anneal/v1/playground/rust-anneal-playground/ui/frontend/Header.module.css create mode 100644 anneal/v1/playground/rust-anneal-playground/ui/frontend/Header.tsx create mode 100644 anneal/v1/playground/rust-anneal-playground/ui/frontend/Help.module.css create mode 100644 anneal/v1/playground/rust-anneal-playground/ui/frontend/Help.tsx create mode 100644 anneal/v1/playground/rust-anneal-playground/ui/frontend/HelpExample.module.css create mode 100644 anneal/v1/playground/rust-anneal-playground/ui/frontend/HelpExample.tsx create mode 100644 anneal/v1/playground/rust-anneal-playground/ui/frontend/Icon.module.css create mode 100644 anneal/v1/playground/rust-anneal-playground/ui/frontend/Icon.tsx create mode 100644 anneal/v1/playground/rust-anneal-playground/ui/frontend/Loader.module.css create mode 100644 anneal/v1/playground/rust-anneal-playground/ui/frontend/Loader.tsx create mode 100644 anneal/v1/playground/rust-anneal-playground/ui/frontend/MenuAside.module.css create mode 100644 anneal/v1/playground/rust-anneal-playground/ui/frontend/MenuAside.tsx create mode 100644 anneal/v1/playground/rust-anneal-playground/ui/frontend/MenuGroup.module.css create mode 100644 anneal/v1/playground/rust-anneal-playground/ui/frontend/MenuGroup.tsx create mode 100644 anneal/v1/playground/rust-anneal-playground/ui/frontend/MenuItem.module.css create mode 100644 anneal/v1/playground/rust-anneal-playground/ui/frontend/MenuItem.tsx create mode 100644 anneal/v1/playground/rust-anneal-playground/ui/frontend/ModeMenu.tsx create mode 100644 anneal/v1/playground/rust-anneal-playground/ui/frontend/Notifications.module.css create mode 100644 anneal/v1/playground/rust-anneal-playground/ui/frontend/Notifications.tsx create mode 100644 anneal/v1/playground/rust-anneal-playground/ui/frontend/Output.module.css create mode 100644 anneal/v1/playground/rust-anneal-playground/ui/frontend/Output.tsx create mode 100644 anneal/v1/playground/rust-anneal-playground/ui/frontend/Output/Assembly.tsx create mode 100644 anneal/v1/playground/rust-anneal-playground/ui/frontend/Output/Execute.module.css create mode 100644 anneal/v1/playground/rust-anneal-playground/ui/frontend/Output/Execute.tsx create mode 100644 anneal/v1/playground/rust-anneal-playground/ui/frontend/Output/Gist.module.css create mode 100644 anneal/v1/playground/rust-anneal-playground/ui/frontend/Output/Gist.tsx create mode 100644 anneal/v1/playground/rust-anneal-playground/ui/frontend/Output/Header.module.css create mode 100644 anneal/v1/playground/rust-anneal-playground/ui/frontend/Output/Header.tsx create mode 100644 anneal/v1/playground/rust-anneal-playground/ui/frontend/Output/LlvmIr.tsx create mode 100644 anneal/v1/playground/rust-anneal-playground/ui/frontend/Output/Loader.tsx create mode 100644 anneal/v1/playground/rust-anneal-playground/ui/frontend/Output/OutputPrism.module.css create mode 100644 anneal/v1/playground/rust-anneal-playground/ui/frontend/Output/OutputPrism.tsx create mode 100644 anneal/v1/playground/rust-anneal-playground/ui/frontend/Output/PaneWithCode.tsx create mode 100644 anneal/v1/playground/rust-anneal-playground/ui/frontend/Output/PaneWithMir.tsx create mode 100644 anneal/v1/playground/rust-anneal-playground/ui/frontend/Output/Section.module.css create mode 100644 anneal/v1/playground/rust-anneal-playground/ui/frontend/Output/Section.tsx create mode 100644 anneal/v1/playground/rust-anneal-playground/ui/frontend/Output/SimplePane.tsx create mode 100644 anneal/v1/playground/rust-anneal-playground/ui/frontend/Output/WarnAboutNoSymbols.tsx create mode 100644 anneal/v1/playground/rust-anneal-playground/ui/frontend/PageSwitcher.tsx create mode 100644 anneal/v1/playground/rust-anneal-playground/ui/frontend/Playground.module.css create mode 100644 anneal/v1/playground/rust-anneal-playground/ui/frontend/Playground.tsx create mode 100644 anneal/v1/playground/rust-anneal-playground/ui/frontend/PopButton.module.css create mode 100644 anneal/v1/playground/rust-anneal-playground/ui/frontend/PopButton.tsx create mode 100644 anneal/v1/playground/rust-anneal-playground/ui/frontend/Prism.tsx create mode 100644 anneal/v1/playground/rust-anneal-playground/ui/frontend/Router.tsx create mode 100644 anneal/v1/playground/rust-anneal-playground/ui/frontend/SelectOne.tsx create mode 100644 anneal/v1/playground/rust-anneal-playground/ui/frontend/SelectableMenuItem.module.css create mode 100644 anneal/v1/playground/rust-anneal-playground/ui/frontend/SelectableMenuItem.tsx create mode 100644 anneal/v1/playground/rust-anneal-playground/ui/frontend/SimpleButtonMenuItem.module.css create mode 100644 anneal/v1/playground/rust-anneal-playground/ui/frontend/SimpleButtonMenuItem.tsx create mode 100644 anneal/v1/playground/rust-anneal-playground/ui/frontend/Stdin.module.css create mode 100644 anneal/v1/playground/rust-anneal-playground/ui/frontend/Stdin.tsx create mode 100644 anneal/v1/playground/rust-anneal-playground/ui/frontend/ToolsMenu.tsx create mode 100644 anneal/v1/playground/rust-anneal-playground/ui/frontend/actions.ts create mode 100644 anneal/v1/playground/rust-anneal-playground/ui/frontend/api.ts create mode 100644 anneal/v1/playground/rust-anneal-playground/ui/frontend/assets/integer32-logo-dark.svg create mode 100644 anneal/v1/playground/rust-anneal-playground/ui/frontend/assets/integer32-logo-light.svg create mode 100644 anneal/v1/playground/rust-anneal-playground/ui/frontend/compileActions.ts create mode 100644 anneal/v1/playground/rust-anneal-playground/ui/frontend/configureStore.ts create mode 100644 anneal/v1/playground/rust-anneal-playground/ui/frontend/declarations.d.ts create mode 100644 anneal/v1/playground/rust-anneal-playground/ui/frontend/editor/AceEditor.tsx create mode 100644 anneal/v1/playground/rust-anneal-playground/ui/frontend/editor/AceEditorCore.tsx create mode 100644 anneal/v1/playground/rust-anneal-playground/ui/frontend/editor/Editor.module.css create mode 100644 anneal/v1/playground/rust-anneal-playground/ui/frontend/editor/Editor.tsx create mode 100644 anneal/v1/playground/rust-anneal-playground/ui/frontend/editor/MonacoEditor.tsx create mode 100644 anneal/v1/playground/rust-anneal-playground/ui/frontend/editor/MonacoEditorCore.tsx create mode 100644 anneal/v1/playground/rust-anneal-playground/ui/frontend/editor/SimpleEditor.tsx create mode 100644 anneal/v1/playground/rust-anneal-playground/ui/frontend/editor/rust_monaco_def.ts create mode 100644 anneal/v1/playground/rust-anneal-playground/ui/frontend/editor/useLatest.ts create mode 100644 anneal/v1/playground/rust-anneal-playground/ui/frontend/eslint.config.mjs create mode 100644 anneal/v1/playground/rust-anneal-playground/ui/frontend/highlighting.ts create mode 100644 anneal/v1/playground/rust-anneal-playground/ui/frontend/hooks.ts create mode 100644 anneal/v1/playground/rust-anneal-playground/ui/frontend/index.ejs create mode 100644 anneal/v1/playground/rust-anneal-playground/ui/frontend/index.module.css create mode 100644 anneal/v1/playground/rust-anneal-playground/ui/frontend/index.tsx create mode 100644 anneal/v1/playground/rust-anneal-playground/ui/frontend/local_storage.spec.ts create mode 100644 anneal/v1/playground/rust-anneal-playground/ui/frontend/local_storage.ts create mode 100644 anneal/v1/playground/rust-anneal-playground/ui/frontend/observer.ts create mode 100644 anneal/v1/playground/rust-anneal-playground/ui/frontend/package.json create mode 100644 anneal/v1/playground/rust-anneal-playground/ui/frontend/pnpm-lock.yaml create mode 100644 anneal/v1/playground/rust-anneal-playground/ui/frontend/pnpm-workspace.yaml create mode 100644 anneal/v1/playground/rust-anneal-playground/ui/frontend/postcss.config.js create mode 100644 anneal/v1/playground/rust-anneal-playground/ui/frontend/prism-shim.ts create mode 100644 anneal/v1/playground/rust-anneal-playground/ui/frontend/prismjs-overrides.css create mode 100644 anneal/v1/playground/rust-anneal-playground/ui/frontend/reducers/browser.ts create mode 100644 anneal/v1/playground/rust-anneal-playground/ui/frontend/reducers/client.ts create mode 100644 anneal/v1/playground/rust-anneal-playground/ui/frontend/reducers/code.ts create mode 100644 anneal/v1/playground/rust-anneal-playground/ui/frontend/reducers/configuration.ts create mode 100644 anneal/v1/playground/rust-anneal-playground/ui/frontend/reducers/crates.ts create mode 100644 anneal/v1/playground/rust-anneal-playground/ui/frontend/reducers/featureFlags.ts create mode 100644 anneal/v1/playground/rust-anneal-playground/ui/frontend/reducers/globalConfiguration.ts create mode 100644 anneal/v1/playground/rust-anneal-playground/ui/frontend/reducers/index.ts create mode 100644 anneal/v1/playground/rust-anneal-playground/ui/frontend/reducers/notifications.ts create mode 100644 anneal/v1/playground/rust-anneal-playground/ui/frontend/reducers/output/assembly.ts create mode 100644 anneal/v1/playground/rust-anneal-playground/ui/frontend/reducers/output/clippy.ts create mode 100644 anneal/v1/playground/rust-anneal-playground/ui/frontend/reducers/output/execute.ts create mode 100644 anneal/v1/playground/rust-anneal-playground/ui/frontend/reducers/output/format.ts create mode 100644 anneal/v1/playground/rust-anneal-playground/ui/frontend/reducers/output/gist.ts create mode 100644 anneal/v1/playground/rust-anneal-playground/ui/frontend/reducers/output/hir.ts create mode 100644 anneal/v1/playground/rust-anneal-playground/ui/frontend/reducers/output/index.ts create mode 100644 anneal/v1/playground/rust-anneal-playground/ui/frontend/reducers/output/llvmIr.ts create mode 100644 anneal/v1/playground/rust-anneal-playground/ui/frontend/reducers/output/macroExpansion.ts create mode 100644 anneal/v1/playground/rust-anneal-playground/ui/frontend/reducers/output/meta.ts create mode 100644 anneal/v1/playground/rust-anneal-playground/ui/frontend/reducers/output/mir.ts create mode 100644 anneal/v1/playground/rust-anneal-playground/ui/frontend/reducers/output/miri.ts create mode 100644 anneal/v1/playground/rust-anneal-playground/ui/frontend/reducers/output/wasm.ts create mode 100644 anneal/v1/playground/rust-anneal-playground/ui/frontend/reducers/page.ts create mode 100644 anneal/v1/playground/rust-anneal-playground/ui/frontend/reducers/position.ts create mode 100644 anneal/v1/playground/rust-anneal-playground/ui/frontend/reducers/selection.ts create mode 100644 anneal/v1/playground/rust-anneal-playground/ui/frontend/reducers/versions.ts create mode 100644 anneal/v1/playground/rust-anneal-playground/ui/frontend/reducers/websocket.ts create mode 100644 anneal/v1/playground/rust-anneal-playground/ui/frontend/robots.txt create mode 100644 anneal/v1/playground/rust-anneal-playground/ui/frontend/selectors/gist.ts create mode 100644 anneal/v1/playground/rust-anneal-playground/ui/frontend/selectors/index.spec.ts create mode 100644 anneal/v1/playground/rust-anneal-playground/ui/frontend/selectors/index.ts create mode 100644 anneal/v1/playground/rust-anneal-playground/ui/frontend/session_storage.ts create mode 100644 anneal/v1/playground/rust-anneal-playground/ui/frontend/shared.module.css create mode 100644 anneal/v1/playground/rust-anneal-playground/ui/frontend/storage.ts create mode 100644 anneal/v1/playground/rust-anneal-playground/ui/frontend/tsconfig.json create mode 100644 anneal/v1/playground/rust-anneal-playground/ui/frontend/tsfmt.json create mode 100644 anneal/v1/playground/rust-anneal-playground/ui/frontend/types.ts create mode 100644 anneal/v1/playground/rust-anneal-playground/ui/frontend/uss-router/Link.tsx create mode 100644 anneal/v1/playground/rust-anneal-playground/ui/frontend/uss-router/Router.tsx create mode 100644 anneal/v1/playground/rust-anneal-playground/ui/frontend/uss-router/index.ts create mode 100644 anneal/v1/playground/rust-anneal-playground/ui/frontend/webpack.config.js create mode 100644 anneal/v1/playground/rust-anneal-playground/ui/frontend/websocketActions.ts create mode 100644 anneal/v1/playground/rust-anneal-playground/ui/frontend/websocketMiddleware.ts diff --git a/anneal/v1/playground/rust-anneal-playground/ui/frontend/.babelrc b/anneal/v1/playground/rust-anneal-playground/ui/frontend/.babelrc new file mode 100644 index 0000000000..95fa8793af --- /dev/null +++ b/anneal/v1/playground/rust-anneal-playground/ui/frontend/.babelrc @@ -0,0 +1,17 @@ +{ + "presets": [ + [ "@babel/preset-env", { + "corejs": "3", + "modules": false, + "useBuiltIns": "entry", + }], + [ "@babel/react", { + "runtime": "automatic", + }], + ], + "plugins": [ + ["babel-plugin-react-compiler", { + "compilationMode": "annotation", + }], + ], +} diff --git a/anneal/v1/playground/rust-anneal-playground/ui/frontend/.browserslistrc b/anneal/v1/playground/rust-anneal-playground/ui/frontend/.browserslistrc new file mode 100644 index 0000000000..638851b724 --- /dev/null +++ b/anneal/v1/playground/rust-anneal-playground/ui/frontend/.browserslistrc @@ -0,0 +1,9 @@ +# https://github.com/rust-lang/rfcs/blob/master/text/1985-tiered-browser-support.md + +last 2 Chrome versions +last 1 Firefox version +Firefox ESR +last 1 Safari version +last 1 iOS version +last 1 Edge version +last 1 UCAndroid version diff --git a/anneal/v1/playground/rust-anneal-playground/ui/frontend/.gitignore b/anneal/v1/playground/rust-anneal-playground/ui/frontend/.gitignore new file mode 100644 index 0000000000..4aa32c94ed --- /dev/null +++ b/anneal/v1/playground/rust-anneal-playground/ui/frontend/.gitignore @@ -0,0 +1,3 @@ +.eslintcache +build +node_modules diff --git a/anneal/v1/playground/rust-anneal-playground/ui/frontend/.prettierignore b/anneal/v1/playground/rust-anneal-playground/ui/frontend/.prettierignore new file mode 100644 index 0000000000..d6ea7cf042 --- /dev/null +++ b/anneal/v1/playground/rust-anneal-playground/ui/frontend/.prettierignore @@ -0,0 +1,35 @@ +pnpm-lock.yaml + +build +node_modules + +# Not for these ready yet +.babelrc +*.js +*.json +*.scss + +# Remove these files when we touch them for other reasons +ChannelMenu.tsx +ConfigMenu.tsx +Help.tsx +Icon.tsx +MenuAside.tsx +MenuGroup.tsx +ModeMenu.tsx +Output.tsx +Output/Execute.tsx +Output/Section.tsx +PageSwitcher.tsx +Router.tsx +SelectOne.tsx +SelectableMenuItem.tsx +ToolsMenu.tsx +editor/AceEditorCore.tsx +editor/MonacoEditor.tsx +highlighting.ts +index.tsx +selectors/index.ts +uss-router/Link.tsx +uss-router/Router.tsx +uss-router/index.ts diff --git a/anneal/v1/playground/rust-anneal-playground/ui/frontend/.prettierrc.json b/anneal/v1/playground/rust-anneal-playground/ui/frontend/.prettierrc.json new file mode 100644 index 0000000000..8a01e8e9ac --- /dev/null +++ b/anneal/v1/playground/rust-anneal-playground/ui/frontend/.prettierrc.json @@ -0,0 +1,11 @@ +{ + "printWidth": 100, + "singleQuote": true, + "trailingComma": "all", + "importOrder": ["^[./].*(? ( + + Code that is accepted by Tree Borrows may be declared + undefined behavior in the future. + +); + +const AdvancedOptionsMenu: React.FC = () => { + const isEditionDefault = useAppSelector(selectors.isEditionDefault); + const edition = useAppSelector((state) => state.configuration.edition); + const isBacktraceDefault = useAppSelector(selectors.isBacktraceDefault); + const backtrace = useAppSelector((state) => state.configuration.backtrace); + const isAliasingModelDefault = useAppSelector(selectors.isAliasingModelDefault); + const aliasingModel = useAppSelector((state) => state.configuration.aliasingModel); + + const dispatch = useAppDispatch(); + + const changeEdition = useCallback((e: Edition) => dispatch(config.changeEdition(e)), [dispatch]); + const changeBacktrace = useCallback( + (b: Backtrace) => dispatch(config.changeBacktrace(b)), + [dispatch], + ); + const changeAliasingModel = useCallback( + (b: AliasingModel) => dispatch(config.changeAliasingModel(b)), + [dispatch], + ); + + return ( + <> + + + + + + + + + + + + + } + /> + + + ); +}; + +export default AdvancedOptionsMenu; diff --git a/anneal/v1/playground/rust-anneal-playground/ui/frontend/BuildMenu.module.css b/anneal/v1/playground/rust-anneal-playground/ui/frontend/BuildMenu.module.css new file mode 100644 index 0000000000..0dac95d5f3 --- /dev/null +++ b/anneal/v1/playground/rust-anneal-playground/ui/frontend/BuildMenu.module.css @@ -0,0 +1,4 @@ +.code { + background: var(--code-background-color); + padding: 0 0.25em; +} diff --git a/anneal/v1/playground/rust-anneal-playground/ui/frontend/BuildMenu.tsx b/anneal/v1/playground/rust-anneal-playground/ui/frontend/BuildMenu.tsx new file mode 100644 index 0000000000..9e72e338c5 --- /dev/null +++ b/anneal/v1/playground/rust-anneal-playground/ui/frontend/BuildMenu.tsx @@ -0,0 +1,93 @@ +import React, { useCallback } from 'react'; + +import ButtonMenuItem from './ButtonMenuItem'; +import MenuAside from './MenuAside'; +import MenuGroup from './MenuGroup'; +import * as actions from './actions'; +import { useAppDispatch, useAppSelector } from './hooks'; +import * as selectors from './selectors'; + +import * as styles from './BuildMenu.module.css'; + +interface BuildMenuProps { + close: () => void; +} + +const useAppDispatchAndClose = (action: () => actions.ThunkAction, close: () => void) => { + const dispatch = useAppDispatch(); + + return useCallback(() => { + dispatch(action()); + close(); + }, [action, close, dispatch]); +}; + +const BuildMenu: React.FC = (props) => { + const isHirAvailable = useAppSelector(selectors.isHirAvailable); + const wasmLikelyToWork = useAppSelector(selectors.wasmLikelyToWork); + + const compile = useAppDispatchAndClose(actions.performCompile, props.close); + const compileToAssembly = useAppDispatchAndClose(actions.performCompileToAssembly, props.close); + const compileToLLVM = useAppDispatchAndClose(actions.performCompileToLLVM, props.close); + const compileToMir = useAppDispatchAndClose(actions.performCompileToMir, props.close); + const compileToHir = useAppDispatchAndClose(actions.performCompileToNightlyHir, props.close); + const compileToWasm = useAppDispatchAndClose(actions.performCompileToWasm, props.close); + const execute = useAppDispatchAndClose(actions.performExecute, props.close); + const test = useAppDispatchAndClose(actions.performTest, props.close); + const performCargoAnneal = useAppDispatchAndClose(actions.performCargoAnneal, props.close); + + return ( + + + Runs cargo anneal verify on the code. + + + Build and run the code, showing the output. Equivalent to cargo run. + + + Build the code without running it. Equivalent to cargo build. + + + Build the code and run all the tests. Equivalent to cargo test. + + + Build and show the resulting assembly code. + + + Build and show the resulting LLVM IR, LLVM’s intermediate representation. + + + Build and show the resulting MIR, Rust’s control-flow-based intermediate representation. + + + Build and show the resulting HIR, Rust’s syntax-based intermediate representation. + {!isHirAvailable && } + + + Build a WebAssembly module for web browsers, in the .WAT textual representation. + {!wasmLikelyToWork && } + + + ); +}; + +const Code: React.FC<{ children: string }> = ({ children }) => ( + {children} +); + +const HirAside: React.FC = () => ( + + Note: HIR currently requires using the Nightly channel, selecting this option will switch to + Nightly. + +); + +const WasmAside: React.FC = () => ( + + Note: WebAssembly works best when using the cdylib crate type, but the source code + does not specify an explicit crate type. Selecting this option will change the code to specify{' '} + cdylib. + +); + +export default BuildMenu; diff --git a/anneal/v1/playground/rust-anneal-playground/ui/frontend/ButtonMenuItem.module.css b/anneal/v1/playground/rust-anneal-playground/ui/frontend/ButtonMenuItem.module.css new file mode 100644 index 0000000000..fcdb323cf1 --- /dev/null +++ b/anneal/v1/playground/rust-anneal-playground/ui/frontend/ButtonMenuItem.module.css @@ -0,0 +1,8 @@ +.name { + composes: -menuItemTitle from './shared.module.css'; + margin: 0; +} + +.description { + margin: 0; +} diff --git a/anneal/v1/playground/rust-anneal-playground/ui/frontend/ButtonMenuItem.tsx b/anneal/v1/playground/rust-anneal-playground/ui/frontend/ButtonMenuItem.tsx new file mode 100644 index 0000000000..bb28e1d51a --- /dev/null +++ b/anneal/v1/playground/rust-anneal-playground/ui/frontend/ButtonMenuItem.tsx @@ -0,0 +1,23 @@ +import React, { type JSX } from 'react'; + +import SimpleButtonMenuItem from './SimpleButtonMenuItem'; + +import * as styles from './ButtonMenuItem.module.css'; + +type Button = JSX.IntrinsicElements['button']; + +interface ButtonMenuItemProps extends Button { + children: React.ReactNode; + name: string; +} + +const ButtonMenuItem: React.FC = ({ name, children, ...props }) => ( + +
+ {name} +
+
{children}
+
+); + +export default ButtonMenuItem; diff --git a/anneal/v1/playground/rust-anneal-playground/ui/frontend/ButtonSet.module.css b/anneal/v1/playground/rust-anneal-playground/ui/frontend/ButtonSet.module.css new file mode 100644 index 0000000000..a65763ab62 --- /dev/null +++ b/anneal/v1/playground/rust-anneal-playground/ui/frontend/ButtonSet.module.css @@ -0,0 +1,124 @@ +$width: 1px; +$radius: 4px; + +.set { + display: flex; +} + +.-border { + border-width: $width 0; + border-style: solid; + + &:first-child { + border-top-left-radius: $radius; + border-bottom-left-radius: $radius; + border-left-width: $width; + } + + &:last-child { + border-top-right-radius: $radius; + border-bottom-right-radius: $radius; + border-right-width: $width; + } +} + +.-button { + --button-gap: 0.5em; + --button-side-padding: 1.25em; + --button-height: 3em; + --button-side-icon-nudge: 0.25em; + + composes: -buttonReset from './shared.module.css'; + display: grid; + grid-auto-flow: column; + gap: var(--button-gap); + align-content: center; + align-items: center; + padding: 0 var(--button-side-padding); + height: var(--button-height); + font-weight: 600; + text-decoration: none; + text-transform: uppercase; + white-space: nowrap; + + &:enabled { + cursor: pointer; + } +} + +.small { + --button-gap: 0.25em; + --button-side-padding: 0.75em; + --button-height: 2.5em; + --button-side-icon-nudge: 0.125em; +} + +.primary { + composes: -border -button; + background-color: var(--button-primary-bg-color); + border-color: var(--button-primary-border-color); + color: var(--button-primary-color); + font-weight: 700; + + &:disabled { + background-color: var(--button-primary-bg-color-light); + border-color: var(--button-primary-border-color-light); + } + + &:hover:enabled { + background-color: var(--button-primary-border-color); + } + + &:active:enabled { + box-shadow: inset 0 0 5px var(--button-primary-active-color); + } +} + +.secondary { + composes: -border -button; + background: var(--button-secondary-bg-color); + border-color: var(--button-secondary-border-color); + color: var(--button-secondary-color); + + &:disabled { + background: inherit; + background-color: var(--button-secondary-bg-color-light); + border-color: var(--button-secondary-border-color-light); + color: var(--button-secondary-color-light); + } + + &:visited { + color: var(--button-secondary-color); + } + + &:hover:enabled { + background: inherit; + background-color: var(--button-secondary-border-color); + } + + &:active:enabled { + box-shadow: inset 0 0 5px var(--button-secondary-active-color); + } +} + +.iconLeft { + transform: translate(calc(-1 * var(--button-side-icon-nudge)), 0); +} + +.iconRight { + transform: translate(var(--button-side-icon-nudge), 0); +} + +.rule { + composes: -border; + background-color: var(--button-secondary-border-color); + width: 1px; + border-color: var(--button-secondary-border-color); +} + +.icon { + composes: secondary; + padding: 0; + aspect-ratio: 1/1; + justify-items: center; +} diff --git a/anneal/v1/playground/rust-anneal-playground/ui/frontend/ButtonSet.tsx b/anneal/v1/playground/rust-anneal-playground/ui/frontend/ButtonSet.tsx new file mode 100644 index 0000000000..85a616872d --- /dev/null +++ b/anneal/v1/playground/rust-anneal-playground/ui/frontend/ButtonSet.tsx @@ -0,0 +1,81 @@ +import React, { type JSX } from 'react'; + +import Link, { LinkProps } from './uss-router/Link'; + +import * as styles from './ButtonSet.module.css'; + +interface ButtonSetProps { + className?: string; + children: React.ReactNode; +} + +export const ButtonSet: React.FC = ({ className = '', children }) => ( +
{children}
+); + +type HTMLButton = JSX.IntrinsicElements['button']; + +interface ButtonProps extends HTMLButton { + isPrimary?: boolean; + isSmall?: boolean; + iconLeft?: React.FC; + iconRight?: React.FC; +} + +export const Button: React.FC = ({ + isPrimary = false, + isSmall = false, + iconLeft: IconLeft, + iconRight: IconRight, + children, + ref, + ...props +}) => { + const iconLeft = IconLeft && ( + + + + ); + const iconRight = IconRight && ( + + + + ); + const ordinalStyle = isPrimary ? styles.primary : styles.secondary; + const smallStyle = isSmall ? styles.small : ''; + + return ( + + ); +}; + +export const Rule: React.FC = () => ; + +interface IconButtonProps extends HTMLButton { + isSmall?: boolean; +} + +export const IconButton: React.FC = ({ + ref, + isSmall = false, + children, + ...props +}) => { + const smallStyle = isSmall ? styles.small : ''; + + return ( + + ); +}; + +export const IconLink: React.FC = ({ children, ref, ...props }) => ( + + {children} + +); diff --git a/anneal/v1/playground/rust-anneal-playground/ui/frontend/ChannelMenu.module.css b/anneal/v1/playground/rust-anneal-playground/ui/frontend/ChannelMenu.module.css new file mode 100644 index 0000000000..cd2bed2509 --- /dev/null +++ b/anneal/v1/playground/rust-anneal-playground/ui/frontend/ChannelMenu.module.css @@ -0,0 +1,3 @@ +.description { + margin: 0; +} diff --git a/anneal/v1/playground/rust-anneal-playground/ui/frontend/ChannelMenu.tsx b/anneal/v1/playground/rust-anneal-playground/ui/frontend/ChannelMenu.tsx new file mode 100644 index 0000000000..1b07a7ef96 --- /dev/null +++ b/anneal/v1/playground/rust-anneal-playground/ui/frontend/ChannelMenu.tsx @@ -0,0 +1,69 @@ +import React, { Fragment, useCallback } from 'react'; + +import MenuGroup from './MenuGroup'; +import SelectOne from './SelectOne'; + +import * as config from './reducers/configuration'; +import * as selectors from './selectors'; +import { Channel } from './types'; +import { useAppDispatch, useAppSelector } from './hooks'; + +import * as styles from './ChannelMenu.module.css'; + +interface ChannelMenuProps { + close: () => void; +} + +const ChannelMenu: React.FC = props => { + const channel = useAppSelector((state) => state.configuration.channel); + const stableVersion = useAppSelector(selectors.stableVersionText); + const betaVersion = useAppSelector(selectors.betaVersionText); + const nightlyVersion = useAppSelector(selectors.nightlyVersionText); + const betaVersionDetails = useAppSelector(selectors.betaVersionDetailsText); + const nightlyVersionDetails = useAppSelector(selectors.nightlyVersionDetailsText); + + const dispatch = useAppDispatch(); + const changeChannel = useCallback((channel: Channel) => { + dispatch(config.changeChannel(channel)); + props.close(); + }, [dispatch, props]); + + return ( + + + + Build using the Stable version: {stableVersion} + + + Build using the Beta version: {betaVersion} + ({betaVersionDetails}) + + + Build using the Nightly version: {nightlyVersion} + ({nightlyVersionDetails}) + + + + ); +}; + +const Desc: React.FC> = ({ children }) => ( +

{children}

+); + +export default ChannelMenu; diff --git a/anneal/v1/playground/rust-anneal-playground/ui/frontend/ConfigElement.module.css b/anneal/v1/playground/rust-anneal-playground/ui/frontend/ConfigElement.module.css new file mode 100644 index 0000000000..b9369292cd --- /dev/null +++ b/anneal/v1/playground/rust-anneal-playground/ui/frontend/ConfigElement.module.css @@ -0,0 +1,73 @@ +.container { + display: flex; + align-items: center; +} + +.name { + flex: 1; +} + +.notDefault { + composes: name; + color: var(--header-tint); + font-weight: 600; +} + +.value { + flex: 1; +} + +.select { + width: 100%; + background: var(--button-secondary-bg-color); + border-color: var(--button-secondary-border-color); + color: var(--button-secondary-color); + + & option { + background-color: var(--button-secondary-bg-color-top); + border-color: var(--button-secondary-border-color); + color: var(--button-secondary-color); + } +} + +.toggle { + display: grid; + grid-template-columns: 1fr 1fr; + + & label { + cursor: pointer; + padding: 0 1em; + color: #777; + font-weight: 600; + text-align: center; + text-transform: uppercase; + border: 1px solid transparent; + + &:hover { + background: hsl(208deg 100% 43% / 10%); + } + + &:first-of-type { + border-radius: var(--header-border-radius) 0 0 var(--header-border-radius); + border-right-width: 0; + } + + &:last-of-type { + border-radius: 0 var(--header-border-radius) var(--header-border-radius) 0; + border-left-width: 0; + } + } + + & input { + display: none; + + &:checked + label { + background: var(--header-tint); + color: #fff; + } + + &:not(:checked) + label { + border-color: #bbb; + } + } +} diff --git a/anneal/v1/playground/rust-anneal-playground/ui/frontend/ConfigElement.tsx b/anneal/v1/playground/rust-anneal-playground/ui/frontend/ConfigElement.tsx new file mode 100644 index 0000000000..02f5ce67dd --- /dev/null +++ b/anneal/v1/playground/rust-anneal-playground/ui/frontend/ConfigElement.tsx @@ -0,0 +1,90 @@ +import React, { type JSX } from 'react'; + +import MenuItem from './MenuItem'; + +import * as styles from './ConfigElement.module.css'; + +interface EitherProps extends ConfigElementProps { + id: string; + a: string; + b: string; + aLabel?: string; + bLabel?: string; + value: T; + onChange: (_: T) => void; +} + +export const Either = ({ + id, + a, + b, + aLabel = a, + bLabel = b, + value, + onChange, + ...rest +}: EitherProps) => ( + +
+ onChange(a as T)} + /> + + onChange(b as T)} + /> + +
+
+); + +interface SelectProps extends ConfigElementProps { + children: React.ReactNode; + value: T; + onChange: (_: T) => void; +} + +export const Select = ({ + value, + onChange, + children, + ...rest +}: SelectProps) => ( + + + +); + +interface ConfigElementProps { + children?: React.ReactNode; + name: string; + isDefault?: boolean; + aside?: JSX.Element; +} + +const ConfigElement: React.FC = ({ name, isDefault, aside, children }) => { + const actuallyDefault = isDefault ?? true; + const defaultStyle = actuallyDefault ? styles.name : styles.notDefault; + + return ( + +
+ {name} +
{children}
+
+ {aside} +
+ ); +}; diff --git a/anneal/v1/playground/rust-anneal-playground/ui/frontend/ConfigMenu.tsx b/anneal/v1/playground/rust-anneal-playground/ui/frontend/ConfigMenu.tsx new file mode 100644 index 0000000000..aebd57a255 --- /dev/null +++ b/anneal/v1/playground/rust-anneal-playground/ui/frontend/ConfigMenu.tsx @@ -0,0 +1,153 @@ +/* global ACE_KEYBINDINGS:false, ACE_THEMES:false */ + +import React from 'react'; + +import { Either as EitherConfig, Select as SelectConfig } from './ConfigElement'; +import MenuGroup from './MenuGroup'; +import SimpleButtonMenuItem from './SimpleButtonMenuItem'; +import { useAppDispatch, useAppSelector } from './hooks'; +import * as client from './reducers/client'; +import * as config from './reducers/configuration'; +import { + AssemblyFlavor, + DemangleAssembly, + Editor, + Orientation, + PairCharacters, + ProcessAssembly, + Theme, +} from './types'; + +const MONACO_THEMES = [ + 'vs', 'vs-dark', 'vscode-dark-plus', +]; + +const ConfigMenu: React.FC = () => { + 'use memo'; + + const keybinding = useAppSelector((state) => state.configuration.ace.keybinding); + const aceTheme = useAppSelector((state) => state.configuration.ace.theme); + const monacoTheme = useAppSelector((state) => state.configuration.monaco.theme); + const theme = useAppSelector((state) => state.configuration.theme); + const orientation = useAppSelector((state) => state.configuration.orientation); + const editorStyle = useAppSelector((state) => state.configuration.editor); + const pairCharacters = useAppSelector((state) => state.configuration.ace.pairCharacters); + const assemblyFlavor = useAppSelector((state) => state.configuration.assemblyFlavor); + const demangleAssembly = useAppSelector((state) => state.configuration.demangleAssembly); + const processAssembly = useAppSelector((state) => state.configuration.processAssembly); + + const dispatch = useAppDispatch(); + + return ( + <> + + dispatch(config.changeEditor(e))} + > + {[Editor.Simple, Editor.Ace, Editor.Monaco] + .map(k => )} + + {editorStyle === Editor.Ace && ( + <> + dispatch(config.changeKeybinding(k))} + > + {ACE_KEYBINDINGS.map(k => )} + + + dispatch(config.changeAceTheme(t))} + > + {ACE_THEMES.map(t => )} + + + dispatch(config.changePairCharacters(p))} /> + + )} + {editorStyle === Editor.Monaco && ( + <> + dispatch(config.changeMonacoTheme(t))} + > + {MONACO_THEMES.map(t => )} + + + )} + + + + dispatch(config.changeTheme(t))}> + + + + + + dispatch(config.changeOrientation(o))} + > + + + + + + + + dispatch(config.changeAssemblyFlavor(a))} /> + + dispatch(config.changeDemangleAssembly(d))} + /> + + dispatch(config.changeProcessAssembly(p))} + /> + + + + dispatch(client.showConfigReset())}> + Reset all code and configuration to default values + + + + ); +}; + +export default ConfigMenu; diff --git a/anneal/v1/playground/rust-anneal-playground/ui/frontend/Header.module.css b/anneal/v1/playground/rust-anneal-playground/ui/frontend/Header.module.css new file mode 100644 index 0000000000..e119259143 --- /dev/null +++ b/anneal/v1/playground/rust-anneal-playground/ui/frontend/Header.module.css @@ -0,0 +1,12 @@ +.container, +.left, +.right { + display: flex; + gap: 0.5em; +} + +.container { + font-size: var(--secondary-font-size); + padding: 1.25em 0; + justify-content: space-between; +} diff --git a/anneal/v1/playground/rust-anneal-playground/ui/frontend/Header.tsx b/anneal/v1/playground/rust-anneal-playground/ui/frontend/Header.tsx new file mode 100644 index 0000000000..10cd8188bc --- /dev/null +++ b/anneal/v1/playground/rust-anneal-playground/ui/frontend/Header.tsx @@ -0,0 +1,197 @@ +import React, { RefObject, useCallback, useRef } from 'react'; + +import AdvancedOptionsMenu from './AdvancedOptionsMenu'; +import BuildMenu from './BuildMenu'; +import { ButtonSet, IconButton, IconLink, Button as OneButton, Rule } from './ButtonSet'; +import ChannelMenu from './ChannelMenu'; +import ConfigMenu from './ConfigMenu'; +import { + BuildIcon, + ConfigIcon, + ExpandableIcon, + HelpIcon, + MoreOptionsActiveIcon, + MoreOptionsIcon, +} from './Icon'; +import ModeMenu from './ModeMenu'; +import PopButton, { ButtonProps } from './PopButton'; +import ToolsMenu from './ToolsMenu'; +import * as actions from './actions'; +import { useAppDispatch, useAppSelector } from './hooks'; +import { performGistSave } from './reducers/output/gist'; +import { navigateToHelp } from './reducers/page'; +import * as selectors from './selectors'; + +import * as styles from './Header.module.css'; + +const Header: React.FC = () => { + const menuContainer = useRef(null); + + return ( + <> +
+
+ + + + + + + + + + + + +
+ +
+ + + + + + + + + + + + + + + +
+
+ +
+ + ); +}; + +interface PortalProps { + menuContainer: RefObject; +} + +const ExecuteButton: React.FC = () => { + const executionLabel = useAppSelector(selectors.getExecutionLabel); + + const dispatch = useAppDispatch(); + const execute = useCallback(() => dispatch(actions.performPrimaryAction()), [dispatch]); + + return ( + + {executionLabel} + + ); +}; + +const BuildMenuButton: React.FC = ({ menuContainer }) => { + const Button: React.FC = ({ toggle, ref }) => ( + + + + ); + + return ; +}; + +const ModeMenuButton: React.FC = ({ menuContainer }) => { + const label = useAppSelector(selectors.getModeLabel); + + const Button: React.FC = ({ toggle, ref }) => ( + + {label} + + ); + + return ; +}; + +const ChannelMenuButton: React.FC = ({ menuContainer }) => { + const label = useAppSelector(selectors.getChannelLabel); + + const Button: React.FC = ({ toggle, ref }) => ( + + {label} + + ); + + return ; +}; + +const AdvancedOptionsMenuButton: React.FC = ({ menuContainer }) => { + const advancedOptionsSet = useAppSelector(selectors.getAdvancedOptionsSet); + + const Button: React.FC = ({ toggle, ref }) => ( + + {advancedOptionsSet ? : } + + ); + + return ; +}; + +const ShareButton: React.FC = () => { + const dispatch = useAppDispatch(); + const gistSave = useCallback(() => dispatch(performGistSave()), [dispatch]); + + return ( + + Share + + ); +}; + +const ToolsMenuButton: React.FC = ({ menuContainer }) => { + const Button: React.FC = ({ toggle, ref }) => ( + + Tools + + ); + + return ; +}; + +const ConfigMenuButton: React.FC = ({ menuContainer }) => { + const Button: React.FC = ({ toggle, ref }) => ( + + Config + + ); + + return ; +}; + +const HelpButton: React.FC = () => ( + + + +); + +export default Header; diff --git a/anneal/v1/playground/rust-anneal-playground/ui/frontend/Help.module.css b/anneal/v1/playground/rust-anneal-playground/ui/frontend/Help.module.css new file mode 100644 index 0000000000..7973f0d381 --- /dev/null +++ b/anneal/v1/playground/rust-anneal-playground/ui/frontend/Help.module.css @@ -0,0 +1,64 @@ +.container { + margin: 1em auto; + color: var(--font-color-high-contrast); + background-color: var(--background-color-high-contrast); + padding: 1em; + max-width: 800px; + line-height: 1.25; +} + +.code { + background: var(--code-background-color); + padding: 0 0.25em; +} + +.logo { + text-align: center; +} + +.logoLight { + display: inherit; + + @media (prefers-color-scheme: dark) { + display: none; + } + + :root[data-theme='light'] & { + display: inherit; + } + + :root[data-theme='dark'] & { + display: none; + } +} + +.logoDark { + display: none; + + @media (prefers-color-scheme: dark) { + display: inherit; + } + + :root[data-theme='light'] & { + display: none; + } + + :root[data-theme='dark'] & { + display: inherit; + } +} + +.header { + &:hover::after { + content: ' §'; + } +} + +.headerLink { + color: var(--header-link-color); + text-decoration: none; + + &:hover { + text-decoration: underline; + } +} diff --git a/anneal/v1/playground/rust-anneal-playground/ui/frontend/Help.tsx b/anneal/v1/playground/rust-anneal-playground/ui/frontend/Help.tsx new file mode 100644 index 0000000000..008d6dec5e --- /dev/null +++ b/anneal/v1/playground/rust-anneal-playground/ui/frontend/Help.tsx @@ -0,0 +1,365 @@ +import React from 'react'; + +import Example from './HelpExample'; +import Link from './uss-router/Link'; + +import * as styles from './Help.module.css'; + +import integer32LogoLight from './assets/integer32-logo-light.svg'; +import integer32LogoDark from './assets/integer32-logo-dark.svg'; +import { navigateToIndex } from './reducers/page'; +import { useAppSelector } from './hooks'; +import { baseUrlSelector } from './selectors'; + +const ACE_URL = 'https://github.com/ajaxorg/ace'; +const MONACO_URL = 'https://microsoft.github.io/monaco-editor/'; +const CLIPPY_URL = 'https://github.com/rust-lang/rust-clippy'; +const MIRI_URL = 'https://github.com/rust-lang/miri'; +const CRATES_IO_URL = 'https://crates.io/'; +const RUST_COOKBOOK_URL = 'https://rust-lang-nursery.github.io/rust-cookbook/'; +const CRATES_URL = 'https://github.com/rust-lang/rust-playground/blob/main/compiler/base/Cargo.toml'; +const GIST_URL = 'https://gist.github.com/'; +const I32_URL = 'http://integer32.com/'; +const LOCALSTORAGE_URL = 'https://developer.mozilla.org/en-US/docs/Web/API/Web_Storage_API'; +const ORIGINAL_PLAYGROUND_URL = 'https://github.com/rust-lang/rust-playpen'; +const REPO_URL = 'https://github.com/rust-lang/rust-playground'; +const RUSTFMT_URL = 'https://github.com/rust-lang/rustfmt'; +const SHEPMASTER_URL = 'https://github.com/shepmaster/'; +const RUST_EDITION_2018_URL = 'https://doc.rust-lang.org/edition-guide/rust-2018/index.html'; + +const CRATE_EXAMPLE = `use rand::prelude::*; + +fn main() { + let mut rng = rand::rng(); + println!("{}", rng.random::()); +}`; + +const CLIPPY_EXAMPLE = `fn main() { + match true { + true => println!("true"), + false => println!("false"), + } +}`; + +const MIRI_EXAMPLE = `fn main() { + let mut a: [u8; 0] = []; + unsafe { + *a.get_unchecked_mut(1) = 1; + } +}`; + +const RUSTFMT_EXAMPLE = `// wow, this is ugly! +fn main () +{ struct Foo { a: u8, b: String, } +match 4 {2=>{},_=>{}} }`; + +const LINK_EXAMPLE_CODE = 'fn main() { println!("hello world!"); }'; + +const TEST_EXAMPLE = `#[test] +fn test_something() { + assert_ne!(42, 0); +}`; + +const LIBRARY_EXAMPLE = `#![crate_type="lib"] + +pub fn library_fn() -> u8 { + 42 +}`; + +const OUTPUT_EXAMPLE = `#[inline(never)] +pub fn a_loop() -> i32 { + let mut sum = 0; + for i in 0..100 { + sum += i; + } + sum +} + +fn main() { + println!("{}", a_loop()); +}`; + +const Help: React.FC = () => { + const baseUrl = useAppSelector(baseUrlSelector); + + // Deliberately **not** using normal tools that would escape + // characters. The example code we use is fine for URLs, and + // skipping the escaping makes the docs prettier. + const linkExample = `${baseUrl}?code=${LINK_EXAMPLE_CODE}`; + + return ( +
+

Anneal Playground

+ Return to the playground + + +

+ The playground is an open source project. + If you have any suggestions for features, issues with the + implementation, or just want to read the code for yourself, + you are invited to participate! +

+ +

+ This playground is modeled after the original + Rust playground, and we owe a great debt to every contributor to + that project. +

+ +

+ This playground was created by Jake Goulding, + part of Integer 32. +

+ +

+ + Integer 32 Logo + Integer 32 Logo + +

+
+ + + +

+ The playground provides the top 100 most downloaded crates + from crates.io, the crates from + the Rust Cookbook, and all + of their dependencies. To use a crate, add the appropriate + {' '} + extern crate foo line to the code, or, since + {' '} + Rust Edition 2018, just + {' '} + use any item from that crate. +

+ + + +

+ See the complete list of crates to know + what’s available. +

+
+ + +

+ rustfmt is a tool for formatting Rust code + according to the Rust style guidelines. Click on the Format + {' '} + button in the Tools menu to automatically reformat your code. +

+ + +
+ + +

+ Clippy is a collection of lints to catch common + mistakes and improve your Rust code. Click on the Clippy + {' '} + button in the Tools menu to see possible improvements to your + code. +

+ + +
+ + +

+ Miri is an interpreter for Rust’s mid-level intermediate + representation (MIR) and can be used to detect certain kinds of undefined behavior + in your unsafe Rust code. Click on the Miri button in + the Tools menu to check. +

+ + +
+ + +

+ Once you have some code worth saving or sharing, click on the + {' '} + Share button. This will create a + {' '} + GitHub Gist. You will also be provided with + a URL to load that Gist back into the playground. +

+
+ + +

+ If you have a web page with Rust code that you’d like to + show in action, you can link to the playground with the + Rust code in the query parameter code. Make sure to + escape any special characters. Keep the code short, as URLs have + limitations on the maximum length. +

+ +
{linkExample}
+
+ + +

+ If your code contains the #[test] attribute and does not + contain a main method, cargo test will be + executed instead of cargo run. +

+ + +
+ + +

+ If your code contains the #![crate_type="lib"] attribute, + {' '} + cargo build will be executed instead of cargo + run. +

+ + +
+ + +

+ Instead of executing the code, you can also see intermediate + output of the compiler as x86_64 assembly, LLVM IR, Rust MIR, or + WebAssembly. This is often used in conjunction with the + {' '} + mode set to “Release” to see how the + compiler has chosen to optimize some specific piece of code. +

+ + +
+ + +

+ Rust has two primary compilation modes: Debug and + {' '} + Release. Debug compiles code faster while Release + performs more aggressive optimizations. +

+ +

+ You can choose which mode to compile in using the Mode + {' '} + menu. +

+
+ + +

+ Rust releases new stable versions every 6 + weeks. Between these stable releases, beta versions of the + next stable release are made available. In addition, builds containing + experimental features are produced nightly. +

+ +

+ You can choose which channel to compile with using the + {' '} + Channel menu. +

+
+ + +

+ The Ajax.org Cloud9 Editor (Ace) is used to + provide a better interface for editing code. Ace comes with + several keybinding options (such as Emacs and Vim) as well as many + themes. +

+ +

+ You may choose to use the Monaco editor instead. + This editor is used in VS Code and may be more familiar to people who + use VS Code. +

+ +

+ You may also disable Ace and Monaco completely, falling back to a + simple HTML text area. +

+ +

+ These options can be configured via the Config menu. +

+
+ + +

+ The most recently entered code will be automatically saved in your browser’s + {' '} + local storage. This allows you to recover + your last work even if you close the browser. +

+ +

+ Local storage is a singleton resource, so if you use multiple windows, + only the most recently saved code will be persisted. +

+
+
+ + +

+ To prevent the playground from being used to attack other computers and + to ensure it is available for everyone to use, some limitations + are enforced. +

+ +
+
Network
+
+ There is no network connection available during compilation or + execution of user-submitted code. +
+ +
Memory
+
+ The amount of memory the compiler and resulting executable use is + limited. +
+ +
Execution Time
+
+ The total compilation and execution time is limited. +
+ +
Disk
+
+ The total disk space available to the compiler and resulting + executable is limited. +
+
+
+
+ ); +}; + +const LinkableSection: React.FC = ({ + id, header, level: Level, children, +}) => ( +
+ + + {header} + + + {children} +
+); + +interface LinkableSectionProps { + children: React.ReactNode; + id: string; + header: string; + level: React.ElementType; +} + +const Code: React.FC> = ({ children }) => ( + {children} +); + +export default Help; diff --git a/anneal/v1/playground/rust-anneal-playground/ui/frontend/HelpExample.module.css b/anneal/v1/playground/rust-anneal-playground/ui/frontend/HelpExample.module.css new file mode 100644 index 0000000000..b2e573b3d3 --- /dev/null +++ b/anneal/v1/playground/rust-anneal-playground/ui/frontend/HelpExample.module.css @@ -0,0 +1,21 @@ +.container { + position: relative; +} + +.loadExample { + position: absolute; + top: 0; + right: 0; + visibility: hidden; + opacity: 0; + transition: all 0.5s; + cursor: pointer; + border: none; + border-bottom-left-radius: 0.5em; + background: rgb(255 255 255 / 80%); +} + +.container:hover .loadExample { + visibility: visible; + opacity: 1; +} diff --git a/anneal/v1/playground/rust-anneal-playground/ui/frontend/HelpExample.tsx b/anneal/v1/playground/rust-anneal-playground/ui/frontend/HelpExample.tsx new file mode 100644 index 0000000000..423287440c --- /dev/null +++ b/anneal/v1/playground/rust-anneal-playground/ui/frontend/HelpExample.tsx @@ -0,0 +1,35 @@ +import React, { useCallback } from 'react'; +import root from 'react-shadow'; + +import Prism from './Prism'; +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 { + code: string; +} + +const HelpExample: React.FC = ({ code }) => { + const dispatch = useAppDispatch(); + const showExample = useCallback(() => dispatch(actions.showExample(code)), [dispatch, code]); + + return ( +
+ + + + + + {code} + +
+ ); +}; + +export default HelpExample; diff --git a/anneal/v1/playground/rust-anneal-playground/ui/frontend/Icon.module.css b/anneal/v1/playground/rust-anneal-playground/ui/frontend/Icon.module.css new file mode 100644 index 0000000000..fece51712e --- /dev/null +++ b/anneal/v1/playground/rust-anneal-playground/ui/frontend/Icon.module.css @@ -0,0 +1,4 @@ +.icon { + fill: currentcolor; + display: block; +} diff --git a/anneal/v1/playground/rust-anneal-playground/ui/frontend/Icon.tsx b/anneal/v1/playground/rust-anneal-playground/ui/frontend/Icon.tsx new file mode 100644 index 0000000000..02f647fc22 --- /dev/null +++ b/anneal/v1/playground/rust-anneal-playground/ui/frontend/Icon.tsx @@ -0,0 +1,70 @@ +import React from 'react'; + +import * as styles from './Icon.module.css'; + +// These icons came from Material Design originally +// https://material.io/tools/icons/?icon=assignment&style=outline + +// M.D. 'play_arrow' +export const BuildIcon = () => ( + + + +); + +// M.D. 'keyboard_arrow_down' +export const ExpandableIcon = () => ( + + + +); + +// M.D. 'more_horiz' +export const MoreOptionsIcon = () => ( + + + +); + +export const MoreOptionsActiveIcon = () => ( + + + +); + +// M.D. 'settings', +export const ConfigIcon = () => ( + + + +); + +// M.D. 'help_outline' +export const HelpIcon = () => ( + + + +); + +export const CheckmarkIcon = () => ( + + + +); + +// M.D. 'assignment' (outline) +export const ClipboardIcon = () => ( + + + + + + +); + +// M.D. 'close' +export const Close = () => ( + + + +); diff --git a/anneal/v1/playground/rust-anneal-playground/ui/frontend/Loader.module.css b/anneal/v1/playground/rust-anneal-playground/ui/frontend/Loader.module.css new file mode 100644 index 0000000000..28ef51e647 --- /dev/null +++ b/anneal/v1/playground/rust-anneal-playground/ui/frontend/Loader.module.css @@ -0,0 +1,23 @@ +@keyframes loader-fade { + 0% { + opacity: 0; + } + + 75% { + opacity: 1; + } +} + +.dot { + opacity: 0; + animation: loader-fade 1s; + animation-iteration-count: infinite; + + &:nth-child(2) { + animation-delay: 0.2s; + } + + &:nth-child(3) { + animation-delay: 0.4s; + } +} diff --git a/anneal/v1/playground/rust-anneal-playground/ui/frontend/Loader.tsx b/anneal/v1/playground/rust-anneal-playground/ui/frontend/Loader.tsx new file mode 100644 index 0000000000..9d3e14f814 --- /dev/null +++ b/anneal/v1/playground/rust-anneal-playground/ui/frontend/Loader.tsx @@ -0,0 +1,13 @@ +import React from 'react'; + +import * as styles from './Loader.module.css'; + +const Loader: React.FC = () => ( +
+ + + +
+); + +export default Loader; diff --git a/anneal/v1/playground/rust-anneal-playground/ui/frontend/MenuAside.module.css b/anneal/v1/playground/rust-anneal-playground/ui/frontend/MenuAside.module.css new file mode 100644 index 0000000000..0ec000cff9 --- /dev/null +++ b/anneal/v1/playground/rust-anneal-playground/ui/frontend/MenuAside.module.css @@ -0,0 +1,4 @@ +.aside { + margin: 0.25em 0 0; + color: #888; +} diff --git a/anneal/v1/playground/rust-anneal-playground/ui/frontend/MenuAside.tsx b/anneal/v1/playground/rust-anneal-playground/ui/frontend/MenuAside.tsx new file mode 100644 index 0000000000..e848746b36 --- /dev/null +++ b/anneal/v1/playground/rust-anneal-playground/ui/frontend/MenuAside.tsx @@ -0,0 +1,11 @@ +import React from 'react'; + +import * as styles from './MenuAside.module.css'; + +const MenuAside: React.FC> = ({ children }) => ( +

+ {children} +

+); + +export default MenuAside; diff --git a/anneal/v1/playground/rust-anneal-playground/ui/frontend/MenuGroup.module.css b/anneal/v1/playground/rust-anneal-playground/ui/frontend/MenuGroup.module.css new file mode 100644 index 0000000000..55bbc159da --- /dev/null +++ b/anneal/v1/playground/rust-anneal-playground/ui/frontend/MenuGroup.module.css @@ -0,0 +1,22 @@ +.container { + padding: 0.75em 1em 0; + width: 27em; + line-height: normal; + + &:last-child { + padding-bottom: 0.75em; + } +} + +.title { + margin: 0; + border-bottom: 1px solid var(--header-main-border); + padding-bottom: 10px; + font-weight: 700; + font-size: var(--secondary-font-size); + text-transform: uppercase; +} + +.content { + padding: 1em 0.25em; +} diff --git a/anneal/v1/playground/rust-anneal-playground/ui/frontend/MenuGroup.tsx b/anneal/v1/playground/rust-anneal-playground/ui/frontend/MenuGroup.tsx new file mode 100644 index 0000000000..43394e307d --- /dev/null +++ b/anneal/v1/playground/rust-anneal-playground/ui/frontend/MenuGroup.tsx @@ -0,0 +1,19 @@ +import React from 'react'; + +import * as styles from './MenuGroup.module.css'; + +interface MenuGroupProps { + children: React.ReactNode; + title: string; +} + +const MenuGroup: React.FC = ({ title, children }) => ( +
+

{title}

+
+ {children} +
+
+); + +export default MenuGroup; diff --git a/anneal/v1/playground/rust-anneal-playground/ui/frontend/MenuItem.module.css b/anneal/v1/playground/rust-anneal-playground/ui/frontend/MenuItem.module.css new file mode 100644 index 0000000000..3878db3f6f --- /dev/null +++ b/anneal/v1/playground/rust-anneal-playground/ui/frontend/MenuItem.module.css @@ -0,0 +1,7 @@ +.container { + margin-bottom: 1em; + + &:last-child { + margin-bottom: 0; + } +} diff --git a/anneal/v1/playground/rust-anneal-playground/ui/frontend/MenuItem.tsx b/anneal/v1/playground/rust-anneal-playground/ui/frontend/MenuItem.tsx new file mode 100644 index 0000000000..a685672571 --- /dev/null +++ b/anneal/v1/playground/rust-anneal-playground/ui/frontend/MenuItem.tsx @@ -0,0 +1,9 @@ +import React from 'react'; + +import * as styles from './MenuItem.module.css'; + +const MenuItem: React.FC> = ({ children }) => ( +
{children}
+); + +export default MenuItem; diff --git a/anneal/v1/playground/rust-anneal-playground/ui/frontend/ModeMenu.tsx b/anneal/v1/playground/rust-anneal-playground/ui/frontend/ModeMenu.tsx new file mode 100644 index 0000000000..ce010afd9e --- /dev/null +++ b/anneal/v1/playground/rust-anneal-playground/ui/frontend/ModeMenu.tsx @@ -0,0 +1,47 @@ +import React, { Fragment, useCallback } from 'react'; + +import MenuGroup from './MenuGroup'; +import SelectOne from './SelectOne'; + +import * as config from './reducers/configuration'; +import { Mode } from './types'; +import { useAppDispatch, useAppSelector } from './hooks'; + +interface ModeMenuProps { + close: () => void; +} + +const ModeMenu: React.FC = props => { + const mode = useAppSelector((state) => state.configuration.mode); + const dispatch = useAppDispatch(); + const changeMode = useCallback((mode: Mode) => { + dispatch(config.changeMode(mode)); + props.close(); + }, [dispatch, props] + ); + + return ( + + + + Build with debug information, without optimizations. + + + Build with optimizations turned on. + + + + ); +}; + +export default ModeMenu; diff --git a/anneal/v1/playground/rust-anneal-playground/ui/frontend/Notifications.module.css b/anneal/v1/playground/rust-anneal-playground/ui/frontend/Notifications.module.css new file mode 100644 index 0000000000..1023219a57 --- /dev/null +++ b/anneal/v1/playground/rust-anneal-playground/ui/frontend/Notifications.module.css @@ -0,0 +1,41 @@ +$buffer: 0.5em; +$space: 0.25em; + +.container { + position: absolute; + right: $buffer; + bottom: $buffer; + left: $buffer; + z-index: 10; + margin-right: auto; + margin-left: auto; + border: 2px solid #428bca; + background: var(--background-color); + max-width: 50em; +} + +.notification { + display: flex; +} + +.notificationContent { + padding: $space 0 $space $space; + width: 100%; +} + +.close { + composes: -buttonReset from './shared.module.css'; + cursor: pointer; + padding: $space; +} + +.action { + display: flex; + justify-content: center; + padding-top: 0.5em; + gap: 0.5em; +} + +.swapTheme { + width: 100%; +} diff --git a/anneal/v1/playground/rust-anneal-playground/ui/frontend/Notifications.tsx b/anneal/v1/playground/rust-anneal-playground/ui/frontend/Notifications.tsx new file mode 100644 index 0000000000..691c59cfe0 --- /dev/null +++ b/anneal/v1/playground/rust-anneal-playground/ui/frontend/Notifications.tsx @@ -0,0 +1,129 @@ +import React, { useCallback } from 'react'; +import { Portal } from 'react-portal'; + +import { Close } from './Icon'; +import { useAppDispatch, useAppSelector } from './hooks'; +import * as client from './reducers/client'; +import { seenRustSurvey2025 } from './reducers/notifications'; +import { allowLongRun, wsExecuteKillCurrent } from './reducers/output/execute'; +import * as selectors from './selectors'; + +import * as styles from './Notifications.module.css'; + +const SURVEY_URL = 'https://blog.rust-lang.org/2025/11/17/launching-the-2025-state-of-rust-survey/'; + +const Notifications: React.FC = () => { + return ( + +
+ + + + +
+
+ ); +}; + +const RustSurvey2025Notification: React.FC = () => { + const showIt = useAppSelector(selectors.showRustSurvey2025Selector); + + const dispatch = useAppDispatch(); + const seenIt = useCallback(() => dispatch(seenRustSurvey2025()), [dispatch]); + + return showIt ? ( + + Please help us take a look at who the Rust community is composed of, how the Rust project is + doing, and how we can improve the Rust programming experience by completing the{' '} + 2025 State of Rust Survey. Whether or not you use Rust today, we want + to know your opinions. + + ) : null; +}; + +const ExcessiveExecutionNotification: React.FC = () => { + const showExcessiveExecution = useAppSelector(selectors.excessiveExecutionSelector); + const time = useAppSelector(selectors.excessiveExecutionTimeSelector); + const gracePeriod = useAppSelector(selectors.killGracePeriodTimeSelector); + + const dispatch = useAppDispatch(); + const allow = useCallback(() => dispatch(allowLongRun()), [dispatch]); + const kill = useCallback(() => dispatch(wsExecuteKillCurrent()), [dispatch]); + + return showExcessiveExecution ? ( + + The running process has used more than {time} of CPU time. This is often caused by an error in + the code. As the playground is a shared resource, the process will be automatically killed in{' '} + {gracePeriod}. You can always kill the process manually via the menu at the bottom of the + screen. +
+ + +
+
+ ) : null; +}; + +interface ResetNotificationCommonProps { + preamble?: string; + onReset: () => void; + onCancel: () => void; +} + +const ResetNotificationCommon: React.FC = ({ + preamble, + onReset, + onCancel, +}) => ( + + {preamble} + Would you like to reset all code and configuration back to the default values to get a fresh + start? +
+ + +
+
+); + +const ResetConfigurationNotification: React.FC = () => { + const showResetConfiguration = useAppSelector(selectors.resetConfigurationSelector); + + const dispatch = useAppDispatch(); + const reset = useCallback(() => dispatch(client.resetEverything()), [dispatch]); + const keep = useCallback(() => dispatch(client.hideConfigReset()), [dispatch]); + + return showResetConfiguration ? ( + + ) : null; +}; + +const ResetOldConfigurationNotification: React.FC = () => { + const showResetOldConfiguration = useAppSelector(selectors.resetOldConfigurationSelector); + + const dispatch = useAppDispatch(); + const reset = useCallback(() => dispatch(client.resetEverything()), [dispatch]); + const keep = useCallback(() => dispatch(client.updateLastVisitedAt()), [dispatch]); + + const preamble = "It's been a while since you've used the Playground. "; + + return showResetOldConfiguration ? ( + + ) : null; +}; + +interface NotificationProps { + children: React.ReactNode; + onClose: () => void; +} + +const Notification: React.FC = ({ onClose, children }) => ( +
+
{children}
+ +
+); + +export default Notifications; diff --git a/anneal/v1/playground/rust-anneal-playground/ui/frontend/Output.module.css b/anneal/v1/playground/rust-anneal-playground/ui/frontend/Output.module.css new file mode 100644 index 0000000000..3af99967d2 --- /dev/null +++ b/anneal/v1/playground/rust-anneal-playground/ui/frontend/Output.module.css @@ -0,0 +1,53 @@ +.container { + display: flex; + flex-direction: column; + width: 100%; + height: 100%; +} + +.tabs { + display: flex; +} + +.tab { + color: var(--font-color); + flex: 1 1 auto; + cursor: pointer; + border: var(--border); + border-right: none; + background-color: var(--output-background-tab); + line-height: 1.5; + + &:last-of-type { + border-right: var(--border); + } +} + +.tabSelected { + composes: tab; + cursor: default; + border-bottom: none; + background-color: var(--output-current-tab); + + &:focus { + outline: none; + } +} + +.tabClose { + composes: tab; + flex: 0 0 auto; +} + +.body { + border: var(--border); + border-top: none; + background-color: var(--output-current-tab); + padding: 0.5em; + height: 100%; + overflow: scroll; +} + +.stdin { + margin-top: 0.25em; +} diff --git a/anneal/v1/playground/rust-anneal-playground/ui/frontend/Output.tsx b/anneal/v1/playground/rust-anneal-playground/ui/frontend/Output.tsx new file mode 100644 index 0000000000..c5afdd1e10 --- /dev/null +++ b/anneal/v1/playground/rust-anneal-playground/ui/frontend/Output.tsx @@ -0,0 +1,148 @@ +import React, { useCallback } from 'react'; + +import { changeFocus } from './reducers/output/meta'; +import { Focus } from './types'; + +import Execute from './Output/Execute'; +import Assembly from './Output/Assembly'; +import Gist from './Output/Gist'; +import SimplePane from './Output/SimplePane'; +import PaneWithMir from './Output/PaneWithMir'; +import PaneWithCode from './Output/PaneWithCode'; +import * as selectors from './selectors'; +import { useAppDispatch, useAppSelector } from './hooks'; + +import * as styles from './Output.module.css'; +import Stdin from './Stdin'; +import LlvmIr from './Output/LlvmIr'; + +const Tab: React.FC = ({ kind, focus, label, onClick, tabProps }) => { + if (selectors.hasProperties(tabProps)) { + return ( + + ); + } else { + return null; + } +}; + +interface TabProps { + kind: Focus; + focus?: Focus; + label: string; + onClick: () => void; + tabProps: object; +} + +const Output: React.FC = () => { + const somethingToShow = useAppSelector(selectors.getSomethingToShow); + const { meta: { focus }, execute, format, clippy, miri, macroExpansion, assembly, llvmIr, mir, hir, wasm, gist } = + useAppSelector((state) => state.output); + + const dispatch = useAppDispatch(); + const focusClose = useCallback(() => dispatch(changeFocus()), [dispatch]); + const focusExecute = useCallback(() => dispatch(changeFocus(Focus.Execute)), [dispatch]); + const focusFormat = useCallback(() => dispatch(changeFocus(Focus.Format)), [dispatch]); + const focusClippy = useCallback(() => dispatch(changeFocus(Focus.Clippy)), [dispatch]); + const focusMiri = useCallback(() => dispatch(changeFocus(Focus.Miri)), [dispatch]); + const focusMacroExpansion = useCallback(() => dispatch(changeFocus(Focus.MacroExpansion)), [dispatch]); + const focusAssembly = useCallback(() => dispatch(changeFocus(Focus.Asm)), [dispatch]); + const focusLlvmIr = useCallback(() => dispatch(changeFocus(Focus.LlvmIr)), [dispatch]); + const focusMir = useCallback(() => dispatch(changeFocus(Focus.Mir)), [dispatch]); + const focusHir = useCallback(() => dispatch(changeFocus(Focus.Hir)), [dispatch]); + const focusWasm = useCallback(() => dispatch(changeFocus(Focus.Wasm)), [dispatch]); + const focusGist = useCallback(() => dispatch(changeFocus(Focus.Gist)), [dispatch]); + + const showStdin = useAppSelector(selectors.showStdinSelector); + + if (!somethingToShow) { + return null; + } + + let close: React.ReactElement | null = null; + let body: React.ReactElement | null = null; + if (focus) { + close = ; + + body = ( + <> +
+ {focus === Focus.Execute && } + {focus === Focus.Format && } + {focus === Focus.Clippy && } + {focus === Focus.Miri && } + {focus === Focus.MacroExpansion && } + {focus === Focus.Asm && } + {focus === Focus.LlvmIr && } + {focus === Focus.Mir && } + {focus === Focus.Hir && } + {focus === Focus.Wasm && } + {focus === Focus.Gist && } +
+ {showStdin && ( +
+ +
+ )} + + ); + } + + return ( +
+
+ + + + + + + + + + + + {close} +
+ { body } +
+ ); +}; + +export default Output; diff --git a/anneal/v1/playground/rust-anneal-playground/ui/frontend/Output/Assembly.tsx b/anneal/v1/playground/rust-anneal-playground/ui/frontend/Output/Assembly.tsx new file mode 100644 index 0000000000..d16ff3f2c5 --- /dev/null +++ b/anneal/v1/playground/rust-anneal-playground/ui/frontend/Output/Assembly.tsx @@ -0,0 +1,24 @@ +import React from 'react'; + +import { useAppSelector } from '../hooks'; +import * as selectors from '../selectors'; +import PaneWithCode from './PaneWithCode'; +import WarnAboutNoSymbols from './WarnAboutNoSymbols'; + +const Assembly: React.FC = () => { + const assembly = useAppSelector((state) => state.output.assembly); + const isAssemblyInProgress = useAppSelector(selectors.isAssemblyInProgressSelector); + const hasAssemblySymbols = useAppSelector(selectors.hasAssemblySymbolsSelector); + + return ( + + + + ); +}; + +export default Assembly; diff --git a/anneal/v1/playground/rust-anneal-playground/ui/frontend/Output/Execute.module.css b/anneal/v1/playground/rust-anneal-playground/ui/frontend/Output/Execute.module.css new file mode 100644 index 0000000000..21251d9b74 --- /dev/null +++ b/anneal/v1/playground/rust-anneal-playground/ui/frontend/Output/Execute.module.css @@ -0,0 +1,3 @@ +.addMain { + composes: -buttonAsLink from '../shared.module.css'; +} diff --git a/anneal/v1/playground/rust-anneal-playground/ui/frontend/Output/Execute.tsx b/anneal/v1/playground/rust-anneal-playground/ui/frontend/Output/Execute.tsx new file mode 100644 index 0000000000..b03a823619 --- /dev/null +++ b/anneal/v1/playground/rust-anneal-playground/ui/frontend/Output/Execute.tsx @@ -0,0 +1,43 @@ +import React, { useCallback } from 'react'; + +import * as selectors from '../selectors'; +import * as code from '../reducers/code'; +import { useAppDispatch, useAppSelector } from '../hooks'; + +import Section from './Section'; +import SimplePane from './SimplePane'; + +import * as styles from './Execute.module.css'; + +const Execute: React.FC = () => { + const details = useAppSelector((state) => state.output.execute); + const isAutoBuild = useAppSelector(selectors.isAutoBuildSelector); + + const dispatch = useAppDispatch(); + const addMainFunction = useCallback(() => dispatch(code.addMainFunction()), [dispatch]); + + return ( + + {isAutoBuild && } + + ); +}; + +interface WarningProps { + addMainFunction: () => void; +} + +const Warning: React.FC = props => ( +
+ No main function was detected, so your code was compiled + {'\n'} + but not run. If you’d like to execute your code, please + {'\n'} + + . +
+); + +export default Execute; diff --git a/anneal/v1/playground/rust-anneal-playground/ui/frontend/Output/Gist.module.css b/anneal/v1/playground/rust-anneal-playground/ui/frontend/Output/Gist.module.css new file mode 100644 index 0000000000..fb5aba29cb --- /dev/null +++ b/anneal/v1/playground/rust-anneal-playground/ui/frontend/Output/Gist.module.css @@ -0,0 +1,40 @@ +.container { + display: flex; + + --copied-duration: 1s ease-in-out; +} + +.button { + composes: -buttonReset from '../shared.module.css'; + transition: color var(--copied-duration); + cursor: pointer; + margin: 0 0.25em 0 0.5em; +} + +.text { + visibility: hidden; + opacity: 0; + transition: + visibility var(--copied-duration), + opacity var(--copied-duration), + color var(--copied-duration); +} + +.active { + composes: container; + + & .button { + transition: color 0s; + color: green; + } + + & .text { + visibility: visible; + opacity: 1; + transition: + visibility 0s, + opacity 0s, + color 0s; + color: green; + } +} diff --git a/anneal/v1/playground/rust-anneal-playground/ui/frontend/Output/Gist.tsx b/anneal/v1/playground/rust-anneal-playground/ui/frontend/Output/Gist.tsx new file mode 100644 index 0000000000..0233f8e6a6 --- /dev/null +++ b/anneal/v1/playground/rust-anneal-playground/ui/frontend/Output/Gist.tsx @@ -0,0 +1,101 @@ +import React, { useState } from 'react'; + +import { ClipboardIcon } from '../Icon'; +import { useAppSelector } from '../hooks'; +import * as selectors from '../selectors/gist'; +import Loader from './Loader'; +import Section from './Section'; + +import * as styles from './Gist.module.css'; + +const Gist: React.FC = () => { + 'use memo'; + + const showLoader = useAppSelector(selectors.showGistLoaderSelector); + const error = useAppSelector((state) => state.output.gist.error); + + if (showLoader) { + return ; + } + + if (error) { + return ; + } + + return ; +}; + +const Error: React.FC<{ error: string }> = ({ error }) => ( +
+ {error} +
+); + +interface CopiedProps { + children: React.ReactNode; + href: string; +} + +const Copied: React.FC = ({ children, href }) => { + 'use memo'; + + const [copied, setCopied] = useState(false); + + const startCopy = () => { + setCopied(true); + window.navigator.clipboard.writeText(href); + + setTimeout(() => { + setCopied(false); + }, 1000); + }; + + return ( +

+ {children} + + Copied! +

+ ); +}; + +const Links: React.FC = () => { + 'use memo'; + + const codeUrl = useAppSelector(selectors.codeUrlSelector); + const gistUrl = useAppSelector((state) => state.output.gist.url); + const permalink = useAppSelector(selectors.permalinkSelector); + const urloUrl = useAppSelector(selectors.urloUrlSelector); + const textChanged = useAppSelector(selectors.textChangedSinceShareSelector); + + return ( + <> + Permalink to the playground + {gistUrl ? Direct link to the gist : null} + Embedded code in link + Open a new thread in the Rust user forum + {textChanged ? ( +
+ Source code has been changed since gist was saved +
+ ) : null} + + ); +}; + +interface NewWindowProps { + children: React.ReactNode; + href: string; +} + +const NewWindow: React.FC = (props) => ( +

+ + {props.children} + +

+); + +export default Gist; diff --git a/anneal/v1/playground/rust-anneal-playground/ui/frontend/Output/Header.module.css b/anneal/v1/playground/rust-anneal-playground/ui/frontend/Output/Header.module.css new file mode 100644 index 0000000000..a1722173d0 --- /dev/null +++ b/anneal/v1/playground/rust-anneal-playground/ui/frontend/Output/Header.module.css @@ -0,0 +1,21 @@ +.container { + display: flex; + color: var(--font-color); + white-space: nowrap; + + &::before, + &::after { + flex: 1 1 auto; + margin: auto; + border-top: var(--border); + content: ''; + } + + &::before { + margin-right: 2%; + } + + &::after { + margin-left: 2%; + } +} diff --git a/anneal/v1/playground/rust-anneal-playground/ui/frontend/Output/Header.tsx b/anneal/v1/playground/rust-anneal-playground/ui/frontend/Output/Header.tsx new file mode 100644 index 0000000000..3a5ab270c1 --- /dev/null +++ b/anneal/v1/playground/rust-anneal-playground/ui/frontend/Output/Header.tsx @@ -0,0 +1,13 @@ +import React from 'react'; + +import * as styles from './Header.module.css'; + +interface HeaderProps { + label: string; +} + +const Header: React.FC = ({ label }) => ( + {label} +); + +export default Header; diff --git a/anneal/v1/playground/rust-anneal-playground/ui/frontend/Output/LlvmIr.tsx b/anneal/v1/playground/rust-anneal-playground/ui/frontend/Output/LlvmIr.tsx new file mode 100644 index 0000000000..96cb2b2d49 --- /dev/null +++ b/anneal/v1/playground/rust-anneal-playground/ui/frontend/Output/LlvmIr.tsx @@ -0,0 +1,24 @@ +import React from 'react'; + +import { useAppSelector } from '../hooks'; +import * as selectors from '../selectors'; +import PaneWithCode from './PaneWithCode'; +import WarnAboutNoSymbols from './WarnAboutNoSymbols'; + +const LlvmIr: React.FC = () => { + const llvmIr = useAppSelector((state) => state.output.llvmIr); + const isLlvmIrInProgress = useAppSelector(selectors.isLlvmIrInProgressSelector); + const hasLlvmIrSymbols = useAppSelector(selectors.hasLlvmIrSymbolsSelector); + + return ( + + + + ); +}; + +export default LlvmIr; diff --git a/anneal/v1/playground/rust-anneal-playground/ui/frontend/Output/Loader.tsx b/anneal/v1/playground/rust-anneal-playground/ui/frontend/Output/Loader.tsx new file mode 100644 index 0000000000..859911173f --- /dev/null +++ b/anneal/v1/playground/rust-anneal-playground/ui/frontend/Output/Loader.tsx @@ -0,0 +1,13 @@ +import React from 'react'; + +import GenericLoader from '../Loader'; +import Header from './Header'; + +const Loader: React.FC = () => ( +
+
+ +
+); + +export default Loader; diff --git a/anneal/v1/playground/rust-anneal-playground/ui/frontend/Output/OutputPrism.module.css b/anneal/v1/playground/rust-anneal-playground/ui/frontend/Output/OutputPrism.module.css new file mode 100644 index 0000000000..c2225ef3cb --- /dev/null +++ b/anneal/v1/playground/rust-anneal-playground/ui/frontend/Output/OutputPrism.module.css @@ -0,0 +1,3 @@ +.container { + composes: -bodyMonospace from '../shared.module.css'; +} diff --git a/anneal/v1/playground/rust-anneal-playground/ui/frontend/Output/OutputPrism.tsx b/anneal/v1/playground/rust-anneal-playground/ui/frontend/Output/OutputPrism.tsx new file mode 100644 index 0000000000..8a5ccef75a --- /dev/null +++ b/anneal/v1/playground/rust-anneal-playground/ui/frontend/Output/OutputPrism.tsx @@ -0,0 +1,18 @@ +import React from 'react'; + +import Prism from '../Prism'; + +import * as styles from './OutputPrism.module.css'; + +interface OutputPrismProps { + children?: string; + language: 'rust_mir' | 'rust_errors'; +} + +const OutputPrism: React.FC = ({ language, children }) => ( + + {children} + +); + +export default OutputPrism; diff --git a/anneal/v1/playground/rust-anneal-playground/ui/frontend/Output/PaneWithCode.tsx b/anneal/v1/playground/rust-anneal-playground/ui/frontend/Output/PaneWithCode.tsx new file mode 100644 index 0000000000..a1cd3a84ca --- /dev/null +++ b/anneal/v1/playground/rust-anneal-playground/ui/frontend/Output/PaneWithCode.tsx @@ -0,0 +1,19 @@ +import React from 'react'; + +import Section from './Section'; +import SimplePane, { SimplePaneProps } from './SimplePane'; + +export interface PaneWithCodeProps extends SimplePaneProps { + code?: string; +} + +const PaneWithCode: React.FC = ({ children, code, ...rest }) => ( + +
+ {code} +
+ {children} +
+); + +export default PaneWithCode; diff --git a/anneal/v1/playground/rust-anneal-playground/ui/frontend/Output/PaneWithMir.tsx b/anneal/v1/playground/rust-anneal-playground/ui/frontend/Output/PaneWithMir.tsx new file mode 100644 index 0000000000..92b38997ef --- /dev/null +++ b/anneal/v1/playground/rust-anneal-playground/ui/frontend/Output/PaneWithMir.tsx @@ -0,0 +1,20 @@ +import React from 'react'; + +import Header from './Header'; +import OutputPrism from './OutputPrism'; +import SimplePane, { SimplePaneProps } from './SimplePane'; + +interface PaneWithMirProps extends SimplePaneProps { + code?: string; +} + +const PaneWithMir: React.FC = ({ code, ...rest }) => ( + +
+
+ {code} +
+
+); + +export default PaneWithMir; diff --git a/anneal/v1/playground/rust-anneal-playground/ui/frontend/Output/Section.module.css b/anneal/v1/playground/rust-anneal-playground/ui/frontend/Output/Section.module.css new file mode 100644 index 0000000000..7d51fd08db --- /dev/null +++ b/anneal/v1/playground/rust-anneal-playground/ui/frontend/Output/Section.module.css @@ -0,0 +1,3 @@ +.code { + composes: -bodyMonospace from '../shared.module.css'; +} diff --git a/anneal/v1/playground/rust-anneal-playground/ui/frontend/Output/Section.tsx b/anneal/v1/playground/rust-anneal-playground/ui/frontend/Output/Section.tsx new file mode 100644 index 0000000000..063d223e5d --- /dev/null +++ b/anneal/v1/playground/rust-anneal-playground/ui/frontend/Output/Section.tsx @@ -0,0 +1,22 @@ +import React from 'react'; + +import Header from './Header'; + +import * as styles from './Section.module.css'; + +interface SectionProps { + children: React.ReactNode; + kind: string; + label: string; +} + +const Section: React.FC = ({ kind, label, children }) => ( + React.Children.count(children) === 0 ? null : ( +
+
+
{children}
+
+ ) +); + +export default Section; diff --git a/anneal/v1/playground/rust-anneal-playground/ui/frontend/Output/SimplePane.tsx b/anneal/v1/playground/rust-anneal-playground/ui/frontend/Output/SimplePane.tsx new file mode 100644 index 0000000000..f5835e06cd --- /dev/null +++ b/anneal/v1/playground/rust-anneal-playground/ui/frontend/Output/SimplePane.tsx @@ -0,0 +1,57 @@ +import React from 'react'; + +import Header from './Header'; +import Loader from './Loader'; +import OutputPrism from './OutputPrism'; +import Section from './Section'; + +interface HighlightErrorsProps { + children?: string; + label: string; +} + +const onOutputPrismCopy: React.ClipboardEventHandler = (event) => { + // Blank out HTML copy data. + // Though linkified output is handy in the Playground, it does more harm + // than good when copied elsewhere, and terminal output is usable on its own. + const selection = document.getSelection(); + if (selection) { + event.clipboardData.setData('text/plain', selection.toString()); + event.preventDefault(); + } +}; + +const HighlightErrors: React.FC = ({ label, children }) => ( +
+
+ {children} +
+); + +export interface SimplePaneProps extends ReallySimplePaneProps { + children?: React.ReactNode; + kind: string; +} + +export interface ReallySimplePaneProps { + requestsInProgress: number; + stdout?: string; + stderr?: string; + error?: string; +} + +const SimplePane: React.FC = (props) => ( +
+ {props.requestsInProgress > 0 && } +
+ {props.error} +
+ {props.stderr} +
+ {props.stdout} +
+ {props.children} +
+); + +export default SimplePane; diff --git a/anneal/v1/playground/rust-anneal-playground/ui/frontend/Output/WarnAboutNoSymbols.tsx b/anneal/v1/playground/rust-anneal-playground/ui/frontend/Output/WarnAboutNoSymbols.tsx new file mode 100644 index 0000000000..5f77c84e0b --- /dev/null +++ b/anneal/v1/playground/rust-anneal-playground/ui/frontend/Output/WarnAboutNoSymbols.tsx @@ -0,0 +1,35 @@ +import React from 'react'; + +import Section from './Section'; + +export interface WarnAboutNoSymbolsProps { + isInProgress: boolean; + hasSymbols: boolean; + name: string; +} + +const WarnAboutNoSymbols: React.FC = ({ + isInProgress, + hasSymbols, + name, +}) => { + const warnAboutNoSymbols = !isInProgress && !hasSymbols; + + if (!warnAboutNoSymbols) { + return null; + } + + return ( +
+ No symbols detected — they may have been optimized away. + {'\n'} + Add the #[unsafe(no_mangle)] attribute to + {'\n'} + functions you want to see {name} for. Generic functions + {'\n'} + only generate {name} when concrete types are provided. +
+ ); +}; + +export default WarnAboutNoSymbols; diff --git a/anneal/v1/playground/rust-anneal-playground/ui/frontend/PageSwitcher.tsx b/anneal/v1/playground/rust-anneal-playground/ui/frontend/PageSwitcher.tsx new file mode 100644 index 0000000000..e14d7bbae6 --- /dev/null +++ b/anneal/v1/playground/rust-anneal-playground/ui/frontend/PageSwitcher.tsx @@ -0,0 +1,18 @@ +import React from 'react'; + +import Help from './Help'; +import Playground from './Playground'; +import { useAppSelector } from './hooks'; + +const PageSwitcher: React.FC = () => { + const page = useAppSelector((state) => state.page); + + switch (page) { + case 'index': + return ; + case 'help': + return ; + } +} + +export default PageSwitcher; diff --git a/anneal/v1/playground/rust-anneal-playground/ui/frontend/Playground.module.css b/anneal/v1/playground/rust-anneal-playground/ui/frontend/Playground.module.css new file mode 100644 index 0000000000..18ed9f5031 --- /dev/null +++ b/anneal/v1/playground/rust-anneal-playground/ui/frontend/Playground.module.css @@ -0,0 +1,101 @@ +.container { + display: flex; + flex-direction: column; + padding-bottom: 1em; + height: 100vh; +} + +.playground { + composes: -autoSize from './shared.module.css'; + + --minimum: 100px; + --gutter-dimension: 12px; + --final-editor-dimension: minmax(var(--minimum), 1fr); + --final-output-width: minmax(var(--minimum), var(--output-width, 1fr)); + + /* Ensure the output height leaves room for the editor and the + * gutter. Otherwise, the output can grow such that the (editor + + * gutter + output) height causes the whole container to grow. + */ + --output-height-cap: min( + var(--output-height), + calc(100% - var(--minimum) - var(--gutter-dimension)) + ); + --final-output-height: minmax(var(--minimum), var(--output-height-cap, 1fr)); + + display: grid; + grid-template-areas: 'eee'; + grid-template-columns: var(--final-editor-dimension); +} + +.playground[data-orientation='horizontal'][data-output-mode='full'] { + grid-template-areas: + 'eee' + 'eog' + 'ooo'; + grid-template-rows: + var(--final-editor-dimension) + var(--gutter-dimension) + var(--final-output-height); +} + +.playground[data-orientation='horizontal'][data-output-mode='slim'] { + grid-template-areas: + 'eee' + 'ooo'; + grid-template-rows: + var(--final-editor-dimension) + auto; +} + +.playground[data-orientation='vertical'][data-output-mode='full'] { + grid-template-areas: 'eee eog ooo'; + grid-template-columns: + var(--final-editor-dimension) + var(--gutter-dimension) + var(--final-output-width); +} + +.playground[data-orientation='vertical'][data-output-mode='slim'] { + grid-template-areas: 'eee ooo'; + grid-template-columns: + var(--final-editor-dimension) + auto; +} + +.editor { + grid-area: eee; + border: 4px solid var(--border-color); + border-radius: 4px; +} + +.editorOutputGutter { + grid-area: eog; +} + +.output { + composes: -autoSize from './shared.module.css'; + grid-area: ooo; +} + +.gutter { + display: flex; + justify-content: center; + align-items: center; +} + +.gutter[data-orientation='vertical'] { + cursor: col-resize; +} + +.gutterHandle { + user-select: none; +} + +.gutter[data-orientation='horizontal'] { + cursor: row-resize; + + & .gutterHandle { + transform: rotate(90deg); + } +} diff --git a/anneal/v1/playground/rust-anneal-playground/ui/frontend/Playground.tsx b/anneal/v1/playground/rust-anneal-playground/ui/frontend/Playground.tsx new file mode 100644 index 0000000000..0f3c47c57a --- /dev/null +++ b/anneal/v1/playground/rust-anneal-playground/ui/frontend/Playground.tsx @@ -0,0 +1,259 @@ +import React, { Activity, useRef, useState } from 'react'; + +import Header from './Header'; +import Notifications from './Notifications'; +import Output from './Output'; +import Editor from './editor/Editor'; +import { useAppSelector } from './hooks'; +import * as selectors from './selectors'; +import { Orientation } from './types'; + +import * as styles from './Playground.module.css'; + +interface Distances { + toTop: number; + toRight: number; + toBottom: number; + toLeft: number; +} + +const clamp = (val: number, min: number, max: number) => Math.min(Math.max(val, min), max); +const clampPositive = (v: number): number => (v > 0 ? v : 0); + +const distances = (bounds: DOMRectReadOnly, evt: React.MouseEvent): Distances => { + const { top, right, bottom, left } = bounds; + + const pageX = clamp(evt.pageX, left, right); + const pageY = clamp(evt.pageY, top, bottom); + + return { + toTop: pageY - top, + toLeft: pageX - left, + toRight: right - pageX, + toBottom: bottom - pageY, + }; +}; + +const subtractDistances = (a: Distances, b: Distances): Distances => ({ + toTop: clampPositive(a.toTop - b.toTop), + toRight: clampPositive(a.toRight - b.toRight), + toBottom: clampPositive(a.toBottom - b.toBottom), + toLeft: clampPositive(a.toLeft - b.toLeft), +}); + +interface GutterProps { + container: React.RefObject; + className: string; + orientation: Orientation; + hidden?: boolean; + onMove: (distances: Distances) => void; + onReset: () => void; +} + +const Gutter: React.FC = ({ container, className, orientation, onMove, onReset }) => { + 'use memo'; + + // Values assumed to stay stable during a drag + interface Cache { + // The container's position and size. The container can not move + // or resize while we are dragging. + containerBounds: DOMRectReadOnly; + + // The pointer's offset within the gutter. We take the offsets + // into account while dragging to avoid an initial jump. + initialPointerOffsets: Distances; + } + const cache = useRef(null); + const pendingCallback = useRef(null); + + const onClick = (evt: React.MouseEvent) => { + if (evt.detail === 2) { + onReset(); + } + }; + + const onPointerDown = (evt: React.PointerEvent) => { + if (!container.current || evt.button !== 0) { + return; + } + + evt.currentTarget.setPointerCapture(evt.pointerId); + + cache.current = { + containerBounds: container.current.getBoundingClientRect(), + initialPointerOffsets: distances(evt.currentTarget.getBoundingClientRect(), evt), + }; + }; + + const onPointerMove = (evt: React.PointerEvent) => { + if (!cache.current) { + return; + } + const { containerBounds, initialPointerOffsets } = cache.current; + + const rawDistances = distances(containerBounds, evt); + + // Keep the offset of the cursor within the gutter to avoid a + // small jump when first dragging. + const offsetDistances = subtractDistances(rawDistances, initialPointerOffsets); + + // Coalescing the callback because it only matters when we render. + if (pendingCallback.current) { + window.cancelAnimationFrame(pendingCallback.current); + } + pendingCallback.current = window.requestAnimationFrame(() => { + onMove(offsetDistances); + pendingCallback.current = null; + }); + }; + + const onPointerUp = () => { + cache.current = null; + }; + + return ( +
+ +
+ ); +}; + +const makeStyleFromPixels = (cssProps: [string, number | undefined][]): React.CSSProperties => { + const massagedProps = cssProps + .filter(([_k, v]) => v !== undefined) + .map(([k, v]) => [k, `${v}px`]); + return Object.fromEntries(massagedProps) as React.CSSProperties; +}; + +const ResizableArea: React.FC = () => { + 'use memo'; + + const somethingToShow = useAppSelector(selectors.getSomethingToShow); + const isFocused = useAppSelector(selectors.isOutputFocused); + const orientation = useAppSelector(selectors.orientation); + + const container = useRef(null); + + const [outputWidth, setOutputWidth] = useState(undefined); + const [outputHeight, setOutputHeight] = useState(undefined); + + const editorOutputGutterMove = (distances: Distances) => { + if (orientation === Orientation.Horizontal) { + setOutputHeight(distances.toBottom); + } else { + setOutputWidth(distances.toRight); + } + }; + + const editorOutputGutterReset = () => { + if (orientation === Orientation.Horizontal) { + setOutputHeight(undefined); + } else { + setOutputWidth(undefined); + } + }; + + const outputMode = (() => { + if (!somethingToShow) { + return 'none'; + } + if (!isFocused) { + return 'slim'; + } + return 'full'; + })(); + + const hideEditorOutputGutter = outputMode !== 'full'; + + const style = makeStyleFromPixels([ + ['--output-height', outputHeight], + ['--output-width', outputWidth], + ]); + + return ( +
+
+ +
+ + + + + + +
+ +
+
+
+ ); +}; + +const WebSocketStatus: React.FC = () => { + const enabled = useAppSelector(selectors.showGemSelector); + const status = useAppSelector(selectors.websocketStatusSelector); + + if (!enabled) { + return null; + } + + const style: React.CSSProperties = { + position: 'absolute', + left: '1em', + bottom: '1em', + zIndex: '1', + }; + + switch (status.state) { + case 'connected': + style.color = 'green'; + return
; + case 'disconnected': + style.color = 'grey'; + return
; + case 'error': + style.color = 'red'; + return ( +
+ ⬤ +
+ ); + } +}; + +const Playground: React.FC = () => { + const showNotifications = useAppSelector(selectors.anyNotificationsToShowSelector); + + return ( + <> +
+ +
+ +
+ {showNotifications && } + + ); +}; + +export default Playground; diff --git a/anneal/v1/playground/rust-anneal-playground/ui/frontend/PopButton.module.css b/anneal/v1/playground/rust-anneal-playground/ui/frontend/PopButton.module.css new file mode 100644 index 0000000000..cdea226ea3 --- /dev/null +++ b/anneal/v1/playground/rust-anneal-playground/ui/frontend/PopButton.module.css @@ -0,0 +1,35 @@ +$arrow-height: 10px; +$arrow-width: 20px; + +.container { + z-index: 10; + font-size: var(--secondary-font-size); + + & button:enabled { + cursor: pointer; + } +} + +.arrow { + fill: var(--background-color-high-contrast); +} + +.-content { + margin: $arrow-height; + box-shadow: + 5px 5px 20px -3px rgb(0 0 0 / 25%), + 0 0 5px -2px rgb(0 0 0 / 90%); + border-radius: var(--header-border-radius); + background: var(--background-color-high-contrast); + color: var(--font-color); +} + +.contentBottom { + composes: -content; + margin-top: 0; +} + +.contentTop { + composes: -content; + margin-bottom: 0; +} diff --git a/anneal/v1/playground/rust-anneal-playground/ui/frontend/PopButton.tsx b/anneal/v1/playground/rust-anneal-playground/ui/frontend/PopButton.tsx new file mode 100644 index 0000000000..28c70576c2 --- /dev/null +++ b/anneal/v1/playground/rust-anneal-playground/ui/frontend/PopButton.tsx @@ -0,0 +1,103 @@ +import { + FloatingArrow, + FloatingFocusManager, + Placement, + arrow, + autoUpdate, + flip, + offset, + shift, + useClick, + useDismiss, + useFloating, + useInteractions, + useRole, +} from '@floating-ui/react'; +import React, { useCallback, useRef, useState } from 'react'; +import { createPortal } from 'react-dom'; + +import * as styles from './PopButton.module.css'; + +export interface ButtonProps { + toggle: () => void; + ref: React.Ref; +} + +export interface MenuProps { + close: () => void; +} + +interface NewPopProps { + Button: React.ComponentType; + Menu: React.ComponentType; + menuContainer?: React.RefObject; +} + +const CONTAINER_STYLE: { [key in Placement]?: string } = { + top: styles.contentTop, + bottom: styles.contentBottom, +}; + +const PopButton: React.FC = ({ Button, Menu, menuContainer }) => { + const [isOpen, setIsOpen] = useState(false); + const toggle = useCallback(() => setIsOpen((v) => !v), []); + const close = useCallback(() => setIsOpen(false), []); + + const arrowRef = useRef(null); + + const { x, y, refs, strategy, context, placement } = useFloating({ + open: isOpen, + onOpenChange: setIsOpen, + // eslint-disable-next-line react-hooks/refs + middleware: [offset(10), flip(), shift(), arrow({ element: arrowRef })], + whileElementsMounted: autoUpdate, + }); + + const containerClass = CONTAINER_STYLE[placement] ?? ''; + + const click = useClick(context); + const dismiss = useDismiss(context); + const role = useRole(context); + + const { getReferenceProps, getFloatingProps } = useInteractions([click, dismiss, role]); + + const FloatingMenu = isOpen && ( + +
+ +
+ +
+
+
+ ); + + const Portal = menuContainer?.current && createPortal(FloatingMenu, menuContainer.current); + + return ( + <> + {/* eslint-disable-next-line react-hooks/refs */} + + +); + +export default SelectableMenuItem; diff --git a/anneal/v1/playground/rust-anneal-playground/ui/frontend/SimpleButtonMenuItem.module.css b/anneal/v1/playground/rust-anneal-playground/ui/frontend/SimpleButtonMenuItem.module.css new file mode 100644 index 0000000000..37d25d1bd8 --- /dev/null +++ b/anneal/v1/playground/rust-anneal-playground/ui/frontend/SimpleButtonMenuItem.module.css @@ -0,0 +1,7 @@ +.container { + composes: -menuItemFullButton from './shared.module.css'; + + &:hover { + color: var(--header-tint); + } +} diff --git a/anneal/v1/playground/rust-anneal-playground/ui/frontend/SimpleButtonMenuItem.tsx b/anneal/v1/playground/rust-anneal-playground/ui/frontend/SimpleButtonMenuItem.tsx new file mode 100644 index 0000000000..8b4db8ed52 --- /dev/null +++ b/anneal/v1/playground/rust-anneal-playground/ui/frontend/SimpleButtonMenuItem.tsx @@ -0,0 +1,25 @@ +import React, { type JSX } from 'react'; + +import MenuItem from './MenuItem'; + +import * as styles from './SimpleButtonMenuItem.module.css'; + +type Button = JSX.IntrinsicElements['button']; + +interface SimpleButtonMenuItemProps extends Button { + children: React.ReactNode; +} + +const SimpleButtonMenuItem: React.FC = ({ + name, + children, + ...props +}) => ( + + + +); + +export default SimpleButtonMenuItem; diff --git a/anneal/v1/playground/rust-anneal-playground/ui/frontend/Stdin.module.css b/anneal/v1/playground/rust-anneal-playground/ui/frontend/Stdin.module.css new file mode 100644 index 0000000000..3de82a7990 --- /dev/null +++ b/anneal/v1/playground/rust-anneal-playground/ui/frontend/Stdin.module.css @@ -0,0 +1,46 @@ +.form { + display: flex; + gap: 0.25em; + align-items: flex-end; +} + +.multiLine { + flex: 1; + display: grid; +} + +.text, +.sizer { + margin: 0; + padding: 0.25em; + grid-area: 1 / 1 / 2 /2; + max-height: 3.5lh; +} + +.text { + resize: none; +} + +.sizer { + visibility: hidden; + white-space: pre-wrap; +} + +.buttons { + font-size: var(--secondary-font-size); +} + +.menu { + list-style: none; + margin: 0; + padding: 0; +} + +.button { + composes: -menuItemFullButton from './shared.module.css'; + padding: 1em; + + &:hover { + color: var(--header-tint); + } +} diff --git a/anneal/v1/playground/rust-anneal-playground/ui/frontend/Stdin.tsx b/anneal/v1/playground/rust-anneal-playground/ui/frontend/Stdin.tsx new file mode 100644 index 0000000000..f85b9c5215 --- /dev/null +++ b/anneal/v1/playground/rust-anneal-playground/ui/frontend/Stdin.tsx @@ -0,0 +1,138 @@ +import React, { ChangeEvent, FormEvent, KeyboardEvent, useCallback, useRef, useState } from 'react'; + +import { Button, ButtonSet, IconButton } from './ButtonSet'; +import PopButton, { ButtonProps, MenuProps } from './PopButton'; +import { useAppDispatch, useAppSelector } from './hooks'; +import { + wsExecuteKillCurrent, + wsExecuteStdin, + wsExecuteStdinClose, +} from './reducers/output/execute'; +import { enableStdinSelector } from './selectors'; + +import * as styles from './Stdin.module.css'; + +const Stdin: React.FC = () => { + const dispatch = useAppDispatch(); + const disabled = !useAppSelector(enableStdinSelector); + + const [content, setContent] = useState(''); + + const form = useRef(null); + + const handleKeyDown = useCallback( + (e: KeyboardEvent) => { + if (e.key === 'Enter' && !e.shiftKey) { + e.preventDefault(); + form.current?.dispatchEvent(new Event('submit', { bubbles: true, cancelable: true })); + } + + if (e.key === 'c' && e.ctrlKey) { + dispatch(wsExecuteKillCurrent()); + } + + if (e.key === 'd' && e.ctrlKey && content.length === 0) { + dispatch(wsExecuteStdinClose()); + } + }, + [dispatch, form, content], + ); + + const handleChange = useCallback( + (e: ChangeEvent) => { + setContent(e.currentTarget.value); + }, + [setContent], + ); + + const handleSubmit = useCallback( + (e: FormEvent) => { + e.preventDefault(); + + dispatch(wsExecuteStdin(content + '\n')); + + setContent(''); + }, + [dispatch, content, setContent], + ); + + const menuContainer = useRef(null); + + return ( +
+
+
+ +

{content}

+
+ + + + + + +
+
+
+ ); +}; + +const MoreButton: React.FC = ({ toggle, ref }) => { + const disabled = !useAppSelector(enableStdinSelector); + + return ( + + ⋮ + + ); +}; + +const MoreMenu: React.FC = ({ close }) => { + const dispatch = useAppDispatch(); + + const stdinClose = useCallback(() => { + dispatch(wsExecuteStdinClose()); + close(); + }, [dispatch, close]); + + const kill = useCallback(() => { + dispatch(wsExecuteKillCurrent()); + close(); + }, [dispatch, close]); + + return ( +
    +
  • + +
  • +
  • + +
  • +
+ ); +}; + +export default Stdin; diff --git a/anneal/v1/playground/rust-anneal-playground/ui/frontend/ToolsMenu.tsx b/anneal/v1/playground/rust-anneal-playground/ui/frontend/ToolsMenu.tsx new file mode 100644 index 0000000000..cbd732f542 --- /dev/null +++ b/anneal/v1/playground/rust-anneal-playground/ui/frontend/ToolsMenu.tsx @@ -0,0 +1,85 @@ +import React, { useCallback } from 'react'; + +import ButtonMenuItem from './ButtonMenuItem'; +import MenuGroup from './MenuGroup'; +import MenuAside from './MenuAside'; + +import * as selectors from './selectors'; +import { useAppDispatch } from './hooks'; +import { performFormat } from './reducers/output/format'; +import { performClippy } from './reducers/output/clippy'; +import { performMiri } from './reducers/output/miri'; +import { performMacroExpansion } from './reducers/output/macroExpansion'; +import { useAppSelector } from './hooks'; + +interface ToolsMenuProps { + close: () => void; +} + +const ToolsMenu: React.FC = props => { + const rustfmtVersion = useAppSelector(selectors.rustfmtVersionText); + const rustfmtVersionDetails = useAppSelector(selectors.rustfmtVersionDetailsText); + const clippyVersionDetails = useAppSelector(selectors.clippyVersionDetailsText); + const clippyVersion = useAppSelector(selectors.clippyVersionText); + const miriVersionDetails = useAppSelector(selectors.miriVersionDetailsText); + const miriVersion = useAppSelector(selectors.miriVersionText); + const nightlyVersion = useAppSelector(selectors.nightlyVersionText); + const nightlyVersionDetails = useAppSelector(selectors.nightlyVersionDetailsText); + + const miriRunningTests = useAppSelector(selectors.runAsTest); + const miriText = miriRunningTests ? "these tests" : "this program"; + + const dispatch = useAppDispatch(); + const clippy = useCallback(() => { + dispatch(performClippy()); + props.close(); + }, [dispatch, props]); + const miri = useCallback(() => { + dispatch(performMiri()); + props.close(); + }, [dispatch, props]); + const format = useCallback(() => { + dispatch(performFormat()); + props.close(); + }, [dispatch, props]); + const expandMacros = useCallback(() => { + dispatch(performMacroExpansion()); + props.close(); + }, [dispatch, props]); + + return ( + + +
Format this code with Rustfmt.
+ {rustfmtVersion} ({rustfmtVersionDetails}) +
+ +
Catch common mistakes and improve the code using the Clippy linter.
+ {clippyVersion} ({clippyVersionDetails}) +
+ +
+ Execute {miriText} in the Miri interpreter to detect certain + cases of undefined behavior (like out-of-bounds memory access). +
+ {miriVersion} ({miriVersionDetails}) +
+ +
+ Expand macros in code using the nightly compiler. +
+ {nightlyVersion} ({nightlyVersionDetails}) +
+
+ ); +}; + +export default ToolsMenu; diff --git a/anneal/v1/playground/rust-anneal-playground/ui/frontend/actions.ts b/anneal/v1/playground/rust-anneal-playground/ui/frontend/actions.ts new file mode 100644 index 0000000000..daecddf8b5 --- /dev/null +++ b/anneal/v1/playground/rust-anneal-playground/ui/frontend/actions.ts @@ -0,0 +1,188 @@ +import { ThunkAction as ReduxThunkAction, UnknownAction } from '@reduxjs/toolkit'; + +import { State } from './reducers'; +import { addCrateType, editCode } from './reducers/code'; +import { + changeBacktrace, + changeChannel, + changeEdition, + changeMode, + changePrimaryAction, +} from './reducers/configuration'; +import { performCompileToAssemblyOnly } from './reducers/output/assembly'; +import { performCommonExecute } from './reducers/output/execute'; +import { performGistLoad } from './reducers/output/gist'; +import { performCompileToHirOnly } from './reducers/output/hir'; +import { performCompileToLlvmIrOnly } from './reducers/output/llvmIr'; +import { performCompileToMirOnly } from './reducers/output/mir'; +import { performCompileToWasmOnly } from './reducers/output/wasm'; +import { navigateToHelp, navigateToIndex } from './reducers/page'; +import { getCrateType, runAsTest, wasmLikelyToWork } from './selectors'; +import { + Backtrace, + Channel, + Edition, + Mode, + PrimaryAction, + PrimaryActionAuto, + PrimaryActionCore, + parseChannel, + parseEdition, + parseMode, +} from './types'; + +export type ThunkAction = ReduxThunkAction; + +export const reExecuteWithBacktrace = (): ThunkAction => (dispatch) => { + dispatch(changeBacktrace(Backtrace.Enabled)); + dispatch(performExecuteOnly()); +}; + +function performAutoOnly(): ThunkAction { + return function (dispatch, getState) { + const state = getState(); + const crateType = getCrateType(state); + const tests = runAsTest(state); + + return dispatch(performCommonExecute(crateType, tests)); + }; +} + +const performExecuteOnly = (): ThunkAction => performCommonExecute('bin', false); +const performCompileOnly = (): ThunkAction => performCommonExecute('lib', false); +const performTestOnly = (): ThunkAction => (dispatch, getState) => { + const state = getState(); + const crateType = getCrateType(state); + return dispatch(performCommonExecute(crateType, true)); +}; + +const performCompileToNightlyHirOnly = (): ThunkAction => (dispatch) => { + dispatch(changeChannel(Channel.Nightly)); + dispatch(performCompileToHirOnly()); +}; + +const performCompileToCdylibWasmOnly = (): ThunkAction => (dispatch, getState) => { + const state = getState(); + + if (!wasmLikelyToWork(state)) { + dispatch(addCrateType('cdylib')); + } + dispatch(performCompileToWasmOnly()); +}; +const performCargoAnnealOnly = (): ThunkAction => (dispatch) => { + dispatch(performCommonExecute('bin', false, 'anneal-verify')); +}; + +const PRIMARY_ACTIONS: { [index in PrimaryAction]: () => ThunkAction } = { + [PrimaryActionCore.Asm]: performCompileToAssemblyOnly, + [PrimaryActionCore.Compile]: performCompileOnly, + [PrimaryActionCore.Execute]: performExecuteOnly, + [PrimaryActionCore.Test]: performTestOnly, + [PrimaryActionAuto.Auto]: performAutoOnly, + [PrimaryActionCore.LlvmIr]: performCompileToLlvmIrOnly, + [PrimaryActionCore.Hir]: performCompileToHirOnly, + [PrimaryActionCore.Mir]: performCompileToMirOnly, + [PrimaryActionCore.Wasm]: performCompileToWasmOnly, + [PrimaryActionCore.Anneal]: performCargoAnnealOnly, +}; + +export const performPrimaryAction = (): ThunkAction => (dispatch, getState) => { + const state = getState(); + const primaryAction = PRIMARY_ACTIONS[state.configuration.primaryAction]; + dispatch(primaryAction()); +}; + +const performAndSwitchPrimaryAction = + (inner: () => ThunkAction, id: PrimaryAction) => (): ThunkAction => (dispatch) => { + dispatch(changePrimaryAction(id)); + dispatch(inner()); + }; + +export const performExecute = performAndSwitchPrimaryAction( + performExecuteOnly, + PrimaryActionCore.Execute, +); +export const performCompile = performAndSwitchPrimaryAction( + performCompileOnly, + PrimaryActionCore.Compile, +); +export const performTest = performAndSwitchPrimaryAction(performTestOnly, PrimaryActionCore.Test); +export const performCompileToAssembly = performAndSwitchPrimaryAction( + performCompileToAssemblyOnly, + PrimaryActionCore.Asm, +); +export const performCompileToLLVM = performAndSwitchPrimaryAction( + performCompileToLlvmIrOnly, + PrimaryActionCore.LlvmIr, +); +export const performCompileToMir = performAndSwitchPrimaryAction( + performCompileToMirOnly, + PrimaryActionCore.Mir, +); +export const performCompileToNightlyHir = performAndSwitchPrimaryAction( + performCompileToNightlyHirOnly, + PrimaryActionCore.Hir, +); +export const performCompileToWasm = performAndSwitchPrimaryAction( + performCompileToCdylibWasmOnly, + PrimaryActionCore.Wasm, +); +export const performCargoAnneal = performAndSwitchPrimaryAction( + performCargoAnnealOnly, + PrimaryActionCore.Anneal, +); + +export function indexPageLoad({ + code, + gist, + version, + mode: modeString, + edition: editionString, +}: { + code?: string; + gist?: string; + version?: string; + mode?: string; + edition?: string; +}): ThunkAction { + return function (dispatch) { + const channel = parseChannel(version) || Channel.Stable; + const mode = parseMode(modeString) || Mode.Debug; + let maybeEdition = parseEdition(editionString); + + dispatch(navigateToIndex()); + + if (code || gist) { + // We need to ensure that any links that predate the existence + // of editions will *forever* pick 2015. However, if we aren't + // loading code, then allow the edition to remain the default. + if (!maybeEdition) { + maybeEdition = Edition.Rust2015; + } + } + + const edition = maybeEdition || Edition.Rust2024; + + if (code) { + dispatch(editCode(code)); + } else if (gist) { + dispatch(performGistLoad({ id: gist, channel, mode, edition })); + } + + dispatch(changeChannel(channel)); + dispatch(changeMode(mode)); + dispatch(changeEdition(edition)); + }; +} + +export const helpPageLoad = navigateToHelp; + +export function showExample(code: string): ThunkAction { + return function (dispatch) { + dispatch(navigateToIndex()); + dispatch(changeChannel(Channel.Stable)); + dispatch(changeMode(Mode.Debug)); + dispatch(changeEdition(Edition.Rust2024)); + dispatch(editCode(code)); + }; +} diff --git a/anneal/v1/playground/rust-anneal-playground/ui/frontend/api.ts b/anneal/v1/playground/rust-anneal-playground/ui/frontend/api.ts new file mode 100644 index 0000000000..041e0743f3 --- /dev/null +++ b/anneal/v1/playground/rust-anneal-playground/ui/frontend/api.ts @@ -0,0 +1,55 @@ +import * as z from 'zod'; + +export const routes = { + compile: '/compile', + execute: '/execute', + format: '/format', + clippy: '/clippy', + miri: '/miri', + macroExpansion: '/macro-expansion', + meta: { + crates: '/meta/crates', + versions: '/meta/versions', + gistSave: '/meta/gist', + gistLoad: '/meta/gist/id', + }, +}; + +type FetchArg = Parameters[0]; + +export function jsonGet(url: FetchArg): Promise { + return fetchJson(url, { + method: 'get', + }); +} + +type ToJson = Parameters[0]; + +export function jsonPost(url: FetchArg, body: ToJson): Promise { + return fetchJson(url, { + method: 'post', + body: JSON.stringify(body), + }); +} + +const ErrorResponse = z.object({ + error: z.string(), +}); +type ErrorResponse = z.infer; + +async function fetchJson(url: FetchArg, args: RequestInit) { + const headers = new Headers(args.headers); + headers.set('Content-Type', 'application/json'); + + const response = await fetch(url, { ...args, headers }); + const body = await response.json(); + + if (response.ok) { + // HTTP 2xx + return body; + } else { + // HTTP 4xx, 5xx (e.g. malformed JSON request) + const error = await ErrorResponse.parseAsync(body); + throw new Error(`The server reported an error: ${error.error}`); + } +} diff --git a/anneal/v1/playground/rust-anneal-playground/ui/frontend/assets/integer32-logo-dark.svg b/anneal/v1/playground/rust-anneal-playground/ui/frontend/assets/integer32-logo-dark.svg new file mode 100644 index 0000000000..c5da26655b --- /dev/null +++ b/anneal/v1/playground/rust-anneal-playground/ui/frontend/assets/integer32-logo-dark.svg @@ -0,0 +1,2 @@ + +image/svg+xml diff --git a/anneal/v1/playground/rust-anneal-playground/ui/frontend/assets/integer32-logo-light.svg b/anneal/v1/playground/rust-anneal-playground/ui/frontend/assets/integer32-logo-light.svg new file mode 100644 index 0000000000..2647667e94 --- /dev/null +++ b/anneal/v1/playground/rust-anneal-playground/ui/frontend/assets/integer32-logo-light.svg @@ -0,0 +1,2 @@ + +image/svg+xml diff --git a/anneal/v1/playground/rust-anneal-playground/ui/frontend/compileActions.ts b/anneal/v1/playground/rust-anneal-playground/ui/frontend/compileActions.ts new file mode 100644 index 0000000000..f103966c2f --- /dev/null +++ b/anneal/v1/playground/rust-anneal-playground/ui/frontend/compileActions.ts @@ -0,0 +1,52 @@ +import { AsyncThunk, createAsyncThunk } from '@reduxjs/toolkit'; +import * as z from 'zod'; + +import { ThunkAction } from './actions'; +import { jsonPost, routes } from './api'; +import { compileRequestPayloadSelector } from './selectors'; + +interface CompileRequestBody { + channel: string; + mode: string; + crateType: string; + tests: boolean; // Used? + code: string; + edition: string; + backtrace: boolean; // Used? + target: string; + assemblyFlavor: string; + demangleAssembly: string; + processAssembly: string; +} + +const CompileResponseBody = z.object({ + code: z.string(), + stdout: z.string(), + stderr: z.string(), +}); +type CompileResponseBody = z.infer; + +interface Props { + sliceName: string; + target: string; +} + +interface CompileActions { + action: AsyncThunk; + performCompile: () => ThunkAction; +} + +export const makeCompileActions = ({ sliceName, target }: Props): CompileActions => { + const action = createAsyncThunk(sliceName, async (payload: CompileRequestBody) => { + const d = await jsonPost(routes.compile, payload); + return CompileResponseBody.parseAsync(d); + }); + + const performCompile = (): ThunkAction => (dispatch, getState) => { + const state = getState(); + const body = compileRequestPayloadSelector(state, { target }); + dispatch(action(body)); + }; + + return { action, performCompile }; +}; diff --git a/anneal/v1/playground/rust-anneal-playground/ui/frontend/configureStore.ts b/anneal/v1/playground/rust-anneal-playground/ui/frontend/configureStore.ts new file mode 100644 index 0000000000..d1a63f03df --- /dev/null +++ b/anneal/v1/playground/rust-anneal-playground/ui/frontend/configureStore.ts @@ -0,0 +1,84 @@ +import { configureStore as reduxConfigureStore } from '@reduxjs/toolkit'; +import { produce } from 'immer'; +import { merge } from 'lodash-es'; + +import initializeLocalStorage from './local_storage'; +import { observer } from './observer'; +import reducer from './reducers'; +import initializeSessionStorage from './session_storage'; +import { websocketMiddleware } from './websocketMiddleware'; + +const editorDarkThemes = { + configuration: { + ace: { + theme: 'github_dark', + }, + monaco: { + theme: 'vs-dark', + }, + }, +}; + +export default function configureStore(window: Window) { + const baseUrl = new URL('/', window.location.href).href; + const websocket = websocketMiddleware(window); + + const prefersDarkTheme = window.matchMedia('(prefers-color-scheme: dark)').matches; + const initialThemes = prefersDarkTheme ? editorDarkThemes : {}; + + const initialGlobalState = { + globalConfiguration: { + baseUrl, + }, + }; + const initialAppState = reducer(undefined, { type: 'initializeForStore' }); + + const localStorage = initializeLocalStorage(); + const sessionStorage = initializeSessionStorage(); + + const preloadedState = produce(initialAppState, (initialAppState) => + merge( + initialAppState, + initialGlobalState, + initialThemes, + localStorage.initialState, + sessionStorage.initialState, + ), + ); + + const store = reduxConfigureStore({ + reducer, + preloadedState, + middleware: (getDefaultMiddleware) => + getDefaultMiddleware().concat(websocket).prepend(observer.middleware), + }); + + store.subscribe(() => { + const state = store.getState(); + + // Some automated tests run fast enough that the following interleaving is possible: + // + // 1. RSpec test finishes, local/session storage cleared + // 2. WebSocket connects, the state updates, and the local/session storage is saved + // 3. Subsequent RSpec test starts and local/session storage has been preserved + // + // We allow the tests to stop saving to sidestep that. + if (state.globalConfiguration.syncChangesToStorage) { + localStorage.saveChanges(state); + sessionStorage.saveChanges(state); + } + + if (state.client.resetEverything) { + localStorage.clear(); + sessionStorage.clear(); + + // This removes any query parameters and triggers all the + // initialization + window.location = state.globalConfiguration.baseUrl; + } + }); + + return store; +} + +export type AppDispatch = ReturnType['dispatch']; diff --git a/anneal/v1/playground/rust-anneal-playground/ui/frontend/declarations.d.ts b/anneal/v1/playground/rust-anneal-playground/ui/frontend/declarations.d.ts new file mode 100644 index 0000000000..29260bd32a --- /dev/null +++ b/anneal/v1/playground/rust-anneal-playground/ui/frontend/declarations.d.ts @@ -0,0 +1,34 @@ +declare module '*.module.css' { + const classes: { [key: string]: string }; + export = classes; +} + +declare module 'normalize.css/normalize.css' { + const content: string; + export default content; +} + +declare module 'prismjs/themes/*.css' { + const content: string; + export default content; +} + +declare module '*prismjs-overrides.css' { + const content: string; + export default content; +} + +declare module '*.svg' { + const content: string; + export default content; +} + +declare const ACE_KEYBINDINGS: string[]; +declare const ACE_THEMES: string[]; + +interface Window { + rustPlayground: { + setCode(code: string): void; + disableSyncChangesToStorage(): void; + }; +} diff --git a/anneal/v1/playground/rust-anneal-playground/ui/frontend/editor/AceEditor.tsx b/anneal/v1/playground/rust-anneal-playground/ui/frontend/editor/AceEditor.tsx new file mode 100644 index 0000000000..4bd311c02d --- /dev/null +++ b/anneal/v1/playground/rust-anneal-playground/ui/frontend/editor/AceEditor.tsx @@ -0,0 +1,62 @@ +import React, { Suspense } from 'react'; +import { suspend } from 'suspend-react'; + +import { useAppSelector } from '../hooks'; +import { + aceKeybinding, + acePairCharacters, + aceTheme, + offerCrateAutocompleteOnUse, +} from '../selectors'; +import { CommonEditorProps } from '../types'; + +const AceEditorDependencies: React.FC<{ + keybinding: string; + theme: string; +}> = ({ keybinding, theme }) => { + suspend( + async (keybinding, theme) => { + const { importKeybinding, importTheme } = await import('./AceEditorCore'); + await Promise.allSettled([importKeybinding(keybinding), importTheme(theme)]); + }, + [keybinding, theme, 'AceEditorDependencies'], + ); + + return <>; +}; + +const AceEditorLazy = React.lazy(() => import('./AceEditorCore')); + +// The ACE editor weighs in at ~250K. Adding all of the themes and the +// (surprisingly chunky) keybindings, it's not that far off from 500K! +// +// To give better initial load performance, we split the editor into a +// separate chunk. As you usually only want one of each theme and +// keybinding, they can also be split, reducing the total size +// transferred. +// +// This also has some benefit if you choose to use the simple editor, +// as ACE should never be loaded. +// +// Themes and keybindings can be changed at runtime. +const AceEditorAsync: React.FC = (props) => { + const autocompleteOnUse = useAppSelector(offerCrateAutocompleteOnUse); + const keybinding = useAppSelector(aceKeybinding); + const pairCharacters = useAppSelector(acePairCharacters); + const theme = useAppSelector(aceTheme); + + return ( + + + + + ); +}; + +export default AceEditorAsync; diff --git a/anneal/v1/playground/rust-anneal-playground/ui/frontend/editor/AceEditorCore.tsx b/anneal/v1/playground/rust-anneal-playground/ui/frontend/editor/AceEditorCore.tsx new file mode 100644 index 0000000000..6db042f6d4 --- /dev/null +++ b/anneal/v1/playground/rust-anneal-playground/ui/frontend/editor/AceEditorCore.tsx @@ -0,0 +1,301 @@ +// This file will be a separate bundle and loaded async. + +import ace, { Ace } from 'ace-builds'; +import 'ace-builds/src-noconflict/ext-language_tools'; +import 'ace-builds/src-noconflict/ext-searchbox'; +import 'ace-builds/src-noconflict/mode-rust'; +import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'; + +import { CommonEditorProps, Crate, PairCharacters, Position } from '../types'; +import { useLatest } from './useLatest'; + +import * as styles from './Editor.module.css'; + +// The keybinding and theme need to be loaded **after** the Ace +// library because they use the global value it provides. Loading this +// file ensures that the library is available. +export const importKeybinding = (name: string) => import( + /* webpackChunkName: "ace-[request]" */ + `ace-builds/src-noconflict/keybinding-${name}` +); + +export const importTheme = (name: string) => import( + /* webpackChunkName: "ace-[request]" */ + `ace-builds/src-noconflict/theme-${name}` +); + +const themeToAceId = (theme: string) => `ace/theme/${theme}`; + +interface CodeMirrorEditor { + ace: Ace.Editor; +} + +interface VimKeybindings { + CodeMirror: { + Vim: { + defineEx: (cmd: string, key: string, cb: (cm: CodeMirrorEditor) => void) => void; + }; + }; +} + +const displayExternCrateAutocomplete = (editor: Ace.Editor, autocompleteOnUse: boolean) => { + const { session } = editor; + const pos = editor.getCursorPosition(); + const line = session.getLine(pos.row); + const precedingText = line.slice(0, pos.column); + + return !!precedingText.match(/^\s*extern\s+crate\s*\w*$/) || + (autocompleteOnUse && !!precedingText.match(/^\s*use\s+(?!crate|self|super)\w*$/)); +}; + +const buildCrateAutocompleter = (autocompleteOnUse: boolean, crates: Crate[]): Ace.Completer => ({ + getCompletions: (editor, _session, _pos, _prefix, callback) => { + let suggestions: Ace.Completion[] = []; + + if (displayExternCrateAutocomplete(editor, autocompleteOnUse)) { + const len = crates.length; + + suggestions = crates.map(({ name, version, id }, i) => ({ + caption: `${name} (${version})`, + value: `${id}; // ${version}`, + meta: 'crate', + score: len - i, // Force alphabetic order before anything is typed + })); + } + + callback(null, suggestions); + }, +}); + +function useRafDebouncedFunction(fn: (...args: A) => void, onCall?: (...args: A) => void) { + const timeout = useRef(undefined); + + return useCallback((...args: A): void => { + if (timeout.current) { + window.cancelAnimationFrame(timeout.current); + } + + timeout.current = window.requestAnimationFrame(() => { + fn(...args); + if (onCall) { onCall(...args); } + }); + }, [fn, onCall, timeout]); +} + +interface AceEditorProps extends CommonEditorProps { + autocompleteOnUse: boolean; + keybinding: string; + theme: string; + pairCharacters: PairCharacters; +} + +// Run an effect when the editor or prop changes +function useEditorProp(editor: Ace.Editor | null, prop: T, whenPresent: (editor: Ace.Editor, prop: T) => void) { + useEffect(() => { + if (editor) { + return whenPresent(editor, prop); + } + }, [editor, prop, whenPresent]); +} + +const AceEditor: React.FC = props => { + const [editor, setEditor] = useState(null); + const latestCodeRef = useLatest(props.code); + const latestThemeRef = useLatest(props.theme); + + const child = useCallback((node: HTMLDivElement | null) => { + if (!node) { return; } + + const editor = ace.edit(node, { + mode: 'ace/mode/rust', + value: latestCodeRef.current, + theme: themeToAceId(latestThemeRef.current), + }); + setEditor(editor); + + // The default keybinding of control/command-l interferes with + // the browser's "edit the location" keycommand which I think + // is way more common. + const gotoCommand = editor.commands.byName.gotoline; + gotoCommand.bindKey = { + win: 'Ctrl-Shift-L', + mac: 'Command-Shift-L', + }; + editor.commands.addCommand(gotoCommand); + + editor.setOptions({ + enableBasicAutocompletion: true, + fixedWidthGutter: true, + }); + + return () => { + editor.destroy(); + setEditor(null); + node.textContent = ''; + }; + }, [latestCodeRef, latestThemeRef]); + + useEditorProp(editor, props.execute, useCallback((editor, execute) => { + // TODO: Remove command? + editor.commands.addCommand({ + name: 'executeCode', + bindKey: { + win: 'Ctrl-Enter', + mac: 'Ctrl-Enter|Command-Enter', + }, + exec: execute, + readOnly: true, + }); + }, [])); + + const autocompleteProps = useMemo(() => ({ + autocompleteOnUse: props.autocompleteOnUse, + crates: props.crates, + }), [props.autocompleteOnUse, props.crates]); + + // When the user types either `extern crate ` or `use `, automatically + // open the autocomplete. This should help people understand that + // there are crates available. + useEditorProp(editor, autocompleteProps, useCallback((editor, { autocompleteOnUse, crates }) => { + editor.commands.on('afterExec', ({ editor, command }) => { + if (!(command.name === 'backspace' || command.name === 'insertstring')) { + return; + } + + if (displayExternCrateAutocomplete(editor, autocompleteOnUse)) { + editor.execCommand('startAutocomplete'); + } + }); + + editor.completers = [buildCrateAutocompleter(autocompleteOnUse, crates)]; + }, [])); + + // Both Ace and the playground want to be the One True Owner of + // the textual content. This can cause issues because the Redux + // store will attempt to change Ace in response to changes + // *originating* from Ace. In addition, Ace can generate multiple + // `change` events in response to what looks like a single user + // action. This includes: + // + // - Auto-indenting after pressing return + // - Invoking undo + // - Multi-cursor editing + // + // To avoid issues... + // + // 1. When we are setting the Ace value based on the prop, we + // prevent generating outgoing events. This requires that the + // events are synchronously generated during the call to + // `setValue` + // + // 2. We throttle outgoing events to once per animation frame, + // only sending the most recent update. This reduces the updates + // to Redux and thus the number of updates to our props. While + // this covers a lot of the problems, it does not handle rapid + // typing (a.k.a. banging on the keyboard). + // + // 3. When we do generate an outgoing event, we log it. If we see + // that same event come back next via the property, we ignore it. + // + // 4. When all else fails, we ignore the prop if the value to set is + // what Ace already has. + const doingSetProp = useRef(false); + const previouslyNotified = useRef([]); + const onEditCodeDebounced = useRafDebouncedFunction( + props.onEditCode, + useCallback((code: string) => previouslyNotified.current.push(code), [previouslyNotified]), + ); + + useEditorProp(editor, onEditCodeDebounced, useCallback((editor, onEditCode) => { + const listener = () => { + if (!doingSetProp.current) { + onEditCode(editor.getValue()); + } + }; + + editor.on('change', listener); + + return () => { + editor.off('change', listener); + }; + }, [])); + + useEditorProp(editor, props.code, useCallback((editor, code) => { + // Is this prop update the result of our own `change` event? + const last = previouslyNotified.current.shift(); + if (code === last) { + return; + } + + // It wasn't; discard any remaining self-generated events and resync + previouslyNotified.current = []; + + // Avoid spuriously resetting the text + if (editor.getValue() === code) { + return; + } + + doingSetProp.current = true; + const currentSelection = editor.selection.toJSON(); + editor.setValue(code); + editor.selection.fromJSON(currentSelection); + doingSetProp.current = false; + }, [])); + + useEditorProp(editor, props.theme, useCallback((editor, theme) => { + editor.setTheme(themeToAceId(theme)); + }, [])); + + const keybindingProps = useMemo(() => ({ + keybinding: props.keybinding, + }), [props.keybinding]); + + useEditorProp(editor, keybindingProps, useCallback((editor, { keybinding }) => { + const handler = keybinding === 'ace' ? null : `ace/keyboard/${keybinding}`; + editor.setOption('keyboardHandler', handler); + + if (keybinding === 'vim') { + const { CodeMirror: { Vim } }: VimKeybindings = ace.require('ace/keyboard/vim'); + Vim.defineEx('write', 'w', (cm) => { + cm.ace.execCommand('executeCode'); + }); + } + }, [])); + + useEditorProp(editor, props.pairCharacters, useCallback((editor, pairCharacters) => { + editor.setBehavioursEnabled(pairCharacters !== PairCharacters.Disabled); + }, [])); + + useEditorProp(editor, props.position, useCallback((editor, { line, column }) => { + // Columns are zero-indexed in ACE + editor.gotoLine(line, column - 1, false); + editor.focus(); + }, [])); + + const selectionProps = useMemo(() => ({ + selection: props.selection, + }), [props.selection]); + + useEditorProp(editor, selectionProps, useCallback((editor, { selection }) => { + if (selection.start && selection.end) { + // Columns are zero-indexed in ACE, but why does the selection + // API and `gotoLine` treat the row/line differently? + const toPoint = ({ line, column }: Position) => ({ row: line - 1, column: column - 1 }); + + const start = toPoint(selection.start); + const end = toPoint(selection.end); + + const range = new ace.Range(start.row, start.column, end.row, end.column); + + editor.selection.setRange(range); + editor.renderer.scrollCursorIntoView(start); + editor.focus(); + } + }, [])); + + return ( +
+ ); +}; + +export default AceEditor; diff --git a/anneal/v1/playground/rust-anneal-playground/ui/frontend/editor/Editor.module.css b/anneal/v1/playground/rust-anneal-playground/ui/frontend/editor/Editor.module.css new file mode 100644 index 0000000000..67969aa417 --- /dev/null +++ b/anneal/v1/playground/rust-anneal-playground/ui/frontend/editor/Editor.module.css @@ -0,0 +1,24 @@ +.container { + composes: -autoSize from '../shared.module.css'; + position: relative; +} + +.-advanced { + composes: -bodyMonospace -autoSize from '../shared.module.css'; + position: absolute; +} + +.ace { + composes: -advanced; +} + +.monaco { + composes: -advanced; +} + +.simple { + composes: -advanced; + border: none; + color: inherit; + background-color: inherit; +} diff --git a/anneal/v1/playground/rust-anneal-playground/ui/frontend/editor/Editor.tsx b/anneal/v1/playground/rust-anneal-playground/ui/frontend/editor/Editor.tsx new file mode 100644 index 0000000000..ee13d29043 --- /dev/null +++ b/anneal/v1/playground/rust-anneal-playground/ui/frontend/editor/Editor.tsx @@ -0,0 +1,47 @@ +import React from 'react'; + +import * as actions from '../actions'; +import { useAppDispatch, useAppSelector } from '../hooks'; +import { editCode } from '../reducers/code'; +import { codeSelector, positionSelector, selectionSelector } from '../selectors'; +import { Editor as EditorType } from '../types'; +import AceEditor from './AceEditor'; +import MonacoEditor from './MonacoEditor'; +import SimpleEditor from './SimpleEditor'; + +import * as styles from './Editor.module.css'; + +const editorMap = { + [EditorType.Simple]: SimpleEditor, + [EditorType.Ace]: AceEditor, + [EditorType.Monaco]: MonacoEditor, +}; + +const Editor: React.FC = () => { + 'use memo'; + + const code = useAppSelector(codeSelector); + const editor = useAppSelector((state) => state.configuration.editor); + const position = useAppSelector(positionSelector); + const selection = useAppSelector(selectionSelector); + const crates = useAppSelector((state) => state.crates); + + const dispatch = useAppDispatch(); + + const SelectedEditor = editorMap[editor]; + + return ( +
+ dispatch(editCode(c))} + execute={() => dispatch(actions.performPrimaryAction())} + /> +
+ ); +}; + +export default Editor; diff --git a/anneal/v1/playground/rust-anneal-playground/ui/frontend/editor/MonacoEditor.tsx b/anneal/v1/playground/rust-anneal-playground/ui/frontend/editor/MonacoEditor.tsx new file mode 100644 index 0000000000..e714fabf29 --- /dev/null +++ b/anneal/v1/playground/rust-anneal-playground/ui/frontend/editor/MonacoEditor.tsx @@ -0,0 +1,13 @@ +import React, { Suspense } from 'react'; + +import { CommonEditorProps } from '../types'; + +const MonacoEditorLazy = React.lazy(() => import('./MonacoEditorCore')); + +const MonacoEditor: React.FC = props => ( + + + +) + +export default MonacoEditor; diff --git a/anneal/v1/playground/rust-anneal-playground/ui/frontend/editor/MonacoEditorCore.tsx b/anneal/v1/playground/rust-anneal-playground/ui/frontend/editor/MonacoEditorCore.tsx new file mode 100644 index 0000000000..3e884e9adc --- /dev/null +++ b/anneal/v1/playground/rust-anneal-playground/ui/frontend/editor/MonacoEditorCore.tsx @@ -0,0 +1,210 @@ +import * as monaco from 'monaco-editor'; +import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'; + +import { useAppSelector } from '../hooks'; +import { offerCrateAutocompleteOnUse } from '../selectors'; +import { CommonEditorProps } from '../types'; +import { themeVsDarkPlus } from './rust_monaco_def'; +import { useLatest } from './useLatest'; + +import * as styles from './Editor.module.css'; + +async function remeasureFontWhenReady(fonts: FontFaceSet, font: string) { + while (!fonts.check(font)) { + await fonts.ready; + } + + monaco.editor.remeasureFonts(); +} + +function useEditorProp( + 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 model = editor.getModel(); + if (!model) { + return; + } + + return whenPresent(editor, model, prop); + }, [editor, prop, whenPresent]); +} + +const MonacoEditorCore: React.FC = (props) => { + const [editor, setEditor] = useState(null); + const theme = useAppSelector((s) => s.configuration.monaco.theme); + const completionProvider = useRef(null); + const autocompleteOnUse = useAppSelector(offerCrateAutocompleteOnUse); + + const latestCodeRef = useLatest(props.code); + const latestThemeRef = useLatest(theme); + + // 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: latestCodeRef.current, + theme: latestThemeRef.current, + fontSize: parseInt(nodeStyle.fontSize, 10), + fontFamily: nodeStyle.fontFamily, + automaticLayout: true, + 'semanticHighlighting.enabled': true, + autoClosingOvertype: 'always', + }); + setEditor(editor); + + remeasureFontWhenReady(document.fonts, nodeStyle.font); + + editor.focus(); + }, + [latestCodeRef, latestThemeRef], + ); + + useEditorProp( + editor, + props.onEditCode, + useCallback((editor, _model, onEditCode) => { + editor.onDidChangeModelContent(() => { + // Must get the current model, not whatever model was active + // when we set up the callback. + const value = editor.getModel()?.getValue(); + if (value) { + onEditCode(value); + } + }); + }, []), + ); + + useEditorProp( + editor, + props.execute, + useCallback((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(); + }); + }, []), + ); + + useEditorProp( + editor, + props.code, + useCallback((editor, model, code) => { + // Short-circuit if nothing interesting to change. + if (code === model.getValue()) { + return; + } + + editor.executeEdits('redux', [ + { + text: code, + range: model.getFullModelRange(), + }, + ]); + }, []), + ); + + useEditorProp( + editor, + theme, + useCallback((editor, _model, theme) => { + editor.updateOptions({ theme }); + }, []), + ); + + const autocompleteProps = useMemo( + () => ({ autocompleteOnUse, crates: props.crates }), + [autocompleteOnUse, props.crates], + ); + + useEditorProp( + editor, + autocompleteProps, + useCallback((_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(); + }; + }, []), + ); + + useEditorProp( + editor, + props.position, + useCallback((editor, _model, { line, column }) => { + editor.setPosition({ lineNumber: line, column }); + editor.focus(); + }, []), + ); + + return
; +}; + +export default MonacoEditorCore; diff --git a/anneal/v1/playground/rust-anneal-playground/ui/frontend/editor/SimpleEditor.tsx b/anneal/v1/playground/rust-anneal-playground/ui/frontend/editor/SimpleEditor.tsx new file mode 100644 index 0000000000..3e82cbe76f --- /dev/null +++ b/anneal/v1/playground/rust-anneal-playground/ui/frontend/editor/SimpleEditor.tsx @@ -0,0 +1,116 @@ +import React from 'react'; + +import { CommonEditorProps, Position, Selection } from '../types'; + +import * as styles from './Editor.module.css'; + +class CodeByteOffsets { + readonly code: string; + readonly lines: string[]; + + constructor(code: string) { + this.code = code; + this.lines = code.split('\n'); + } + + public lineToOffsets(line: number) { + const precedingBytes = this.bytesBeforeLine(line); + const nextPrecedingBytes = this.bytesBeforeLine(line + 1); + + return [precedingBytes, nextPrecedingBytes]; + } + + public rangeToOffsets(start: Position, end: Position) { + const startBytes = this.positionToBytes(start); + const endBytes = this.positionToBytes(end); + return [startBytes, endBytes]; + } + + private positionToBytes(position: Position) { + // Subtract one as this logic is zero-based and the columns are one-based + return this.bytesBeforeLine(position.line) + position.column - 1; + } + + private bytesBeforeLine(line: number) { + // Subtract one as this logic is zero-based and the lines are one-based + line -= 1; + + const precedingLines = this.lines.slice(0, line); + + // Add one to account for the newline we split on and removed + return precedingLines.map((l) => l.length + 1).reduce((a, b) => a + b, 0); + } +} + +class SimpleEditor extends React.PureComponent { + private _editor: HTMLTextAreaElement | null = null; + + private onChange: React.ChangeEventHandler = (e) => + this.props.onEditCode(e.target.value); + private trackEditor: React.RefCallback = (component) => { + this._editor = component; + }; + private onKeyDown: React.KeyboardEventHandler = (e) => { + if (e.key === 'Enter' && (e.ctrlKey || e.metaKey)) { + this.props.execute(); + } + }; + + public render() { + return ( +