From 19cd11118cde7160cd447c81bc965007c0437479 Mon Sep 17 00:00:00 2001 From: Kristóf Marussy Date: Tue, 16 Aug 2022 21:14:50 +0200 Subject: refactor(frondend): improve editor store and theme Also bumps frontend dependencies. --- subprojects/frontend/package.json | 15 +- subprojects/frontend/src/RootStore.tsx | 6 +- subprojects/frontend/src/TopBar.tsx | 4 +- subprojects/frontend/src/editor/EditorArea.tsx | 138 +-------- subprojects/frontend/src/editor/EditorButtons.tsx | 10 +- subprojects/frontend/src/editor/EditorParent.ts | 228 -------------- subprojects/frontend/src/editor/EditorStore.ts | 320 ++++++++----------- subprojects/frontend/src/editor/EditorTheme.ts | 342 +++++++++++++++++++++ subprojects/frontend/src/editor/GenerateButton.tsx | 2 +- subprojects/frontend/src/editor/PanelStore.ts | 90 ++++++ .../frontend/src/editor/createEditorState.ts | 121 ++++++++ .../frontend/src/editor/editorClassNames.ts | 10 + .../src/language/problemLanguageSupport.ts | 8 +- subprojects/frontend/src/theme/EditorTheme.ts | 7 - subprojects/frontend/src/theme/ThemeProvider.tsx | 85 +++-- subprojects/frontend/src/theme/ThemeStore.ts | 26 +- subprojects/frontend/tsconfig.base.json | 1 + yarn.lock | 204 ++++++------ 18 files changed, 894 insertions(+), 723 deletions(-) delete mode 100644 subprojects/frontend/src/editor/EditorParent.ts create mode 100644 subprojects/frontend/src/editor/EditorTheme.ts create mode 100644 subprojects/frontend/src/editor/PanelStore.ts create mode 100644 subprojects/frontend/src/editor/createEditorState.ts create mode 100644 subprojects/frontend/src/editor/editorClassNames.ts delete mode 100644 subprojects/frontend/src/theme/EditorTheme.ts diff --git a/subprojects/frontend/package.json b/subprojects/frontend/package.json index d438c104..69ff74c6 100644 --- a/subprojects/frontend/package.json +++ b/subprojects/frontend/package.json @@ -26,7 +26,7 @@ "@codemirror/commands": "^6.0.1", "@codemirror/language": "^6.2.1", "@codemirror/lint": "^6.0.0", - "@codemirror/search": "^6.0.1", + "@codemirror/search": "^6.1.0", "@codemirror/state": "^6.1.1", "@codemirror/view": "^6.2.0", "@emotion/react": "^11.10.0", @@ -36,9 +36,10 @@ "@fontsource/roboto": "^4.5.8", "@lezer/common": "^1.0.0", "@lezer/highlight": "^1.0.0", - "@lezer/lr": "^1.2.2", + "@lezer/lr": "^1.2.3", + "@material-icons/svg": "^1.0.32", "@mui/icons-material": "5.8.4", - "@mui/material": "5.10.0", + "@mui/material": "5.10.1", "ansi-styles": "^6.1.0", "escape-string-regexp": "^5.0.0", "loglevel": "^1.8.0", @@ -53,12 +54,12 @@ "devDependencies": { "@lezer/generator": "^1.1.1", "@types/eslint": "^8.4.5", - "@types/node": "^18.7.4", + "@types/node": "^18.7.6", "@types/prettier": "^2.7.0", "@types/react": "^18.0.17", "@types/react-dom": "^18.0.6", - "@typescript-eslint/eslint-plugin": "^5.33.0", - "@typescript-eslint/parser": "^5.33.0", + "@typescript-eslint/eslint-plugin": "^5.33.1", + "@typescript-eslint/parser": "^5.33.1", "@vitejs/plugin-react": "^2.0.1", "cross-env": "^7.0.3", "eslint": "^8.22.0", @@ -73,7 +74,7 @@ "eslint-plugin-react-hooks": "^4.6.0", "prettier": "^2.7.1", "typescript": "~4.7.4", - "vite": "^3.0.7", + "vite": "^3.0.8", "vite-plugin-inject-preload": "^1.0.1" } } diff --git a/subprojects/frontend/src/RootStore.tsx b/subprojects/frontend/src/RootStore.tsx index a7406d7b..4a267b0e 100644 --- a/subprojects/frontend/src/RootStore.tsx +++ b/subprojects/frontend/src/RootStore.tsx @@ -4,13 +4,13 @@ import EditorStore from './editor/EditorStore'; import ThemeStore from './theme/ThemeStore'; export default class RootStore { - editorStore; + readonly editorStore: EditorStore; - themeStore; + readonly themeStore: ThemeStore; constructor(initialValue: string) { + this.editorStore = new EditorStore(initialValue); this.themeStore = new ThemeStore(); - this.editorStore = new EditorStore(initialValue, this.themeStore); } } diff --git a/subprojects/frontend/src/TopBar.tsx b/subprojects/frontend/src/TopBar.tsx index af571a1e..5ad80d40 100644 --- a/subprojects/frontend/src/TopBar.tsx +++ b/subprojects/frontend/src/TopBar.tsx @@ -12,8 +12,8 @@ export default function TopBar(): JSX.Element { elevation={0} color="transparent" sx={(theme) => ({ - background: theme.palette.highlight.activeLine, - borderBottom: `1px solid ${theme.palette.divider2}`, + background: theme.palette.outer.background, + borderBottom: `1px solid ${theme.palette.outer.border}`, })} > diff --git a/subprojects/frontend/src/editor/EditorArea.tsx b/subprojects/frontend/src/editor/EditorArea.tsx index d4305610..e5712461 100644 --- a/subprojects/frontend/src/editor/EditorArea.tsx +++ b/subprojects/frontend/src/editor/EditorArea.tsx @@ -1,139 +1,31 @@ -import { closeLintPanel, openLintPanel } from '@codemirror/lint'; -import { closeSearchPanel, openSearchPanel } from '@codemirror/search'; -import { type Command, EditorView } from '@codemirror/view'; +import { useTheme } from '@mui/material/styles'; import { observer } from 'mobx-react-lite'; -import React, { useCallback, useEffect, useRef, useState } from 'react'; +import React, { useCallback, useEffect } from 'react'; import { useRootStore } from '../RootStore'; -import getLogger from '../utils/getLogger'; -import EditorParent from './EditorParent'; - -const log = getLogger('editor.EditorArea'); - -function usePanel( - panelId: string, - stateToSet: boolean, - editorView: EditorView | null, - openCommand: Command, - closeCommand: Command, - closeCallback: () => void, -) { - const [cachedViewState, setCachedViewState] = useState(false); - useEffect(() => { - if (editorView === null || cachedViewState === stateToSet) { - return; - } - if (stateToSet) { - openCommand(editorView); - const buttonQuery = `.cm-${panelId}.cm-panel button[name="close"]`; - const closeButton = editorView.dom.querySelector(buttonQuery); - if (closeButton) { - log.debug('Addig close button callback to', panelId, 'panel'); - // We must remove the event listener added by CodeMirror from the button - // that dispatches a transaction without going through `EditorStorre`. - // Cloning a DOM node removes event listeners, - // see https://stackoverflow.com/a/9251864 - const closeButtonWithoutListeners = closeButton.cloneNode(true); - closeButtonWithoutListeners.addEventListener('click', (event) => { - closeCallback(); - event.preventDefault(); - }); - closeButton.replaceWith(closeButtonWithoutListeners); - } else { - log.error('Opened', panelId, 'panel has no close button'); - } - } else { - closeCommand(editorView); - } - setCachedViewState(stateToSet); - }, [ - stateToSet, - editorView, - cachedViewState, - panelId, - openCommand, - closeCommand, - closeCallback, - ]); - return setCachedViewState; -} - -function fixCodeMirrorAccessibility(editorView: EditorView) { - // Reported by Lighthouse 8.3.0. - const { contentDOM } = editorView; - contentDOM.removeAttribute('aria-expanded'); - contentDOM.setAttribute('aria-label', 'Code editor'); -} +import EditorTheme from './EditorTheme'; function EditorArea(): JSX.Element { const { editorStore } = useRootStore(); - const editorParentRef = useRef(null); - const [editorViewState, setEditorViewState] = useState( - null, - ); + const { + palette: { mode: paletteMode }, + } = useTheme(); - const setSearchPanelOpen = usePanel( - 'search', - editorStore.showSearchPanel, - editorViewState, - openSearchPanel, - closeSearchPanel, - useCallback(() => editorStore.setSearchPanelOpen(false), [editorStore]), + useEffect( + () => editorStore.setDarkMode(paletteMode === 'dark'), + [editorStore, paletteMode], ); - const setLintPanelOpen = usePanel( - 'panel-lint', - editorStore.showLintPanel, - editorViewState, - openLintPanel, - closeLintPanel, - useCallback(() => editorStore.setLintPanelOpen(false), [editorStore]), + const editorParentRef = useCallback( + (editorParent: HTMLDivElement | null) => { + editorStore.setEditorParent(editorParent); + }, + [editorStore], ); - useEffect(() => { - if (editorParentRef.current === null) { - return () => { - // Nothing to clean up. - }; - } - - const editorView = new EditorView({ - state: editorStore.state, - parent: editorParentRef.current, - dispatch: (transaction) => { - editorStore.onTransaction(transaction); - editorView.update([transaction]); - if (editorView.state !== editorStore.state) { - log.error( - 'Failed to synchronize editor state - store state:', - editorStore.state, - 'view state:', - editorView.state, - ); - } - }, - }); - fixCodeMirrorAccessibility(editorView); - setEditorViewState(editorView); - setSearchPanelOpen(false); - setLintPanelOpen(false); - // `dispatch` is bound to the view instance, - // so it does not have to be called as a method. - // eslint-disable-next-line @typescript-eslint/unbound-method - editorStore.updateDispatcher(editorView.dispatch); - log.info('Editor created'); - - return () => { - editorStore.updateDispatcher(null); - editorView.destroy(); - log.info('Editor destroyed'); - }; - }, [editorStore, setSearchPanelOpen, setLintPanelOpen]); - return ( - diff --git a/subprojects/frontend/src/editor/EditorButtons.tsx b/subprojects/frontend/src/editor/EditorButtons.tsx index 1412a314..34b64751 100644 --- a/subprojects/frontend/src/editor/EditorButtons.tsx +++ b/subprojects/frontend/src/editor/EditorButtons.tsx @@ -19,7 +19,7 @@ import { useRootStore } from '../RootStore'; // Exhastive switch as proven by TypeScript. // eslint-disable-next-line consistent-return -function getLintIcon(severity: Diagnostic['severity'] | null) { +function getLintIcon(severity: Diagnostic['severity'] | undefined) { switch (severity) { case 'error': return ; @@ -61,16 +61,16 @@ function EditorButtons(): JSX.Element { editorStore.toggleSearchPanel()} + selected={editorStore.searchPanel.state} + onClick={() => editorStore.searchPanel.toggle()} aria-label="Show find/replace" value="show-search-panel" > editorStore.toggleLintPanel()} + selected={editorStore.lintPanel.state} + onClick={() => editorStore.lintPanel.toggle()} aria-label="Show diagnostics panel" value="show-lint-panel" > diff --git a/subprojects/frontend/src/editor/EditorParent.ts b/subprojects/frontend/src/editor/EditorParent.ts deleted file mode 100644 index 3742b89c..00000000 --- a/subprojects/frontend/src/editor/EditorParent.ts +++ /dev/null @@ -1,228 +0,0 @@ -import { alpha, styled } from '@mui/material/styles'; - -export default styled('div', { - name: 'EditorParent', - shouldForwardProp: (propName) => propName !== 'showLineNumbers', -})<{ showLineNumbers: boolean }>(({ theme, showLineNumbers }) => { - const codeMirrorLintStyle: Record = {}; - (['error', 'warning', 'info'] as const).forEach((severity) => { - const color = theme.palette[severity].main; - codeMirrorLintStyle[`.cm-diagnostic-${severity}`] = { - borderLeftColor: color, - }; - codeMirrorLintStyle[`.cm-lintRange-${severity}`] = { - backgroundImage: 'none', - textDecoration: `underline wavy ${color}`, - textDecorationSkipInk: 'none', - }; - }); - - return { - background: theme.palette.background.default, - '&, .cm-editor': { - height: '100%', - }, - '.cm-content': { - padding: 0, - }, - '.cm-scroller, .cm-tooltip-autocomplete, .cm-completionLabel, .cm-completionDetail': - { - ...theme.typography.body1, - fontFamily: '"JetBrains MonoVariable", "JetBrains Mono", monospace', - fontFeatureSettings: '"liga", "calt"', - letterSpacing: 0, - textRendering: 'optimizeLegibility', - }, - '.cm-scroller': { - color: theme.palette.text.secondary, - }, - '.cm-gutters': { - background: 'transparent', - color: theme.palette.text.disabled, - border: 'none', - }, - '.cm-specialChar': { - color: theme.palette.secondary.main, - }, - '.cm-activeLine': { - background: theme.palette.highlight.activeLine, - }, - '.cm-foldGutter': { - color: alpha(theme.palette.text.primary, 0), - transition: theme.transitions.create('color', { - duration: theme.transitions.duration.short, - }), - '@media (hover: none)': { - color: theme.palette.text.primary, - }, - }, - '.cm-gutters:hover .cm-foldGutter': { - color: theme.palette.text.primary, - }, - '.cm-activeLineGutter': { - background: 'transparent', - }, - '.cm-lineNumbers': { - ...(!showLineNumbers && { - display: 'none !important', - }), - '.cm-activeLineGutter': { - color: theme.palette.text.primary, - }, - }, - '.cm-cursor, .cm-cursor-primary': { - borderLeft: `2px solid ${theme.palette.primary.main}`, - }, - '.cm-selectionBackground': { - background: theme.palette.selection.main, - }, - '.cm-focused': { - outline: 'none', - '.cm-selectionBackground': { - background: theme.palette.selection.main, - }, - }, - '.cm-panels-top': { - color: theme.palette.text.secondary, - }, - '.cm-panel': { - '&, & button, & input': { - fontFamily: theme.typography.fontFamily, - }, - background: theme.palette.background.default, - borderTop: `1px solid ${theme.palette.divider2}`, - 'button[name="close"]': { - background: 'transparent', - color: theme.palette.text.secondary, - cursor: 'pointer', - }, - }, - '.cm-panel.cm-panel-lint': { - boderBottom: 'none', - 'button[name="close"]': { - // Close button interferes with scrollbar, so we better hide it. - // The panel can still be closed from the toolbar. - display: 'none', - }, - ul: { - li: { - cursor: 'pointer', - color: theme.palette.text.primary, - }, - '[aria-selected], &:focus [aria-selected]': { - background: theme.palette.selection.main, - color: theme.palette.selection.contrastText, - }, - }, - }, - '.cm-foldPlaceholder': { - color: theme.palette.text.secondary, - backgroundColor: alpha(theme.palette.text.secondary, 0), - border: `1px solid ${alpha(theme.palette.text.secondary, 0.5)}`, - borderRadius: theme.shape.borderRadius, - transition: theme.transitions.create( - ['background-color', 'border-color', 'color'], - { - duration: theme.transitions.duration.short, - }, - ), - '&:hover': { - backgroundColor: alpha( - theme.palette.text.secondary, - theme.palette.action.hoverOpacity, - ), - borderColor: theme.palette.text.secondary, - '@media (hover: none)': { - backgroundColor: 'transparent', - }, - }, - }, - '.tok-comment': { - fontStyle: 'italic', - color: theme.palette.highlight.comment, - }, - '.tok-number': { - color: theme.palette.highlight.number, - }, - '.tok-string': { - color: theme.palette.secondary.dark, - }, - '.tok-keyword': { - color: theme.palette.primary.main, - }, - '.tok-typeName, .tok-macroName, .tok-atom': { - color: theme.palette.text.primary, - }, - '.tok-variableName': { - color: theme.palette.highlight.parameter, - }, - '.tok-problem-node': { - '&, & .tok-variableName': { - color: theme.palette.text.secondary, - }, - }, - '.tok-problem-individual': { - '&, & .tok-variableName': { - color: theme.palette.text.primary, - }, - }, - '.tok-problem-abstract, .tok-problem-new': { - fontStyle: 'italic', - }, - '.tok-problem-containment': { - fontWeight: 700, - }, - '.tok-problem-error': { - '&, & .tok-typeName': { - color: theme.palette.error.main, - }, - }, - '.tok-problem-builtin': { - '&, & .tok-typeName, & .tok-atom, & .tok-variableName': { - color: theme.palette.primary.main, - fontWeight: 400, - fontStyle: 'normal', - }, - }, - '.cm-tooltip-autocomplete': { - background: theme.palette.background.paper, - ...(theme.palette.mode === 'dark' && { - overflow: 'hidden', - borderRadius: theme.shape.borderRadius, - // https://github.com/mui/material-ui/blob/10c72729c7d03bab8cdce6eb422642684c56dca2/packages/mui-material/src/Paper/Paper.js#L18 - backgroundImage: - 'linear-gradient(rgba(255, 255, 255, 0.09), rgba(255, 255, 255, 0.09))', - }), - boxShadow: theme.shadows[4], - '.cm-completionIcon': { - color: theme.palette.text.secondary, - }, - '.cm-completionLabel': { - color: theme.palette.text.primary, - }, - '.cm-completionDetail': { - color: theme.palette.text.secondary, - fontStyle: 'normal', - }, - '[aria-selected]': { - background: `${theme.palette.primary.main} !important`, - '.cm-completionIcon, .cm-completionLabel, .cm-completionDetail': { - color: theme.palette.primary.contrastText, - }, - }, - }, - '.cm-completionIcon': { - width: 16, - padding: 0, - marginRight: '0.5em', - textAlign: 'center', - }, - ...codeMirrorLintStyle, - '.cm-problem-read': { - background: theme.palette.highlight.occurences.read, - }, - '.cm-problem-write': { - background: theme.palette.highlight.occurences.write, - }, - }; -}); diff --git a/subprojects/frontend/src/editor/EditorStore.ts b/subprojects/frontend/src/editor/EditorStore.ts index f75147a4..4bad68b3 100644 --- a/subprojects/frontend/src/editor/EditorStore.ts +++ b/subprojects/frontend/src/editor/EditorStore.ts @@ -1,58 +1,30 @@ +import { CompletionContext, CompletionResult } from '@codemirror/autocomplete'; +import { redo, redoDepth, undo, undoDepth } from '@codemirror/commands'; import { - closeBrackets, - closeBracketsKeymap, - autocompletion, - completionKeymap, -} from '@codemirror/autocomplete'; + type Diagnostic, + setDiagnostics, + closeLintPanel, + openLintPanel, + nextDiagnostic, +} from '@codemirror/lint'; +import { closeSearchPanel, openSearchPanel } from '@codemirror/search'; import { - defaultKeymap, - history, - historyKeymap, - indentWithTab, - redo, - redoDepth, - undo, - undoDepth, -} from '@codemirror/commands'; -import { - bracketMatching, - foldGutter, - foldKeymap, - indentOnInput, - syntaxHighlighting, -} from '@codemirror/language'; -import { type Diagnostic, lintKeymap, setDiagnostics } from '@codemirror/lint'; -import { search, searchKeymap } from '@codemirror/search'; -import { - EditorState, type StateCommand, StateEffect, type Transaction, type TransactionSpec, + type EditorState, } from '@codemirror/state'; -import { - drawSelection, - EditorView, - highlightActiveLine, - highlightActiveLineGutter, - highlightSpecialChars, - keymap, - lineNumbers, - rectangularSelection, -} from '@codemirror/view'; -import { classHighlighter } from '@lezer/highlight'; -import { makeAutoObservable, observable, reaction } from 'mobx'; - -import problemLanguageSupport from '../language/problemLanguageSupport'; -import type ThemeStore from '../theme/ThemeStore'; +import { type Command, EditorView } from '@codemirror/view'; +import { action, computed, makeObservable, observable } from 'mobx'; + import getLogger from '../utils/getLogger'; import XtextClient from '../xtext/XtextClient'; -import findOccurrences, { - type IOccurrence, - setOccurrences, -} from './findOccurrences'; -import semanticHighlighting, { +import PanelStore from './PanelStore'; +import createEditorState from './createEditorState'; +import { type IOccurrence, setOccurrences } from './findOccurrences'; +import { type IHighlightRange, setSemanticHighlighting, } from './semanticHighlighting'; @@ -60,17 +32,17 @@ import semanticHighlighting, { const log = getLogger('editor.EditorStore'); export default class EditorStore { - private readonly themeStore; - state: EditorState; private readonly client: XtextClient; - showLineNumbers = false; + view: EditorView | undefined; - showSearchPanel = false; + readonly searchPanel: PanelStore; - showLintPanel = false; + readonly lintPanel: PanelStore; + + showLineNumbers = false; errorCount = 0; @@ -78,116 +50,124 @@ export default class EditorStore { infoCount = 0; - private readonly defaultDispatcher = (tr: Transaction): void => { - this.onTransaction(tr); - }; - - private dispatcher = this.defaultDispatcher; - - constructor(initialValue: string, themeStore: ThemeStore) { - this.themeStore = themeStore; - this.state = EditorState.create({ - doc: initialValue, - extensions: [ - autocompletion({ - activateOnTyping: true, - override: [(context) => this.client.contentAssist(context)], - }), - closeBrackets(), - bracketMatching(), - drawSelection(), - EditorState.allowMultipleSelections.of(true), - EditorView.theme( - {}, - { - dark: this.themeStore.darkMode, - }, - ), - findOccurrences, - highlightActiveLine(), - highlightActiveLineGutter(), - highlightSpecialChars(), - history(), - indentOnInput(), - rectangularSelection(), - search({ - top: true, - caseSensitive: true, - }), - syntaxHighlighting(classHighlighter), - semanticHighlighting, - // We add the gutters to `extensions` in the order we want them to appear. - lineNumbers(), - foldGutter(), - keymap.of([ - { key: 'Mod-Shift-f', run: () => this.formatText() }, - ...closeBracketsKeymap, - ...completionKeymap, - ...foldKeymap, - ...historyKeymap, - indentWithTab, - // Override keys in `lintKeymap` to go through the `EditorStore`. - { key: 'Mod-Shift-m', run: () => this.setLintPanelOpen(true) }, - ...lintKeymap, - // Override keys in `searchKeymap` to go through the `EditorStore`. - { - key: 'Mod-f', - run: () => this.setSearchPanelOpen(true), - scope: 'editor search-panel', - }, - { - key: 'Escape', - run: () => this.setSearchPanelOpen(false), - scope: 'editor search-panel', - }, - ...searchKeymap, - ...defaultKeymap, - ]), - problemLanguageSupport(), - ], - }); + constructor(initialValue: string) { + this.state = createEditorState(initialValue, this); this.client = new XtextClient(this); - reaction( - () => this.themeStore.darkMode, - (darkMode) => { - log.debug('Update editor dark mode', darkMode); - this.dispatch({ - effects: [ - StateEffect.appendConfig.of( - EditorView.theme( - {}, - { - dark: darkMode, - }, - ), - ), - ], - }); - }, + this.searchPanel = new PanelStore( + 'search', + openSearchPanel, + closeSearchPanel, + this, + ); + this.lintPanel = new PanelStore( + 'panel-lint', + openLintPanel, + closeLintPanel, + this, ); - makeAutoObservable(this, { + makeObservable(this, { state: observable.ref, + view: observable.ref, + showLineNumbers: observable, + errorCount: observable, + warningCount: observable, + infoCount: observable, + highestDiagnosticLevel: computed, + canUndo: computed, + canRedo: computed, + setDarkMode: action, + setEditorParent: action, + dispatch: action, + dispatchTransaction: action, + doCommand: action, + doStateCommand: action, + updateDiagnostics: action, + nextDiagnostic: action, + updateOccurrences: action, + updateSemanticHighlighting: action, + undo: action, + redo: action, + toggleLineNumbers: action, }); } - updateDispatcher(newDispatcher: ((tr: Transaction) => void) | null): void { - this.dispatcher = newDispatcher || this.defaultDispatcher; + setDarkMode(darkMode: boolean): void { + log.debug('Update editor dark mode', darkMode); + this.dispatch({ + effects: [ + StateEffect.appendConfig.of([EditorView.darkTheme.of(darkMode)]), + ], + }); } - onTransaction(tr: Transaction): void { - log.trace('Editor transaction', tr); - this.state = tr.state; - this.client.onTransaction(tr); + setEditorParent(editorParent: Element | null): void { + if (this.view !== undefined) { + this.view.destroy(); + } + if (editorParent === null) { + this.view = undefined; + return; + } + const view = new EditorView({ + state: this.state, + parent: editorParent, + dispatch: (transaction) => { + this.dispatchTransactionWithoutView(transaction); + view.update([transaction]); + if (view.state !== this.state) { + log.error( + 'Failed to synchronize editor state - store state:', + this.state, + 'view state:', + view.state, + ); + } + }, + }); + this.view = view; + this.searchPanel.synchronizeStateToView(); + this.lintPanel.synchronizeStateToView(); + + // Reported by Lighthouse 8.3.0. + const { contentDOM } = view; + contentDOM.removeAttribute('aria-expanded'); + contentDOM.setAttribute('aria-label', 'Code editor'); + + log.info('Editor created'); } dispatch(...specs: readonly TransactionSpec[]): void { - this.dispatcher(this.state.update(...specs)); + const transaction = this.state.update(...specs); + this.dispatchTransaction(transaction); + } + + dispatchTransaction(transaction: Transaction): void { + if (this.view === undefined) { + this.dispatchTransactionWithoutView(transaction); + } else { + this.view.dispatch(transaction); + } + } + + private readonly dispatchTransactionWithoutView = action( + (tr: Transaction) => { + log.trace('Editor transaction', tr); + this.state = tr.state; + this.client.onTransaction(tr); + }, + ); + + doCommand(command: Command): boolean { + if (this.view === undefined) { + return false; + } + return command(this.view); } doStateCommand(command: StateCommand): boolean { return command({ state: this.state, - dispatch: this.dispatcher, + dispatch: (transaction) => this.dispatchTransaction(transaction), }); } @@ -213,7 +193,11 @@ export default class EditorStore { }); } - get highestDiagnosticLevel(): Diagnostic['severity'] | null { + nextDiagnostic(): void { + this.doCommand(nextDiagnostic); + } + + get highestDiagnosticLevel(): Diagnostic['severity'] | undefined { if (this.errorCount > 0) { return 'error'; } @@ -223,7 +207,7 @@ export default class EditorStore { if (this.infoCount > 0) { return 'info'; } - return null; + return undefined; } updateSemanticHighlighting(ranges: IHighlightRange[]): void { @@ -234,6 +218,10 @@ export default class EditorStore { this.dispatch(setOccurrences(write, read)); } + contentAssist(context: CompletionContext): Promise { + return this.client.contentAssist(context); + } + /** * @returns `true` if there is history to undo */ @@ -241,7 +229,6 @@ export default class EditorStore { return undoDepth(this.state) > 0; } - // eslint-disable-next-line class-methods-use-this undo(): void { log.debug('Undo', this.doStateCommand(undo)); } @@ -253,7 +240,6 @@ export default class EditorStore { return redoDepth(this.state) > 0; } - // eslint-disable-next-line class-methods-use-this redo(): void { log.debug('Redo', this.doStateCommand(redo)); } @@ -263,44 +249,6 @@ export default class EditorStore { log.debug('Show line numbers', this.showLineNumbers); } - /** - * Sets whether the CodeMirror search panel should be open. - * - * This method can be used as a CodeMirror command, - * because it returns `false` if it didn't execute, - * allowing other commands for the same keybind to run instead. - * This matches the behavior of the `openSearchPanel` and `closeSearchPanel` - * commands from `'@codemirror/search'`. - * - * @param newShowSearchPanel whether we should show the search panel - * @returns `true` if the state was changed, `false` otherwise - */ - setSearchPanelOpen(newShowSearchPanel: boolean): boolean { - if (this.showSearchPanel === newShowSearchPanel) { - return false; - } - this.showSearchPanel = newShowSearchPanel; - log.debug('Show search panel', this.showSearchPanel); - return true; - } - - toggleSearchPanel(): void { - this.setSearchPanelOpen(!this.showSearchPanel); - } - - setLintPanelOpen(newShowLintPanel: boolean): boolean { - if (this.showLintPanel === newShowLintPanel) { - return false; - } - this.showLintPanel = newShowLintPanel; - log.debug('Show lint panel', this.showLintPanel); - return true; - } - - toggleLintPanel(): void { - this.setLintPanelOpen(!this.showLintPanel); - } - formatText(): boolean { this.client.formatText(); return true; diff --git a/subprojects/frontend/src/editor/EditorTheme.ts b/subprojects/frontend/src/editor/EditorTheme.ts new file mode 100644 index 00000000..8d98e832 --- /dev/null +++ b/subprojects/frontend/src/editor/EditorTheme.ts @@ -0,0 +1,342 @@ +import errorSVG from '@material-icons/svg/svg/error/baseline.svg?raw'; +import expandMoreSVG from '@material-icons/svg/svg/expand_more/baseline.svg?raw'; +import infoSVG from '@material-icons/svg/svg/info/baseline.svg?raw'; +import warningSVG from '@material-icons/svg/svg/warning/baseline.svg?raw'; +import { alpha, styled } from '@mui/material/styles'; + +import editorClassNames from './editorClassNames'; + +function svgURL(svg: string): string { + return `url('data:image/svg+xml;utf8,${svg}')`; +} + +export default styled('div', { + name: 'EditorTheme', + shouldForwardProp: (propName) => propName !== 'showLineNumbers', +})<{ showLineNumbers: boolean }>(({ theme, showLineNumbers }) => { + let codeMirrorLintStyle: Record = {}; + ( + [ + { + severity: 'error', + icon: errorSVG, + }, + { + severity: 'warning', + icon: warningSVG, + }, + { + severity: 'info', + icon: infoSVG, + }, + ] as const + ).forEach(({ severity, icon }) => { + const palette = theme.palette[severity]; + const color = palette.main; + const iconStyle = { + background: color, + maskImage: svgURL(icon), + maskSize: '16px 16px', + height: 16, + width: 16, + }; + const tooltipColor = + theme.palette.mode === 'dark' ? palette.main : palette.light; + codeMirrorLintStyle = { + ...codeMirrorLintStyle, + [`.cm-lintRange-${severity}`]: { + backgroundImage: 'none', + textDecoration: `underline wavy ${color}`, + textDecorationSkipInk: 'none', + }, + [`.cm-diagnostic-${severity}`]: { + marginLeft: 0, + padding: '4px 8px 4px 32px', + borderLeft: 'none', + position: 'relative', + '::before': { + ...iconStyle, + content: '" "', + position: 'absolute', + top: 6, + left: 8, + }, + }, + [`.cm-tooltip .cm-diagnostic-${severity}::before`]: { + background: tooltipColor, + }, + [`.cm-lint-marker-${severity}`]: { + ...iconStyle, + display: 'block', + margin: '4px 0', + // Remove original CodeMirror icon. + content: '""', + '::before': { + // Remove original CodeMirror icon. + content: '""', + display: 'none', + }, + }, + }; + }); + + return { + background: theme.palette.background.default, + '&, .cm-editor': { + height: '100%', + }, + '.cm-content': { + padding: 0, + }, + '.cm-scroller': { + color: theme.palette.text.secondary, + }, + '.cm-scroller, .cm-tooltip-autocomplete, .cm-completionLabel, .cm-completionDetail': + { + ...theme.typography.body1, + fontFamily: '"JetBrains MonoVariable", "JetBrains Mono", monospace', + fontFeatureSettings: '"liga", "calt"', + letterSpacing: 0, + textRendering: 'optimizeLegibility', + }, + '.cm-gutters': { + background: 'transparent', + color: theme.palette.text.disabled, + border: 'none', + }, + '.cm-specialChar': { + color: theme.palette.secondary.main, + }, + '.cm-activeLine': { + background: theme.palette.highlight.activeLine, + }, + '.cm-gutter-lint': { + width: 16, + '.cm-gutterElement': { + padding: 0, + }, + }, + '.cm-foldGutter': { + opacity: 0, + width: 16, + transition: theme.transitions.create('opacity', { + duration: theme.transitions.duration.short, + }), + '@media (hover: none)': { + opacity: 1, + }, + }, + '.cm-gutters:hover .cm-foldGutter': { + opacity: 1, + }, + [`.${editorClassNames.foldMarker}`]: { + display: 'block', + margin: '4px 0', + padding: 0, + maskImage: svgURL(expandMoreSVG), + maskSize: '16px 16px', + height: 16, + width: 16, + background: theme.palette.text.primary, + border: 'none', + cursor: 'pointer', + }, + [`.${editorClassNames.foldMarkerClosed}`]: { + transform: 'rotate(-90deg)', + }, + '.cm-activeLineGutter': { + background: 'transparent', + }, + '.cm-lineNumbers': { + ...(!showLineNumbers && { + display: 'none !important', + }), + '.cm-activeLineGutter': { + color: theme.palette.text.primary, + }, + }, + '.cm-cursor, .cm-cursor-primary': { + borderLeft: `2px solid ${theme.palette.primary.main}`, + }, + '.cm-selectionBackground': { + background: theme.palette.highlight.selection, + }, + '.cm-focused': { + outline: 'none', + '.cm-selectionBackground': { + background: theme.palette.highlight.selection, + }, + }, + '.cm-panels-top': { + color: theme.palette.text.secondary, + borderBottom: `1px solid ${theme.palette.outer.border}`, + marginBottom: theme.spacing(1), + }, + '.cm-panel': { + position: 'relative', + overflow: 'hidden', + background: theme.palette.outer.background, + borderTop: `1px solid ${theme.palette.outer.border}`, + '&, & button, & input': { + fontFamily: theme.typography.fontFamily, + }, + 'button[name="close"]': { + background: 'transparent', + color: theme.palette.text.secondary, + cursor: 'pointer', + }, + }, + '.cm-panel.cm-panel-lint': { + borderTop: `1px solid ${theme.palette.outer.border}`, + borderBottom: 'none', + 'button[name="close"]': { + // Close button interferes with scrollbar, so we better hide it. + // The panel can still be closed from the toolbar. + display: 'none', + }, + ul: { + maxHeight: 'max(112px, 20vh)', + li: { + cursor: 'pointer', + color: theme.palette.text.primary, + }, + '.cm-diagnostic': { + ...theme.typography.body2, + '&[aria-selected="true"]': { + color: theme.palette.text.primary, + background: 'transparent', + fontWeight: 700, + }, + ':hover': { + background: alpha( + theme.palette.text.primary, + theme.palette.action.hoverOpacity, + ), + }, + }, + }, + }, + [`.${editorClassNames.foldPlaceholder}`]: { + ...theme.typography.body1, + padding: 0, + fontFamily: 'inherit', + fontFeatureSettings: '"liga", "calt"', + color: theme.palette.text.secondary, + backgroundColor: alpha( + theme.palette.text.secondary, + theme.palette.action.focusOpacity, + ), + border: 'none', + cursor: 'pointer', + transition: theme.transitions.create(['background-color', 'color'], { + duration: theme.transitions.duration.short, + }), + '&:hover': { + color: theme.palette.text.primary, + backgroundColor: alpha( + theme.palette.text.secondary, + theme.palette.action.focusOpacity + theme.palette.action.hoverOpacity, + ), + }, + }, + '.tok-comment': { + fontStyle: 'italic', + color: theme.palette.highlight.comment, + }, + '.tok-number': { + color: theme.palette.highlight.number, + }, + '.tok-string': { + color: theme.palette.secondary, + }, + '.tok-keyword': { + color: theme.palette.primary.main, + }, + '.tok-typeName, .tok-atom': { + color: theme.palette.text.primary, + }, + '.tok-variableName': { + color: theme.palette.highlight.parameter, + }, + '.tok-problem-node': { + '&, & .tok-variableName': { + color: theme.palette.text.secondary, + }, + }, + '.tok-problem-individual': { + '&, & .tok-variableName': { + color: theme.palette.text.primary, + }, + }, + '.tok-problem-abstract, .tok-problem-new': { + fontStyle: 'italic', + }, + '.tok-problem-containment': { + fontWeight: 700, + }, + '.tok-problem-error': { + '&, & .tok-typeName': { + color: theme.palette.error.main, + }, + }, + '.tok-problem-builtin': { + '&, & .tok-typeName, & .tok-atom, & .tok-variableName': { + color: theme.palette.primary.main, + fontWeight: 400, + fontStyle: 'normal', + }, + }, + '.cm-tooltip.cm-tooltip-autocomplete': { + background: theme.palette.background.paper, + borderRadius: theme.shape.borderRadius, + overflow: 'hidden', + ...(theme.palette.mode === 'dark' && { + // https://github.com/mui/material-ui/blob/10c72729c7d03bab8cdce6eb422642684c56dca2/packages/mui-material/src/Paper/Paper.js#L18 + backgroundImage: + 'linear-gradient(rgba(255, 255, 255, 0.07), rgba(255, 255, 255, 0.07))', + }), + boxShadow: theme.shadows[2], + '.cm-completionIcon': { + color: theme.palette.text.secondary, + }, + '.cm-completionLabel': { + color: theme.palette.text.primary, + }, + '.cm-completionDetail': { + color: theme.palette.text.secondary, + fontStyle: 'normal', + }, + 'li[aria-selected="true"]': { + background: alpha( + theme.palette.text.primary, + theme.palette.action.focusOpacity, + ), + '.cm-completionIcon, .cm-completionLabel, .cm-completionDetail': { + color: theme.palette.text.primary, + }, + }, + }, + '.cm-tooltip.cm-tooltip-hover, .cm-tooltip.cm-tooltip-lint': { + ...theme.typography.body2, + // https://github.com/mui/material-ui/blob/dee9529f7a298c54ae760761112c3ae9ba082137/packages/mui-material/src/Tooltip/Tooltip.js#L121-L125 + background: alpha(theme.palette.grey[700], 0.92), + borderRadius: theme.shape.borderRadius, + color: theme.palette.common.white, + overflow: 'hidden', + maxWidth: 400, + }, + '.cm-completionIcon': { + width: 16, + padding: 0, + marginRight: '0.5em', + textAlign: 'center', + }, + ...codeMirrorLintStyle, + '.cm-problem-read': { + background: theme.palette.highlight.occurences.read, + }, + '.cm-problem-write': { + background: theme.palette.highlight.occurences.write, + }, + }; +}); diff --git a/subprojects/frontend/src/editor/GenerateButton.tsx b/subprojects/frontend/src/editor/GenerateButton.tsx index 4d5c4e44..0eed129e 100644 --- a/subprojects/frontend/src/editor/GenerateButton.tsx +++ b/subprojects/frontend/src/editor/GenerateButton.tsx @@ -22,7 +22,7 @@ function GenerateButton(): JSX.Element { if (errorCount > 0) { return ( - ); diff --git a/subprojects/frontend/src/editor/PanelStore.ts b/subprojects/frontend/src/editor/PanelStore.ts new file mode 100644 index 00000000..653d309c --- /dev/null +++ b/subprojects/frontend/src/editor/PanelStore.ts @@ -0,0 +1,90 @@ +import type { Command } from '@codemirror/view'; +import { action, makeObservable, observable } from 'mobx'; + +import getLogger from '../utils/getLogger'; + +import type EditorStore from './EditorStore'; + +const log = getLogger('editor.PanelStore'); + +export default class PanelStore { + state = false; + + constructor( + private readonly panelId: string, + private readonly openCommand: Command, + private readonly closeCommand: Command, + private readonly store: EditorStore, + ) { + makeObservable(this, { + state: observable, + open: action, + close: action, + toggle: action, + synchronizeStateToView: action, + }); + } + + open(): boolean { + return this.setState(true); + } + + close(): boolean { + return this.setState(false); + } + + toggle(): void { + this.setState(!this.state); + } + + private setState(newState: boolean): boolean { + if (this.state === newState) { + return false; + } + log.debug('Show', this.panelId, 'panel', newState); + if (newState) { + this.doOpen(); + } else { + this.doClose(); + } + this.state = newState; + return true; + } + + synchronizeStateToView(): void { + this.doClose(); + if (this.state) { + this.doOpen(); + } + } + + private doOpen(): void { + if (!this.store.doCommand(this.openCommand)) { + return; + } + const { view } = this.store; + if (view === undefined) { + return; + } + const buttonQuery = `.cm-${this.panelId}.cm-panel button[name="close"]`; + const closeButton = view.dom.querySelector(buttonQuery); + if (closeButton !== null) { + log.debug('Addig close button callback to', this.panelId, 'panel'); + // We must remove the event listener from the button that dispatches a transaction + // without going through `EditorStore`. This listened is added by CodeMirror, + // and we can only remove it by cloning the DOM node: https://stackoverflow.com/a/9251864 + const closeButtonWithoutListeners = closeButton.cloneNode(true); + closeButtonWithoutListeners.addEventListener('click', (event) => { + this.close(); + event.preventDefault(); + }); + closeButton.replaceWith(closeButtonWithoutListeners); + } else { + log.error('Opened', this.panelId, 'panel has no close button'); + } + } + + private doClose(): void { + this.store.doCommand(this.closeCommand); + } +} diff --git a/subprojects/frontend/src/editor/createEditorState.ts b/subprojects/frontend/src/editor/createEditorState.ts new file mode 100644 index 00000000..33346c05 --- /dev/null +++ b/subprojects/frontend/src/editor/createEditorState.ts @@ -0,0 +1,121 @@ +import { + closeBrackets, + closeBracketsKeymap, + autocompletion, + completionKeymap, +} from '@codemirror/autocomplete'; +import { + defaultKeymap, + history, + historyKeymap, + indentWithTab, +} from '@codemirror/commands'; +import { + bracketMatching, + codeFolding, + foldGutter, + foldKeymap, + indentOnInput, + syntaxHighlighting, +} from '@codemirror/language'; +import { lintKeymap, lintGutter } from '@codemirror/lint'; +import { search, searchKeymap } from '@codemirror/search'; +import { EditorState } from '@codemirror/state'; +import { + drawSelection, + highlightActiveLine, + highlightActiveLineGutter, + highlightSpecialChars, + keymap, + lineNumbers, + rectangularSelection, +} from '@codemirror/view'; +import { classHighlighter } from '@lezer/highlight'; + +import problemLanguageSupport from '../language/problemLanguageSupport'; + +import type EditorStore from './EditorStore'; +import editorClassNames from './editorClassNames'; +import findOccurrences from './findOccurrences'; +import semanticHighlighting from './semanticHighlighting'; + +export default function createEditorState( + initialValue: string, + store: EditorStore, +): EditorState { + return EditorState.create({ + doc: initialValue, + extensions: [ + autocompletion({ + activateOnTyping: true, + override: [(context) => store.contentAssist(context)], + }), + closeBrackets(), + bracketMatching(), + drawSelection(), + EditorState.allowMultipleSelections.of(true), + findOccurrences, + highlightActiveLine(), + highlightActiveLineGutter(), + highlightSpecialChars(), + history(), + indentOnInput(), + rectangularSelection(), + search({ top: true }), + syntaxHighlighting(classHighlighter), + semanticHighlighting, + // We add the gutters to `extensions` in the order we want them to appear. + lintGutter(), + lineNumbers(), + codeFolding({ + placeholderDOM(_view, onClick) { + const button = document.createElement('button'); + button.className = editorClassNames.foldPlaceholder; + button.ariaLabel = 'Unfold lines'; + button.innerText = '...'; + button.onclick = onClick; + return button; + }, + }), + foldGutter({ + markerDOM(open) { + const button = document.createElement('button'); + button.className = [ + editorClassNames.foldMarker, + open + ? editorClassNames.foldMarkerOpen + : editorClassNames.foldMarkerClosed, + ].join(' '); + button.ariaPressed = open ? 'true' : 'false'; + button.ariaLabel = 'Fold lines'; + return button; + }, + }), + keymap.of([ + { key: 'Mod-Shift-f', run: () => store.formatText() }, + ...closeBracketsKeymap, + ...completionKeymap, + ...foldKeymap, + ...historyKeymap, + indentWithTab, + // Override keys in `lintKeymap` to go through the `EditorStore`. + { key: 'Mod-Shift-m', run: () => store.lintPanel.open() }, + ...lintKeymap, + // Override keys in `searchKeymap` to go through the `EditorStore`. + { + key: 'Mod-f', + run: () => store.searchPanel.open(), + scope: 'editor search-panel', + }, + { + key: 'Escape', + run: () => store.searchPanel.close(), + scope: 'editor search-panel', + }, + ...searchKeymap, + ...defaultKeymap, + ]), + problemLanguageSupport(), + ], + }); +} diff --git a/subprojects/frontend/src/editor/editorClassNames.ts b/subprojects/frontend/src/editor/editorClassNames.ts new file mode 100644 index 00000000..5584e8c2 --- /dev/null +++ b/subprojects/frontend/src/editor/editorClassNames.ts @@ -0,0 +1,10 @@ +const PREFIX = 'problem-editor-'; + +const editorClassNames = { + foldPlaceholder: `${PREFIX}fold-placeholder`, + foldMarker: `${PREFIX}fold-marker`, + foldMarkerClosed: `${PREFIX}fold-marker-closed`, + foldMarkerOpen: `${PREFIX}fold-marker-open`, +}; + +export default editorClassNames; diff --git a/subprojects/frontend/src/language/problemLanguageSupport.ts b/subprojects/frontend/src/language/problemLanguageSupport.ts index 246135d8..07a884e7 100644 --- a/subprojects/frontend/src/language/problemLanguageSupport.ts +++ b/subprojects/frontend/src/language/problemLanguageSupport.ts @@ -28,18 +28,18 @@ const parserWithMetadata = parser.configure({ BlockComment: t.blockComment, 'problem class enum pred rule indiv scope': t.definitionKeyword, 'abstract extends refers contains opposite error default': t.modifier, - 'true false unknown error': t.keyword, + 'true false unknown error': t.operatorKeyword, 'may must current count': t.operatorKeyword, - 'new delete': t.operatorKeyword, + 'new delete': t.keyword, NotOp: t.operator, UnknownOp: t.operator, - OrOp: t.punctuation, + OrOp: t.separator, StarArgument: t.keyword, 'IntMult StarMult Real': t.number, StarMult: t.number, String: t.string, 'RelationName/QualifiedName': t.typeName, - 'RuleName/QualifiedName': t.macroName, + 'RuleName/QualifiedName': t.typeName, 'IndividualNodeName/QualifiedName': t.atom, 'VariableName/QualifiedName': t.variableName, '{ }': t.brace, diff --git a/subprojects/frontend/src/theme/EditorTheme.ts b/subprojects/frontend/src/theme/EditorTheme.ts deleted file mode 100644 index a16b4c3b..00000000 --- a/subprojects/frontend/src/theme/EditorTheme.ts +++ /dev/null @@ -1,7 +0,0 @@ -enum EditorTheme { - Light, - Dark, - Default = EditorTheme.Dark, -} - -export default EditorTheme; diff --git a/subprojects/frontend/src/theme/ThemeProvider.tsx b/subprojects/frontend/src/theme/ThemeProvider.tsx index 9a8fdd44..dd4f5bb8 100644 --- a/subprojects/frontend/src/theme/ThemeProvider.tsx +++ b/subprojects/frontend/src/theme/ThemeProvider.tsx @@ -11,13 +11,17 @@ import React, { type ReactNode } from 'react'; import { useRootStore } from '../RootStore'; -import EditorTheme from './EditorTheme'; +interface OuterPalette { + background: string; + border: string; +} -interface HighlightStyles { +interface HighlightPalette { number: string; parameter: string; comment: string; activeLine: string; + selection: string; occurences: { read: string; write: string; @@ -26,19 +30,17 @@ interface HighlightStyles { declare module '@mui/material/styles' { interface Palette { - divider2: string; - selection: Palette['primary']; - highlight: HighlightStyles; + outer: OuterPalette; + highlight: HighlightPalette; } interface PaletteOptions { - divider2: string; - selection: PaletteOptions['primary']; - highlight: HighlightStyles; + outer: OuterPalette; + highlight: HighlightPalette; } } -function getMUIThemeOptions(currentTheme: EditorTheme): ThemeOptions { +function getMUIThemeOptions(darkMode: boolean): ThemeOptions { const components: Components = { MuiButton: { styleOverrides: { @@ -67,32 +69,8 @@ function getMUIThemeOptions(currentTheme: EditorTheme): ThemeOptions { }, }; - switch (currentTheme) { - case EditorTheme.Light: - return { - components, - palette: { - mode: 'light', - primary: { main: '#0097a7' }, - selection: { - main: '#c8e4fb', - contrastText: '#000', - }, - divider2: '#d7d7d7', - highlight: { - number: '#1976d2', - parameter: '#6a3e3e', - comment: alpha('#000', 0.38), - activeLine: '#f5f5f5', - occurences: { - read: '#ceccf7', - write: '#f0d8a8', - }, - }, - }, - }; - case EditorTheme.Dark: - return { + return darkMode + ? { components, palette: { mode: 'dark', @@ -111,34 +89,53 @@ function getMUIThemeOptions(currentTheme: EditorTheme): ThemeOptions { disabled: '#4b5263', }, divider: alpha('#abb2bf', 0.16), - divider2: '#181a1f', - selection: { - main: '#3e4453', - contrastText: '#fff', + outer: { + background: '#21252b', + border: '#181a1f', }, highlight: { number: '#6188a6', parameter: '#c8ae9d', comment: '#6b717d', activeLine: '#21252b', + selection: '#3e4453', occurences: { read: 'rgba(255, 255, 255, 0.15)', write: 'rgba(255, 255, 128, 0.4)', }, }, }, + } + : { + components, + palette: { + mode: 'light', + primary: { main: '#0097a7' }, + outer: { + background: '#f5f5f5', + border: '#d7d7d7', + }, + highlight: { + number: '#1976d2', + parameter: '#6a3e3e', + comment: alpha('#000', 0.38), + activeLine: '#f5f5f5', + selection: '#c8e4fb', + occurences: { + read: '#ceccf7', + write: '#f0d8a8', + }, + }, + }, }; - default: - throw new Error(`Unknown theme: ${currentTheme}`); - } } function ThemeProvider({ children }: { children?: ReactNode }) { const { - themeStore: { currentTheme }, + themeStore: { darkMode }, } = useRootStore(); - const themeOptions = getMUIThemeOptions(currentTheme); + const themeOptions = getMUIThemeOptions(darkMode); const theme = responsiveFontSizes(createTheme(themeOptions)); return ( diff --git a/subprojects/frontend/src/theme/ThemeStore.ts b/subprojects/frontend/src/theme/ThemeStore.ts index ded1f29a..11391b06 100644 --- a/subprojects/frontend/src/theme/ThemeStore.ts +++ b/subprojects/frontend/src/theme/ThemeStore.ts @@ -1,28 +1,16 @@ -import { makeAutoObservable } from 'mobx'; - -import EditorTheme from './EditorTheme'; +import { action, makeObservable, observable } from 'mobx'; export default class ThemeStore { - currentTheme: EditorTheme = EditorTheme.Default; + darkMode = true; constructor() { - makeAutoObservable(this); + makeObservable(this, { + darkMode: observable, + toggleDarkMode: action, + }); } toggleDarkMode(): void { - switch (this.currentTheme) { - case EditorTheme.Light: - this.currentTheme = EditorTheme.Dark; - break; - case EditorTheme.Dark: - this.currentTheme = EditorTheme.Light; - break; - default: - throw new Error(`Unknown theme: ${this.currentTheme}`); - } - } - - get darkMode(): boolean { - return this.currentTheme === EditorTheme.Dark; + this.darkMode = !this.darkMode; } } diff --git a/subprojects/frontend/tsconfig.base.json b/subprojects/frontend/tsconfig.base.json index e33e330e..9cc8ace4 100644 --- a/subprojects/frontend/tsconfig.base.json +++ b/subprojects/frontend/tsconfig.base.json @@ -5,6 +5,7 @@ "moduleResolution": "Node", "esModuleInterop": true, "allowSyntheticDefaultImports": true, + "useDefineForClassFields": true, "strict": true, "noImplicitOverride": true, "noImplicitReturns": true, diff --git a/yarn.lock b/yarn.lock index 16dcb9f6..0f71c223 100644 --- a/yarn.lock +++ b/yarn.lock @@ -388,14 +388,14 @@ __metadata: languageName: node linkType: hard -"@codemirror/search@npm:^6.0.1": - version: 6.0.1 - resolution: "@codemirror/search@npm:6.0.1" +"@codemirror/search@npm:^6.1.0": + version: 6.1.0 + resolution: "@codemirror/search@npm:6.1.0" dependencies: "@codemirror/state": ^6.0.0 "@codemirror/view": ^6.0.0 crelt: ^1.0.5 - checksum: e73536db38a4ee0f5a2c2044ad4754f8d4098900f36ca8deb42b3548b39be7d6be2a475fc41b75cd188f25c9613fe93b20177a03f79e2eac5ec0fa71f56c552e + checksum: 3cf85eec96236f856ba947241a0587e46bbfd3c88f5a90bd4c5f0bbb867055732b8cf6dc146e5e0c3f4dfd87540e97908fdae033f991c55f6fa9179059cb80fc languageName: node linkType: hard @@ -718,24 +718,31 @@ __metadata: languageName: node linkType: hard -"@lezer/lr@npm:^1.0.0, @lezer/lr@npm:^1.2.2": - version: 1.2.2 - resolution: "@lezer/lr@npm:1.2.2" +"@lezer/lr@npm:^1.0.0, @lezer/lr@npm:^1.2.3": + version: 1.2.3 + resolution: "@lezer/lr@npm:1.2.3" dependencies: "@lezer/common": ^1.0.0 - checksum: fa951958ad9d0012acb09c0fe4c381fe97f6d2ab56e89932272c9d444775232a9b10f4be577068b609a60ae990d3e2714e8f5f9030d18260531f746af857c266 + checksum: 118db077d32f6eb9d6b219c766d7e5715c6c004d89d76ed0137ea2a364aeca67f21dc5924ea67a649b5de1d13bfbb85571923b38536e6995820eb0a5c8844594 languageName: node linkType: hard -"@mui/base@npm:5.0.0-alpha.92": - version: 5.0.0-alpha.92 - resolution: "@mui/base@npm:5.0.0-alpha.92" +"@material-icons/svg@npm:^1.0.32": + version: 1.0.32 + resolution: "@material-icons/svg@npm:1.0.32" + checksum: ed3827a61834d92e70354b450212ff8c7a84ddb9ca148bf9bf096d265903da96f835edf1a7d5d83218951105592a42de70e6ba4c7f46013968e3b63a5a5b5185 + languageName: node + linkType: hard + +"@mui/base@npm:5.0.0-alpha.93": + version: 5.0.0-alpha.93 + resolution: "@mui/base@npm:5.0.0-alpha.93" dependencies: "@babel/runtime": ^7.17.2 "@emotion/is-prop-valid": ^1.1.3 "@mui/types": ^7.1.5 "@mui/utils": ^5.9.3 - "@popperjs/core": ^2.11.5 + "@popperjs/core": ^2.11.6 clsx: ^1.2.1 prop-types: ^15.8.1 react-is: ^18.2.0 @@ -746,7 +753,14 @@ __metadata: peerDependenciesMeta: "@types/react": optional: true - checksum: ecb4ed58164525125bc17fbfc8e7cbb8270746ab9cce001fada2116b80d8857c42d5ea8e43de6bc313bae2a78ff9e0180e844cde7c868706e023ac4df759d23b + checksum: 8e04ac3d7e453d8acea73884fea899db4561218ba48644b04a11dfb905dbf630a84847a28ca62bc03d678c8db749e76e150d9cf99d1fbb5ff892e6b734527c29 + languageName: node + linkType: hard + +"@mui/core-downloads-tracker@npm:^5.10.1": + version: 5.10.1 + resolution: "@mui/core-downloads-tracker@npm:5.10.1" + checksum: 275fbabd9beb6d4cbbe5d6cf2a7621fb827bb7ad8be2fdd218ac3b1cc7c2ee9f69cbb71022b8a6f9ecaadbc21225015b33232fc3827dabe8db5c12fbf642cfc9 languageName: node linkType: hard @@ -766,13 +780,14 @@ __metadata: languageName: node linkType: hard -"@mui/material@npm:5.10.0": - version: 5.10.0 - resolution: "@mui/material@npm:5.10.0" +"@mui/material@npm:5.10.1": + version: 5.10.1 + resolution: "@mui/material@npm:5.10.1" dependencies: "@babel/runtime": ^7.17.2 - "@mui/base": 5.0.0-alpha.92 - "@mui/system": ^5.10.0 + "@mui/base": 5.0.0-alpha.93 + "@mui/core-downloads-tracker": ^5.10.1 + "@mui/system": ^5.10.1 "@mui/types": ^7.1.5 "@mui/utils": ^5.9.3 "@types/react-transition-group": ^4.4.5 @@ -794,7 +809,7 @@ __metadata: optional: true "@types/react": optional: true - checksum: c31cdefa094ab9f802d60519cf1884e8c5a8f162f6e567d32072d86d4bfa8807a8c1e4fe52614d0853ebdd4da5daccdfdce3d4cf79b8f29e8b67864f1d326a7b + checksum: 47c8157757df28863f4788f551ddb55e8268a246b2691fe5f0351d83040d874314a3b483d33359824f918abb861e3c6dca91562a5999ebda456c743890e6978a languageName: node linkType: hard @@ -815,9 +830,9 @@ __metadata: languageName: node linkType: hard -"@mui/styled-engine@npm:^5.10.0": - version: 5.10.0 - resolution: "@mui/styled-engine@npm:5.10.0" +"@mui/styled-engine@npm:^5.10.1": + version: 5.10.1 + resolution: "@mui/styled-engine@npm:5.10.1" dependencies: "@babel/runtime": ^7.17.2 "@emotion/cache": ^11.9.3 @@ -832,17 +847,17 @@ __metadata: optional: true "@emotion/styled": optional: true - checksum: 7fc41cdb72e3b660ab3dafbb1e3101f86a9a8c133443291254ad4e158638e1253b4ecd4cd55c48c5c1364ca31dbe93c8495ed125bba9ba305f6d2dd175ff0285 + checksum: 27c4003bdb1f8a76f30ed6c458789631a93efee832631edeb63940ca3ede70f8aee8e39987b1da1b8717fcbd907d00554d2b5fecc83266eee70c3637c9b66712 languageName: node linkType: hard -"@mui/system@npm:^5.10.0": - version: 5.10.0 - resolution: "@mui/system@npm:5.10.0" +"@mui/system@npm:^5.10.1": + version: 5.10.1 + resolution: "@mui/system@npm:5.10.1" dependencies: "@babel/runtime": ^7.17.2 "@mui/private-theming": ^5.9.3 - "@mui/styled-engine": ^5.10.0 + "@mui/styled-engine": ^5.10.1 "@mui/types": ^7.1.5 "@mui/utils": ^5.9.3 clsx: ^1.2.1 @@ -860,7 +875,7 @@ __metadata: optional: true "@types/react": optional: true - checksum: be86853ca79ed8a889d37fa02780c0dfe477ff5ffda4cabba8bf5865186b578cbd50646ea902c3f5712baf83d4b11fbd21428f643b42d1839d671cbedddfc546 + checksum: 0acb163dec856af3813ff043b4d8441d44431add7cb234cf16d386b2b70e30cba6261b51ac936f886d746f4b3ab3475b58e33e09f76cd92a6f6aadd61dca3927 languageName: node linkType: hard @@ -952,10 +967,10 @@ __metadata: languageName: node linkType: hard -"@popperjs/core@npm:^2.11.5": - version: 2.11.5 - resolution: "@popperjs/core@npm:2.11.5" - checksum: fd7f9dca3fb716d7426332b6ee283f88d2724c0ab342fb678865a640bad403dfb9eeebd8204a406986162f7e2b33394f104320008b74d0e9066d7322f70ea35d +"@popperjs/core@npm:^2.11.6": + version: 2.11.6 + resolution: "@popperjs/core@npm:2.11.6" + checksum: 47fb328cec1924559d759b48235c78574f2d71a8a6c4c03edb6de5d7074078371633b91e39bbf3f901b32aa8af9b9d8f82834856d2f5737a23475036b16817f0 languageName: node linkType: hard @@ -967,7 +982,7 @@ __metadata: "@codemirror/commands": ^6.0.1 "@codemirror/language": ^6.2.1 "@codemirror/lint": ^6.0.0 - "@codemirror/search": ^6.0.1 + "@codemirror/search": ^6.1.0 "@codemirror/state": ^6.1.1 "@codemirror/view": ^6.2.0 "@emotion/react": ^11.10.0 @@ -978,16 +993,17 @@ __metadata: "@lezer/common": ^1.0.0 "@lezer/generator": ^1.1.1 "@lezer/highlight": ^1.0.0 - "@lezer/lr": ^1.2.2 + "@lezer/lr": ^1.2.3 + "@material-icons/svg": ^1.0.32 "@mui/icons-material": 5.8.4 - "@mui/material": 5.10.0 + "@mui/material": 5.10.1 "@types/eslint": ^8.4.5 - "@types/node": ^18.7.4 + "@types/node": ^18.7.6 "@types/prettier": ^2.7.0 "@types/react": ^18.0.17 "@types/react-dom": ^18.0.6 - "@typescript-eslint/eslint-plugin": ^5.33.0 - "@typescript-eslint/parser": ^5.33.0 + "@typescript-eslint/eslint-plugin": ^5.33.1 + "@typescript-eslint/parser": ^5.33.1 "@vitejs/plugin-react": ^2.0.1 ansi-styles: ^6.1.0 cross-env: ^7.0.3 @@ -1011,7 +1027,7 @@ __metadata: react: ^18.2.0 react-dom: ^18.2.0 typescript: ~4.7.4 - vite: ^3.0.7 + vite: ^3.0.8 vite-plugin-inject-preload: ^1.0.1 zod: ^3.18.0 languageName: unknown @@ -1064,10 +1080,10 @@ __metadata: languageName: node linkType: hard -"@types/node@npm:^18.7.4": - version: 18.7.4 - resolution: "@types/node@npm:18.7.4" - checksum: 051d2147e4d8129fceb63ee9384259b2f224dbc4e4b0c46d96a6b61cbaad4e3fe4060950e7f4fc3d5692b1e6ea47e68ad03b61155754bfa169593747cfe3f8f4 +"@types/node@npm:^18.7.6": + version: 18.7.6 + resolution: "@types/node@npm:18.7.6" + checksum: 5122988c325eda8d1f5cbe4494916036aae1758f9d5bb2d8139a800b8bad1540fbb167cd3c759da9a5cb4600cd3507609ac7969747113c1549a3e4320a17b1a9 languageName: node linkType: hard @@ -1137,13 +1153,13 @@ __metadata: languageName: node linkType: hard -"@typescript-eslint/eslint-plugin@npm:^5.33.0": - version: 5.33.0 - resolution: "@typescript-eslint/eslint-plugin@npm:5.33.0" +"@typescript-eslint/eslint-plugin@npm:^5.33.1": + version: 5.33.1 + resolution: "@typescript-eslint/eslint-plugin@npm:5.33.1" dependencies: - "@typescript-eslint/scope-manager": 5.33.0 - "@typescript-eslint/type-utils": 5.33.0 - "@typescript-eslint/utils": 5.33.0 + "@typescript-eslint/scope-manager": 5.33.1 + "@typescript-eslint/type-utils": 5.33.1 + "@typescript-eslint/utils": 5.33.1 debug: ^4.3.4 functional-red-black-tree: ^1.0.1 ignore: ^5.2.0 @@ -1156,42 +1172,42 @@ __metadata: peerDependenciesMeta: typescript: optional: true - checksum: d408f3f474b34fefde8ee65d98deb126949fd7d8e211a7f95c5cc2b507dedbf8eb239f3895e0c37aa6338989531e37c5f35c2e0de36a126c52f0846e89605487 + checksum: d9b6b038f70e4959ad211c84f50a38de2d00b54f0636ad76eea414fb070fa616933690da80de6668e62c8fbbeb227086322001b7d7ad1924421a232547c97936 languageName: node linkType: hard -"@typescript-eslint/parser@npm:^5.33.0": - version: 5.33.0 - resolution: "@typescript-eslint/parser@npm:5.33.0" +"@typescript-eslint/parser@npm:^5.33.1": + version: 5.33.1 + resolution: "@typescript-eslint/parser@npm:5.33.1" dependencies: - "@typescript-eslint/scope-manager": 5.33.0 - "@typescript-eslint/types": 5.33.0 - "@typescript-eslint/typescript-estree": 5.33.0 + "@typescript-eslint/scope-manager": 5.33.1 + "@typescript-eslint/types": 5.33.1 + "@typescript-eslint/typescript-estree": 5.33.1 debug: ^4.3.4 peerDependencies: eslint: ^6.0.0 || ^7.0.0 || ^8.0.0 peerDependenciesMeta: typescript: optional: true - checksum: 2617aba987a70ee6b16ecc6afa6d245422df33a9d056018ff2e316159e667a0ab9d9c15fcea95e0ba65832661e71cc2753a221e77f0b0fab278e52c4497b8278 + checksum: fb3a4e000ce6d9583656fc3b3fb80f127a0ec1b7c3872ea469164516d993a588859ded4ec1338e6bbf2151168380d8aa29ec31027af23b50f5107949f8e7b438 languageName: node linkType: hard -"@typescript-eslint/scope-manager@npm:5.33.0": - version: 5.33.0 - resolution: "@typescript-eslint/scope-manager@npm:5.33.0" +"@typescript-eslint/scope-manager@npm:5.33.1": + version: 5.33.1 + resolution: "@typescript-eslint/scope-manager@npm:5.33.1" dependencies: - "@typescript-eslint/types": 5.33.0 - "@typescript-eslint/visitor-keys": 5.33.0 - checksum: b2cbea9abd528d01a5acb2d68a2a5be51ec6827760d3869bdd70920cf6c3a4f9f96d87c77177f8313009d9db71253e4a75f8393f38651e2abaf91ef28e60fb9d + "@typescript-eslint/types": 5.33.1 + "@typescript-eslint/visitor-keys": 5.33.1 + checksum: b9918d8320ea59081d19070ce952b56984e72fb2c113215e5e6a0f97deac9aae5aa67ec7a07cddb010c0f75cdf8df096ab45e9241e4b7b611acfa6d4cdfb6516 languageName: node linkType: hard -"@typescript-eslint/type-utils@npm:5.33.0": - version: 5.33.0 - resolution: "@typescript-eslint/type-utils@npm:5.33.0" +"@typescript-eslint/type-utils@npm:5.33.1": + version: 5.33.1 + resolution: "@typescript-eslint/type-utils@npm:5.33.1" dependencies: - "@typescript-eslint/utils": 5.33.0 + "@typescript-eslint/utils": 5.33.1 debug: ^4.3.4 tsutils: ^3.21.0 peerDependencies: @@ -1199,23 +1215,23 @@ __metadata: peerDependenciesMeta: typescript: optional: true - checksum: a1d1ffb42fe96bfc2339cc2875e218aa82fa9391be04c1a266bb11da1eca6835555687e81cde75477c60e6702049cd4dde7d2638e7e9b9d8cf4b7b2242353a6e + checksum: ddf88835bc87b3ad946aaeb29b770a49a8e1c3c5e294ee9cb93b1936f432a1016efb97803f197eea1be61545cbc79b5526cc05e9339ca9beada22fc83801ddea languageName: node linkType: hard -"@typescript-eslint/types@npm:5.33.0": - version: 5.33.0 - resolution: "@typescript-eslint/types@npm:5.33.0" - checksum: 8bbddda84cb3adf5c659b0d42547a2d6ab87f4eea574aca5dd63a3bd85169f32796ecbddad3b27f18a609070f6b1d18a54018d488bad746ae0f6ea5c02206109 +"@typescript-eslint/types@npm:5.33.1": + version: 5.33.1 + resolution: "@typescript-eslint/types@npm:5.33.1" + checksum: 122891bd4ab4b930b1d33f3ce43a010825c1e61b9879520a0f3dc34cf92df71e2a873410845ab8d746333511c455c115eaafdec149298a161cef713829dfdb77 languageName: node linkType: hard -"@typescript-eslint/typescript-estree@npm:5.33.0": - version: 5.33.0 - resolution: "@typescript-eslint/typescript-estree@npm:5.33.0" +"@typescript-eslint/typescript-estree@npm:5.33.1": + version: 5.33.1 + resolution: "@typescript-eslint/typescript-estree@npm:5.33.1" dependencies: - "@typescript-eslint/types": 5.33.0 - "@typescript-eslint/visitor-keys": 5.33.0 + "@typescript-eslint/types": 5.33.1 + "@typescript-eslint/visitor-keys": 5.33.1 debug: ^4.3.4 globby: ^11.1.0 is-glob: ^4.0.3 @@ -1224,33 +1240,33 @@ __metadata: peerDependenciesMeta: typescript: optional: true - checksum: 26f9005cdfb14654125a33d90d872b926820e560dff8970c4629fd5f6f47ad2a31e4c63161564d21bb42a8fc3ced0033994854ee37336ae07d90ccf6300d702b + checksum: 1418e409b141c2f012bc2dd5c40d95dfd8aa572dd3e9523ed23e4371e4459d10ecd074fda75dc770ce980686b25ffc44725eebf165c494818ed4131d1ac0239f languageName: node linkType: hard -"@typescript-eslint/utils@npm:5.33.0": - version: 5.33.0 - resolution: "@typescript-eslint/utils@npm:5.33.0" +"@typescript-eslint/utils@npm:5.33.1": + version: 5.33.1 + resolution: "@typescript-eslint/utils@npm:5.33.1" dependencies: "@types/json-schema": ^7.0.9 - "@typescript-eslint/scope-manager": 5.33.0 - "@typescript-eslint/types": 5.33.0 - "@typescript-eslint/typescript-estree": 5.33.0 + "@typescript-eslint/scope-manager": 5.33.1 + "@typescript-eslint/types": 5.33.1 + "@typescript-eslint/typescript-estree": 5.33.1 eslint-scope: ^5.1.1 eslint-utils: ^3.0.0 peerDependencies: eslint: ^6.0.0 || ^7.0.0 || ^8.0.0 - checksum: 6ce5ee5eabeb6d73538b24e6487f811ecb0ef3467bd366cbd15bf30d904bdedb73fc6f48cf2e2e742dda462b42999ea505e8b59255545825ec9db86f3d423ea7 + checksum: c550504d62fc72f29bf3d7a651bd3a81f49fb1fccaf47583721c2ab1abd2ef78a1e4bc392cb4be4a61a45a4f24fc14a59d67b98aac8a16a207a7cace86538cab languageName: node linkType: hard -"@typescript-eslint/visitor-keys@npm:5.33.0": - version: 5.33.0 - resolution: "@typescript-eslint/visitor-keys@npm:5.33.0" +"@typescript-eslint/visitor-keys@npm:5.33.1": + version: 5.33.1 + resolution: "@typescript-eslint/visitor-keys@npm:5.33.1" dependencies: - "@typescript-eslint/types": 5.33.0 + "@typescript-eslint/types": 5.33.1 eslint-visitor-keys: ^3.3.0 - checksum: d7e3653de6bac6841e6fcc54226b93ad6bdca4aa76ebe7d83459c016c3eebcc50d4f65ee713174bc267d765295b642d1927a778c5de707b8389e3fcc052aa4a1 + checksum: 0d32a433450f61e97b5fa6b1e167f06ed395c200b16b4dbd4490a1c4941de420689b622f8a2486f5398806fb24f57b9fab901b4cbc8fdb8853f568264b3a182a languageName: node linkType: hard @@ -4705,9 +4721,9 @@ __metadata: languageName: node linkType: hard -"vite@npm:^3.0.7": - version: 3.0.7 - resolution: "vite@npm:3.0.7" +"vite@npm:^3.0.8": + version: 3.0.8 + resolution: "vite@npm:3.0.8" dependencies: esbuild: ^0.14.47 fsevents: ~2.3.2 @@ -4733,7 +4749,7 @@ __metadata: optional: true bin: vite: bin/vite.js - checksum: 3cdcb68e16433b9addee61c117c379692e4d12d44ee7011da99ed9516b8c37ce76f1bb1728eab0e3a28957c9d2dc839024e1607c2ad03f38089e3be55d7dd456 + checksum: ec3f57d52f2bf28f2f89898053c2156f025a108a95e9308ce6580f43d8fdaae866f7988afa8207a8c8509069d3a0b50ee79b9a8050590a825f4b7771646c2755 languageName: node linkType: hard -- cgit v1.2.3-54-g00ecf