diff options
Diffstat (limited to 'subprojects/frontend/src')
38 files changed, 649 insertions, 589 deletions
diff --git a/subprojects/frontend/src/App.tsx b/subprojects/frontend/src/App.tsx index 54f92f9a..d3ec63eb 100644 --- a/subprojects/frontend/src/App.tsx +++ b/subprojects/frontend/src/App.tsx | |||
@@ -1,26 +1,19 @@ | |||
1 | import MenuIcon from '@mui/icons-material/Menu'; | ||
1 | import AppBar from '@mui/material/AppBar'; | 2 | import AppBar from '@mui/material/AppBar'; |
2 | import Box from '@mui/material/Box'; | 3 | import Box from '@mui/material/Box'; |
3 | import IconButton from '@mui/material/IconButton'; | 4 | import IconButton from '@mui/material/IconButton'; |
4 | import Toolbar from '@mui/material/Toolbar'; | 5 | import Toolbar from '@mui/material/Toolbar'; |
5 | import Typography from '@mui/material/Typography'; | 6 | import Typography from '@mui/material/Typography'; |
6 | import MenuIcon from '@mui/icons-material/Menu'; | ||
7 | import React from 'react'; | 7 | import React from 'react'; |
8 | 8 | ||
9 | import { EditorArea } from './editor/EditorArea'; | 9 | import EditorArea from './editor/EditorArea'; |
10 | import { EditorButtons } from './editor/EditorButtons'; | 10 | import EditorButtons from './editor/EditorButtons'; |
11 | import { GenerateButton } from './editor/GenerateButton'; | 11 | import GenerateButton from './editor/GenerateButton'; |
12 | 12 | ||
13 | export function App(): JSX.Element { | 13 | export default function App(): JSX.Element { |
14 | return ( | 14 | return ( |
15 | <Box | 15 | <Box display="flex" flexDirection="column" sx={{ height: '100vh' }}> |
16 | display="flex" | 16 | <AppBar position="static" color="inherit"> |
17 | flexDirection="column" | ||
18 | sx={{ height: '100vh' }} | ||
19 | > | ||
20 | <AppBar | ||
21 | position="static" | ||
22 | color="inherit" | ||
23 | > | ||
24 | <Toolbar> | 17 | <Toolbar> |
25 | <IconButton | 18 | <IconButton |
26 | edge="start" | 19 | edge="start" |
@@ -30,11 +23,7 @@ export function App(): JSX.Element { | |||
30 | > | 23 | > |
31 | <MenuIcon /> | 24 | <MenuIcon /> |
32 | </IconButton> | 25 | </IconButton> |
33 | <Typography | 26 | <Typography variant="h6" component="h1" flexGrow={1}> |
34 | variant="h6" | ||
35 | component="h1" | ||
36 | flexGrow={1} | ||
37 | > | ||
38 | Refinery | 27 | Refinery |
39 | </Typography> | 28 | </Typography> |
40 | </Toolbar> | 29 | </Toolbar> |
@@ -48,11 +37,7 @@ export function App(): JSX.Element { | |||
48 | <EditorButtons /> | 37 | <EditorButtons /> |
49 | <GenerateButton /> | 38 | <GenerateButton /> |
50 | </Box> | 39 | </Box> |
51 | <Box | 40 | <Box flexGrow={1} flexShrink={1} sx={{ overflow: 'auto' }}> |
52 | flexGrow={1} | ||
53 | flexShrink={1} | ||
54 | sx={{ overflow: 'auto' }} | ||
55 | > | ||
56 | <EditorArea /> | 41 | <EditorArea /> |
57 | </Box> | 42 | </Box> |
58 | </Box> | 43 | </Box> |
diff --git a/subprojects/frontend/src/Loading.tsx b/subprojects/frontend/src/Loading.tsx new file mode 100644 index 00000000..a699adca --- /dev/null +++ b/subprojects/frontend/src/Loading.tsx | |||
@@ -0,0 +1,19 @@ | |||
1 | import CircularProgress from '@mui/material/CircularProgress'; | ||
2 | import { styled } from '@mui/material/styles'; | ||
3 | import React from 'react'; | ||
4 | |||
5 | const LoadingRoot = styled('div')({ | ||
6 | width: '100vw', | ||
7 | height: '100vh', | ||
8 | display: 'flex', | ||
9 | alignItems: 'center', | ||
10 | justifyContent: 'center', | ||
11 | }); | ||
12 | |||
13 | export default function Loading() { | ||
14 | return ( | ||
15 | <LoadingRoot> | ||
16 | <CircularProgress /> | ||
17 | </LoadingRoot> | ||
18 | ); | ||
19 | } | ||
diff --git a/subprojects/frontend/src/RootStore.tsx b/subprojects/frontend/src/RootStore.tsx index baf0b61e..a7406d7b 100644 --- a/subprojects/frontend/src/RootStore.tsx +++ b/subprojects/frontend/src/RootStore.tsx | |||
@@ -1,9 +1,9 @@ | |||
1 | import React, { createContext, useContext } from 'react'; | 1 | import React, { createContext, useContext } from 'react'; |
2 | 2 | ||
3 | import { EditorStore } from './editor/EditorStore'; | 3 | import EditorStore from './editor/EditorStore'; |
4 | import { ThemeStore } from './theme/ThemeStore'; | 4 | import ThemeStore from './theme/ThemeStore'; |
5 | 5 | ||
6 | export class RootStore { | 6 | export default class RootStore { |
7 | editorStore; | 7 | editorStore; |
8 | 8 | ||
9 | themeStore; | 9 | themeStore; |
@@ -22,11 +22,12 @@ export interface RootStoreProviderProps { | |||
22 | rootStore: RootStore; | 22 | rootStore: RootStore; |
23 | } | 23 | } |
24 | 24 | ||
25 | export function RootStoreProvider({ children, rootStore }: RootStoreProviderProps): JSX.Element { | 25 | export function RootStoreProvider({ |
26 | children, | ||
27 | rootStore, | ||
28 | }: RootStoreProviderProps): JSX.Element { | ||
26 | return ( | 29 | return ( |
27 | <StoreContext.Provider value={rootStore}> | 30 | <StoreContext.Provider value={rootStore}>{children}</StoreContext.Provider> |
28 | {children} | ||
29 | </StoreContext.Provider> | ||
30 | ); | 31 | ); |
31 | } | 32 | } |
32 | 33 | ||
diff --git a/subprojects/frontend/src/editor/EditorArea.tsx b/subprojects/frontend/src/editor/EditorArea.tsx index dba20f6e..14294371 100644 --- a/subprojects/frontend/src/editor/EditorArea.tsx +++ b/subprojects/frontend/src/editor/EditorArea.tsx | |||
@@ -1,17 +1,13 @@ | |||
1 | import { Command, EditorView } from '@codemirror/view'; | ||
2 | import { closeSearchPanel, openSearchPanel } from '@codemirror/search'; | ||
3 | import { closeLintPanel, openLintPanel } from '@codemirror/lint'; | 1 | import { closeLintPanel, openLintPanel } from '@codemirror/lint'; |
2 | import { closeSearchPanel, openSearchPanel } from '@codemirror/search'; | ||
3 | import { type Command, EditorView } from '@codemirror/view'; | ||
4 | import { observer } from 'mobx-react-lite'; | 4 | import { observer } from 'mobx-react-lite'; |
5 | import React, { | 5 | import React, { useCallback, useEffect, useRef, useState } from 'react'; |
6 | useCallback, | ||
7 | useEffect, | ||
8 | useRef, | ||
9 | useState, | ||
10 | } from 'react'; | ||
11 | 6 | ||
12 | import { EditorParent } from './EditorParent'; | ||
13 | import { useRootStore } from '../RootStore'; | 7 | import { useRootStore } from '../RootStore'; |
14 | import { getLogger } from '../utils/logger'; | 8 | import getLogger from '../utils/getLogger'; |
9 | |||
10 | import EditorParent from './EditorParent'; | ||
15 | 11 | ||
16 | const log = getLogger('editor.EditorArea'); | 12 | const log = getLogger('editor.EditorArea'); |
17 | 13 | ||
@@ -70,10 +66,12 @@ function fixCodeMirrorAccessibility(editorView: EditorView) { | |||
70 | contentDOM.setAttribute('aria-label', 'Code editor'); | 66 | contentDOM.setAttribute('aria-label', 'Code editor'); |
71 | } | 67 | } |
72 | 68 | ||
73 | export const EditorArea = observer(() => { | 69 | function EditorArea(): JSX.Element { |
74 | const { editorStore } = useRootStore(); | 70 | const { editorStore } = useRootStore(); |
75 | const editorParentRef = useRef<HTMLDivElement | null>(null); | 71 | const editorParentRef = useRef<HTMLDivElement | null>(null); |
76 | const [editorViewState, setEditorViewState] = useState<EditorView | null>(null); | 72 | const [editorViewState, setEditorViewState] = useState<EditorView | null>( |
73 | null, | ||
74 | ); | ||
77 | 75 | ||
78 | const setSearchPanelOpen = usePanel( | 76 | const setSearchPanelOpen = usePanel( |
79 | 'search', | 77 | 'search', |
@@ -131,22 +129,21 @@ export const EditorArea = observer(() => { | |||
131 | editorView.destroy(); | 129 | editorView.destroy(); |
132 | log.info('Editor destroyed'); | 130 | log.info('Editor destroyed'); |
133 | }; | 131 | }; |
134 | }, [ | 132 | }, [editorStore, setSearchPanelOpen, setLintPanelOpen]); |
135 | editorParentRef, | ||
136 | editorStore, | ||
137 | setSearchPanelOpen, | ||
138 | setLintPanelOpen, | ||
139 | ]); | ||
140 | 133 | ||
141 | return ( | 134 | return ( |
142 | <EditorParent | 135 | <EditorParent |
143 | className="dark" | 136 | className="dark" |
144 | sx={{ | 137 | sx={{ |
145 | '.cm-lineNumbers': editorStore.showLineNumbers ? {} : { | 138 | '.cm-lineNumbers': editorStore.showLineNumbers |
146 | display: 'none !important', | 139 | ? {} |
147 | }, | 140 | : { |
141 | display: 'none !important', | ||
142 | }, | ||
148 | }} | 143 | }} |
149 | ref={editorParentRef} | 144 | ref={editorParentRef} |
150 | /> | 145 | /> |
151 | ); | 146 | ); |
152 | }); | 147 | } |
148 | |||
149 | export default observer(EditorArea); | ||
diff --git a/subprojects/frontend/src/editor/EditorButtons.tsx b/subprojects/frontend/src/editor/EditorButtons.tsx index 150aa00d..652ca71e 100644 --- a/subprojects/frontend/src/editor/EditorButtons.tsx +++ b/subprojects/frontend/src/editor/EditorButtons.tsx | |||
@@ -1,9 +1,4 @@ | |||
1 | import type { Diagnostic } from '@codemirror/lint'; | 1 | import type { Diagnostic } from '@codemirror/lint'; |
2 | import { observer } from 'mobx-react-lite'; | ||
3 | import IconButton from '@mui/material/IconButton'; | ||
4 | import Stack from '@mui/material/Stack'; | ||
5 | import ToggleButton from '@mui/material/ToggleButton'; | ||
6 | import ToggleButtonGroup from '@mui/material/ToggleButtonGroup'; | ||
7 | import CheckIcon from '@mui/icons-material/Check'; | 2 | import CheckIcon from '@mui/icons-material/Check'; |
8 | import ErrorIcon from '@mui/icons-material/Error'; | 3 | import ErrorIcon from '@mui/icons-material/Error'; |
9 | import FormatListNumberedIcon from '@mui/icons-material/FormatListNumbered'; | 4 | import FormatListNumberedIcon from '@mui/icons-material/FormatListNumbered'; |
@@ -13,6 +8,11 @@ import RedoIcon from '@mui/icons-material/Redo'; | |||
13 | import SearchIcon from '@mui/icons-material/Search'; | 8 | import SearchIcon from '@mui/icons-material/Search'; |
14 | import UndoIcon from '@mui/icons-material/Undo'; | 9 | import UndoIcon from '@mui/icons-material/Undo'; |
15 | import WarningIcon from '@mui/icons-material/Warning'; | 10 | import WarningIcon from '@mui/icons-material/Warning'; |
11 | import IconButton from '@mui/material/IconButton'; | ||
12 | import Stack from '@mui/material/Stack'; | ||
13 | import ToggleButton from '@mui/material/ToggleButton'; | ||
14 | import ToggleButtonGroup from '@mui/material/ToggleButtonGroup'; | ||
15 | import { observer } from 'mobx-react-lite'; | ||
16 | import React from 'react'; | 16 | import React from 'react'; |
17 | 17 | ||
18 | import { useRootStore } from '../RootStore'; | 18 | import { useRootStore } from '../RootStore'; |
@@ -27,23 +27,17 @@ function getLintIcon(severity: Diagnostic['severity'] | null) { | |||
27 | return <WarningIcon fontSize="small" />; | 27 | return <WarningIcon fontSize="small" />; |
28 | case 'info': | 28 | case 'info': |
29 | return <InfoOutlinedIcon fontSize="small" />; | 29 | return <InfoOutlinedIcon fontSize="small" />; |
30 | case null: | 30 | default: |
31 | return <CheckIcon fontSize="small" />; | 31 | return <CheckIcon fontSize="small" />; |
32 | } | 32 | } |
33 | } | 33 | } |
34 | 34 | ||
35 | export const EditorButtons = observer(() => { | 35 | function EditorButtons(): JSX.Element { |
36 | const { editorStore } = useRootStore(); | 36 | const { editorStore } = useRootStore(); |
37 | 37 | ||
38 | return ( | 38 | return ( |
39 | <Stack | 39 | <Stack direction="row" spacing={1}> |
40 | direction="row" | 40 | <Stack direction="row" alignItems="center"> |
41 | spacing={1} | ||
42 | > | ||
43 | <Stack | ||
44 | direction="row" | ||
45 | alignItems="center" | ||
46 | > | ||
47 | <IconButton | 41 | <IconButton |
48 | disabled={!editorStore.canUndo} | 42 | disabled={!editorStore.canUndo} |
49 | onClick={() => editorStore.undo()} | 43 | onClick={() => editorStore.undo()} |
@@ -59,9 +53,7 @@ export const EditorButtons = observer(() => { | |||
59 | <RedoIcon fontSize="small" /> | 53 | <RedoIcon fontSize="small" /> |
60 | </IconButton> | 54 | </IconButton> |
61 | </Stack> | 55 | </Stack> |
62 | <ToggleButtonGroup | 56 | <ToggleButtonGroup size="small"> |
63 | size="small" | ||
64 | > | ||
65 | <ToggleButton | 57 | <ToggleButton |
66 | selected={editorStore.showLineNumbers} | 58 | selected={editorStore.showLineNumbers} |
67 | onClick={() => editorStore.toggleLineNumbers()} | 59 | onClick={() => editorStore.toggleLineNumbers()} |
@@ -95,4 +87,6 @@ export const EditorButtons = observer(() => { | |||
95 | </IconButton> | 87 | </IconButton> |
96 | </Stack> | 88 | </Stack> |
97 | ); | 89 | ); |
98 | }); | 90 | } |
91 | |||
92 | export default observer(EditorButtons); | ||
diff --git a/subprojects/frontend/src/editor/EditorParent.ts b/subprojects/frontend/src/editor/EditorParent.ts index 9aaf541a..dbc35a0d 100644 --- a/subprojects/frontend/src/editor/EditorParent.ts +++ b/subprojects/frontend/src/editor/EditorParent.ts | |||
@@ -1,4 +1,4 @@ | |||
1 | import { styled } from '@mui/material/styles'; | 1 | import { alpha, styled } from '@mui/material/styles'; |
2 | 2 | ||
3 | /** | 3 | /** |
4 | * Returns a squiggly underline background image encoded as a CSS `url()` data URI with Base64. | 4 | * Returns a squiggly underline background image encoded as a CSS `url()` data URI with Base64. |
@@ -17,7 +17,9 @@ function underline(color: string) { | |||
17 | return `url('data:image/svg+xml;base64,${svgBase64}')`; | 17 | return `url('data:image/svg+xml;base64,${svgBase64}')`; |
18 | } | 18 | } |
19 | 19 | ||
20 | export const EditorParent = styled('div')(({ theme }) => { | 20 | export default styled('div', { |
21 | name: 'EditorParent', | ||
22 | })(({ theme }) => { | ||
21 | const codeMirrorLintStyle: Record<string, unknown> = {}; | 23 | const codeMirrorLintStyle: Record<string, unknown> = {}; |
22 | (['error', 'warning', 'info'] as const).forEach((severity) => { | 24 | (['error', 'warning', 'info'] as const).forEach((severity) => { |
23 | const color = theme.palette[severity].main; | 25 | const color = theme.palette[severity].main; |
@@ -37,19 +39,20 @@ export const EditorParent = styled('div')(({ theme }) => { | |||
37 | '.cm-content': { | 39 | '.cm-content': { |
38 | padding: 0, | 40 | padding: 0, |
39 | }, | 41 | }, |
40 | '.cm-scroller, .cm-tooltip-autocomplete, .cm-completionLabel, .cm-completionDetail': { | 42 | '.cm-scroller, .cm-tooltip-autocomplete, .cm-completionLabel, .cm-completionDetail': |
41 | fontSize: 16, | 43 | { |
42 | fontFamily: '"JetBrains MonoVariable", "JetBrains Mono", monospace', | 44 | fontSize: 16, |
43 | fontFeatureSettings: '"liga", "calt"', | 45 | fontFamily: '"JetBrains MonoVariable", "JetBrains Mono", monospace', |
44 | fontWeight: 400, | 46 | fontFeatureSettings: '"liga", "calt"', |
45 | letterSpacing: 0, | 47 | fontWeight: 400, |
46 | textRendering: 'optimizeLegibility', | 48 | letterSpacing: 0, |
47 | }, | 49 | textRendering: 'optimizeLegibility', |
50 | }, | ||
48 | '.cm-scroller': { | 51 | '.cm-scroller': { |
49 | color: theme.palette.text.secondary, | 52 | color: theme.palette.text.secondary, |
50 | }, | 53 | }, |
51 | '.cm-gutters': { | 54 | '.cm-gutters': { |
52 | background: 'rgba(255, 255, 255, 0.1)', | 55 | background: 'transparent', |
53 | color: theme.palette.text.disabled, | 56 | color: theme.palette.text.disabled, |
54 | border: 'none', | 57 | border: 'none', |
55 | }, | 58 | }, |
@@ -57,7 +60,19 @@ export const EditorParent = styled('div')(({ theme }) => { | |||
57 | color: theme.palette.secondary.main, | 60 | color: theme.palette.secondary.main, |
58 | }, | 61 | }, |
59 | '.cm-activeLine': { | 62 | '.cm-activeLine': { |
60 | background: 'rgba(0, 0, 0, 0.3)', | 63 | background: alpha(theme.palette.text.secondary, 0.06), |
64 | }, | ||
65 | '.cm-foldGutter': { | ||
66 | color: alpha(theme.palette.text.primary, 0), | ||
67 | transition: theme.transitions.create('color', { | ||
68 | duration: theme.transitions.duration.short, | ||
69 | }), | ||
70 | '@media (hover: none)': { | ||
71 | color: theme.palette.text.primary, | ||
72 | }, | ||
73 | }, | ||
74 | '.cm-gutters:hover .cm-foldGutter': { | ||
75 | color: theme.palette.text.primary, | ||
61 | }, | 76 | }, |
62 | '.cm-activeLineGutter': { | 77 | '.cm-activeLineGutter': { |
63 | background: 'transparent', | 78 | background: 'transparent', |
@@ -66,8 +81,7 @@ export const EditorParent = styled('div')(({ theme }) => { | |||
66 | color: theme.palette.text.primary, | 81 | color: theme.palette.text.primary, |
67 | }, | 82 | }, |
68 | '.cm-cursor, .cm-cursor-primary': { | 83 | '.cm-cursor, .cm-cursor-primary': { |
69 | borderColor: theme.palette.primary.main, | 84 | borderLeft: `2px solid ${theme.palette.primary.main}`, |
70 | background: theme.palette.common.black, | ||
71 | }, | 85 | }, |
72 | '.cm-selectionBackground': { | 86 | '.cm-selectionBackground': { |
73 | background: '#3e4453', | 87 | background: '#3e4453', |
@@ -115,9 +129,26 @@ export const EditorParent = styled('div')(({ theme }) => { | |||
115 | }, | 129 | }, |
116 | }, | 130 | }, |
117 | '.cm-foldPlaceholder': { | 131 | '.cm-foldPlaceholder': { |
118 | background: theme.palette.background.paper, | ||
119 | borderColor: theme.palette.text.disabled, | ||
120 | color: theme.palette.text.secondary, | 132 | color: theme.palette.text.secondary, |
133 | backgroundColor: alpha(theme.palette.text.secondary, 0), | ||
134 | border: `1px solid ${alpha(theme.palette.text.secondary, 0.5)}`, | ||
135 | borderRadius: theme.shape.borderRadius, | ||
136 | transition: theme.transitions.create( | ||
137 | ['background-color', 'border-color', 'color'], | ||
138 | { | ||
139 | duration: theme.transitions.duration.short, | ||
140 | }, | ||
141 | ), | ||
142 | '&:hover': { | ||
143 | backgroundColor: alpha( | ||
144 | theme.palette.text.secondary, | ||
145 | theme.palette.action.hoverOpacity, | ||
146 | ), | ||
147 | borderColor: theme.palette.text.secondary, | ||
148 | '@media (hover: none)': { | ||
149 | backgroundColor: 'transparent', | ||
150 | }, | ||
151 | }, | ||
121 | }, | 152 | }, |
122 | '.tok-comment': { | 153 | '.tok-comment': { |
123 | fontStyle: 'italic', | 154 | fontStyle: 'italic', |
@@ -168,9 +199,14 @@ export const EditorParent = styled('div')(({ theme }) => { | |||
168 | }, | 199 | }, |
169 | '.cm-tooltip-autocomplete': { | 200 | '.cm-tooltip-autocomplete': { |
170 | background: theme.palette.background.paper, | 201 | background: theme.palette.background.paper, |
171 | boxShadow: `0px 2px 4px -1px rgb(0 0 0 / 20%), | 202 | ...(theme.palette.mode === 'dark' && { |
172 | 0px 4px 5px 0px rgb(0 0 0 / 14%), | 203 | overflow: 'hidden', |
173 | 0px 1px 10px 0px rgb(0 0 0 / 12%)`, | 204 | borderRadius: theme.shape.borderRadius, |
205 | // https://github.com/mui/material-ui/blob/10c72729c7d03bab8cdce6eb422642684c56dca2/packages/mui-material/src/Paper/Paper.js#L18 | ||
206 | backgroundImage: | ||
207 | 'linear-gradient(rgba(255, 255, 255, 0.09), rgba(255, 255, 255, 0.09))', | ||
208 | }), | ||
209 | boxShadow: theme.shadows[4], | ||
174 | '.cm-completionIcon': { | 210 | '.cm-completionIcon': { |
175 | color: theme.palette.text.secondary, | 211 | color: theme.palette.text.secondary, |
176 | }, | 212 | }, |
diff --git a/subprojects/frontend/src/editor/EditorStore.ts b/subprojects/frontend/src/editor/EditorStore.ts index 0f4d2936..f75147a4 100644 --- a/subprojects/frontend/src/editor/EditorStore.ts +++ b/subprojects/frontend/src/editor/EditorStore.ts | |||
@@ -21,18 +21,14 @@ import { | |||
21 | indentOnInput, | 21 | indentOnInput, |
22 | syntaxHighlighting, | 22 | syntaxHighlighting, |
23 | } from '@codemirror/language'; | 23 | } from '@codemirror/language'; |
24 | import { | 24 | import { type Diagnostic, lintKeymap, setDiagnostics } from '@codemirror/lint'; |
25 | Diagnostic, | ||
26 | lintKeymap, | ||
27 | setDiagnostics, | ||
28 | } from '@codemirror/lint'; | ||
29 | import { search, searchKeymap } from '@codemirror/search'; | 25 | import { search, searchKeymap } from '@codemirror/search'; |
30 | import { | 26 | import { |
31 | EditorState, | 27 | EditorState, |
32 | StateCommand, | 28 | type StateCommand, |
33 | StateEffect, | 29 | StateEffect, |
34 | Transaction, | 30 | type Transaction, |
35 | TransactionSpec, | 31 | type TransactionSpec, |
36 | } from '@codemirror/state'; | 32 | } from '@codemirror/state'; |
37 | import { | 33 | import { |
38 | drawSelection, | 34 | drawSelection, |
@@ -45,26 +41,25 @@ import { | |||
45 | rectangularSelection, | 41 | rectangularSelection, |
46 | } from '@codemirror/view'; | 42 | } from '@codemirror/view'; |
47 | import { classHighlighter } from '@lezer/highlight'; | 43 | import { classHighlighter } from '@lezer/highlight'; |
48 | import { | 44 | import { makeAutoObservable, observable, reaction } from 'mobx'; |
49 | makeAutoObservable, | 45 | |
50 | observable, | 46 | import problemLanguageSupport from '../language/problemLanguageSupport'; |
51 | reaction, | 47 | import type ThemeStore from '../theme/ThemeStore'; |
52 | } from 'mobx'; | 48 | import getLogger from '../utils/getLogger'; |
53 | 49 | import XtextClient from '../xtext/XtextClient'; | |
54 | import { findOccurrences, IOccurrence, setOccurrences } from './findOccurrences'; | 50 | |
55 | import { problemLanguageSupport } from '../language/problemLanguageSupport'; | 51 | import findOccurrences, { |
56 | import { | 52 | type IOccurrence, |
57 | IHighlightRange, | 53 | setOccurrences, |
58 | semanticHighlighting, | 54 | } from './findOccurrences'; |
55 | import semanticHighlighting, { | ||
56 | type IHighlightRange, | ||
59 | setSemanticHighlighting, | 57 | setSemanticHighlighting, |
60 | } from './semanticHighlighting'; | 58 | } from './semanticHighlighting'; |
61 | import type { ThemeStore } from '../theme/ThemeStore'; | ||
62 | import { getLogger } from '../utils/logger'; | ||
63 | import { XtextClient } from '../xtext/XtextClient'; | ||
64 | 59 | ||
65 | const log = getLogger('editor.EditorStore'); | 60 | const log = getLogger('editor.EditorStore'); |
66 | 61 | ||
67 | export class EditorStore { | 62 | export default class EditorStore { |
68 | private readonly themeStore; | 63 | private readonly themeStore; |
69 | 64 | ||
70 | state: EditorState; | 65 | state: EditorState; |
@@ -96,17 +91,18 @@ export class EditorStore { | |||
96 | extensions: [ | 91 | extensions: [ |
97 | autocompletion({ | 92 | autocompletion({ |
98 | activateOnTyping: true, | 93 | activateOnTyping: true, |
99 | override: [ | 94 | override: [(context) => this.client.contentAssist(context)], |
100 | (context) => this.client.contentAssist(context), | ||
101 | ], | ||
102 | }), | 95 | }), |
103 | closeBrackets(), | 96 | closeBrackets(), |
104 | bracketMatching(), | 97 | bracketMatching(), |
105 | drawSelection(), | 98 | drawSelection(), |
106 | EditorState.allowMultipleSelections.of(true), | 99 | EditorState.allowMultipleSelections.of(true), |
107 | EditorView.theme({}, { | 100 | EditorView.theme( |
108 | dark: this.themeStore.darkMode, | 101 | {}, |
109 | }), | 102 | { |
103 | dark: this.themeStore.darkMode, | ||
104 | }, | ||
105 | ), | ||
110 | findOccurrences, | 106 | findOccurrences, |
111 | highlightActiveLine(), | 107 | highlightActiveLine(), |
112 | highlightActiveLineGutter(), | 108 | highlightActiveLineGutter(), |
@@ -134,8 +130,16 @@ export class EditorStore { | |||
134 | { key: 'Mod-Shift-m', run: () => this.setLintPanelOpen(true) }, | 130 | { key: 'Mod-Shift-m', run: () => this.setLintPanelOpen(true) }, |
135 | ...lintKeymap, | 131 | ...lintKeymap, |
136 | // Override keys in `searchKeymap` to go through the `EditorStore`. | 132 | // Override keys in `searchKeymap` to go through the `EditorStore`. |
137 | { key: 'Mod-f', run: () => this.setSearchPanelOpen(true), scope: 'editor search-panel' }, | 133 | { |
138 | { key: 'Escape', run: () => this.setSearchPanelOpen(false), scope: 'editor search-panel' }, | 134 | key: 'Mod-f', |
135 | run: () => this.setSearchPanelOpen(true), | ||
136 | scope: 'editor search-panel', | ||
137 | }, | ||
138 | { | ||
139 | key: 'Escape', | ||
140 | run: () => this.setSearchPanelOpen(false), | ||
141 | scope: 'editor search-panel', | ||
142 | }, | ||
139 | ...searchKeymap, | 143 | ...searchKeymap, |
140 | ...defaultKeymap, | 144 | ...defaultKeymap, |
141 | ]), | 145 | ]), |
@@ -149,9 +153,14 @@ export class EditorStore { | |||
149 | log.debug('Update editor dark mode', darkMode); | 153 | log.debug('Update editor dark mode', darkMode); |
150 | this.dispatch({ | 154 | this.dispatch({ |
151 | effects: [ | 155 | effects: [ |
152 | StateEffect.appendConfig.of(EditorView.theme({}, { | 156 | StateEffect.appendConfig.of( |
153 | dark: darkMode, | 157 | EditorView.theme( |
154 | })), | 158 | {}, |
159 | { | ||
160 | dark: darkMode, | ||
161 | }, | ||
162 | ), | ||
163 | ), | ||
155 | ], | 164 | ], |
156 | }); | 165 | }); |
157 | }, | 166 | }, |
@@ -198,6 +207,8 @@ export class EditorStore { | |||
198 | case 'info': | 207 | case 'info': |
199 | this.infoCount += 1; | 208 | this.infoCount += 1; |
200 | break; | 209 | break; |
210 | default: | ||
211 | throw new Error('Unknown severity'); | ||
201 | } | 212 | } |
202 | }); | 213 | }); |
203 | } | 214 | } |
@@ -261,7 +272,7 @@ export class EditorStore { | |||
261 | * This matches the behavior of the `openSearchPanel` and `closeSearchPanel` | 272 | * This matches the behavior of the `openSearchPanel` and `closeSearchPanel` |
262 | * commands from `'@codemirror/search'`. | 273 | * commands from `'@codemirror/search'`. |
263 | * | 274 | * |
264 | * @param newShosSearchPanel whether we should show the search panel | 275 | * @param newShowSearchPanel whether we should show the search panel |
265 | * @returns `true` if the state was changed, `false` otherwise | 276 | * @returns `true` if the state was changed, `false` otherwise |
266 | */ | 277 | */ |
267 | setSearchPanelOpen(newShowSearchPanel: boolean): boolean { | 278 | setSearchPanelOpen(newShowSearchPanel: boolean): boolean { |
diff --git a/subprojects/frontend/src/editor/GenerateButton.tsx b/subprojects/frontend/src/editor/GenerateButton.tsx index 3834cec4..fc337da9 100644 --- a/subprojects/frontend/src/editor/GenerateButton.tsx +++ b/subprojects/frontend/src/editor/GenerateButton.tsx | |||
@@ -1,13 +1,13 @@ | |||
1 | import { observer } from 'mobx-react-lite'; | ||
2 | import Button from '@mui/material/Button'; | ||
3 | import PlayArrowIcon from '@mui/icons-material/PlayArrow'; | 1 | import PlayArrowIcon from '@mui/icons-material/PlayArrow'; |
2 | import Button from '@mui/material/Button'; | ||
3 | import { observer } from 'mobx-react-lite'; | ||
4 | import React from 'react'; | 4 | import React from 'react'; |
5 | 5 | ||
6 | import { useRootStore } from '../RootStore'; | 6 | import { useRootStore } from '../RootStore'; |
7 | 7 | ||
8 | const GENERATE_LABEL = 'Generate'; | 8 | const GENERATE_LABEL = 'Generate'; |
9 | 9 | ||
10 | export const GenerateButton = observer(() => { | 10 | function GenerateButton(): JSX.Element { |
11 | const { editorStore } = useRootStore(); | 11 | const { editorStore } = useRootStore(); |
12 | const { errorCount, warningCount } = editorStore; | 12 | const { errorCount, warningCount } = editorStore; |
13 | 13 | ||
@@ -41,4 +41,6 @@ export const GenerateButton = observer(() => { | |||
41 | {summary === '' ? GENERATE_LABEL : `${GENERATE_LABEL} (${summary})`} | 41 | {summary === '' ? GENERATE_LABEL : `${GENERATE_LABEL} (${summary})`} |
42 | </Button> | 42 | </Button> |
43 | ); | 43 | ); |
44 | }); | 44 | } |
45 | |||
46 | export default observer(GenerateButton); | ||
diff --git a/subprojects/frontend/src/editor/decorationSetExtension.ts b/subprojects/frontend/src/editor/defineDecorationSetExtension.ts index 2d630c20..d9c7bc7d 100644 --- a/subprojects/frontend/src/editor/decorationSetExtension.ts +++ b/subprojects/frontend/src/editor/defineDecorationSetExtension.ts | |||
@@ -1,11 +1,16 @@ | |||
1 | import { StateEffect, StateField, TransactionSpec } from '@codemirror/state'; | 1 | import { StateEffect, StateField, TransactionSpec } from '@codemirror/state'; |
2 | import { EditorView, Decoration, DecorationSet } from '@codemirror/view'; | 2 | import { EditorView, Decoration, DecorationSet } from '@codemirror/view'; |
3 | 3 | ||
4 | export type TransactionSpecFactory = (decorations: DecorationSet) => TransactionSpec; | 4 | export type TransactionSpecFactory = ( |
5 | decorations: DecorationSet, | ||
6 | ) => TransactionSpec; | ||
5 | 7 | ||
6 | export function decorationSetExtension(): [TransactionSpecFactory, StateField<DecorationSet>] { | 8 | export default function defineDecorationSetExtension(): [ |
9 | TransactionSpecFactory, | ||
10 | StateField<DecorationSet>, | ||
11 | ] { | ||
7 | const setEffect = StateEffect.define<DecorationSet>(); | 12 | const setEffect = StateEffect.define<DecorationSet>(); |
8 | const field = StateField.define<DecorationSet>({ | 13 | const stateField = StateField.define<DecorationSet>({ |
9 | create() { | 14 | create() { |
10 | return Decoration.none; | 15 | return Decoration.none; |
11 | }, | 16 | }, |
@@ -24,16 +29,14 @@ export function decorationSetExtension(): [TransactionSpecFactory, StateField<De | |||
24 | } | 29 | } |
25 | return newDecorations; | 30 | return newDecorations; |
26 | }, | 31 | }, |
27 | provide: (f) => EditorView.decorations.from(f), | 32 | provide: (field) => EditorView.decorations.from(field), |
28 | }); | 33 | }); |
29 | 34 | ||
30 | function transactionSpecFactory(decorations: DecorationSet) { | 35 | function transactionSpecFactory(decorations: DecorationSet) { |
31 | return { | 36 | return { |
32 | effects: [ | 37 | effects: [setEffect.of(decorations)], |
33 | setEffect.of(decorations), | ||
34 | ], | ||
35 | }; | 38 | }; |
36 | } | 39 | } |
37 | 40 | ||
38 | return [transactionSpecFactory, field]; | 41 | return [transactionSpecFactory, stateField]; |
39 | } | 42 | } |
diff --git a/subprojects/frontend/src/editor/findOccurrences.ts b/subprojects/frontend/src/editor/findOccurrences.ts index c4a4e8ec..d7aae8d1 100644 --- a/subprojects/frontend/src/editor/findOccurrences.ts +++ b/subprojects/frontend/src/editor/findOccurrences.ts | |||
@@ -1,7 +1,7 @@ | |||
1 | import { Range, RangeSet, type TransactionSpec } from '@codemirror/state'; | 1 | import { type Range, RangeSet, type TransactionSpec } from '@codemirror/state'; |
2 | import { Decoration } from '@codemirror/view'; | 2 | import { Decoration } from '@codemirror/view'; |
3 | 3 | ||
4 | import { decorationSetExtension } from './decorationSetExtension'; | 4 | import defineDecorationSetExtension from './defineDecorationSetExtension'; |
5 | 5 | ||
6 | export interface IOccurrence { | 6 | export interface IOccurrence { |
7 | from: number; | 7 | from: number; |
@@ -9,7 +9,7 @@ export interface IOccurrence { | |||
9 | to: number; | 9 | to: number; |
10 | } | 10 | } |
11 | 11 | ||
12 | const [setOccurrencesInteral, findOccurrences] = decorationSetExtension(); | 12 | const [setOccurrencesInteral, findOccurrences] = defineDecorationSetExtension(); |
13 | 13 | ||
14 | const writeDecoration = Decoration.mark({ | 14 | const writeDecoration = Decoration.mark({ |
15 | class: 'cm-problem-write', | 15 | class: 'cm-problem-write', |
@@ -19,7 +19,10 @@ const readDecoration = Decoration.mark({ | |||
19 | class: 'cm-problem-read', | 19 | class: 'cm-problem-read', |
20 | }); | 20 | }); |
21 | 21 | ||
22 | export function setOccurrences(write: IOccurrence[], read: IOccurrence[]): TransactionSpec { | 22 | export function setOccurrences( |
23 | write: IOccurrence[], | ||
24 | read: IOccurrence[], | ||
25 | ): TransactionSpec { | ||
23 | const decorations: Range<Decoration>[] = []; | 26 | const decorations: Range<Decoration>[] = []; |
24 | write.forEach(({ from, to }) => { | 27 | write.forEach(({ from, to }) => { |
25 | decorations.push(writeDecoration.range(from, to)); | 28 | decorations.push(writeDecoration.range(from, to)); |
@@ -31,4 +34,4 @@ export function setOccurrences(write: IOccurrence[], read: IOccurrence[]): Trans | |||
31 | return setOccurrencesInteral(rangeSet); | 34 | return setOccurrencesInteral(rangeSet); |
32 | } | 35 | } |
33 | 36 | ||
34 | export { findOccurrences }; | 37 | export default findOccurrences; |
diff --git a/subprojects/frontend/src/editor/semanticHighlighting.ts b/subprojects/frontend/src/editor/semanticHighlighting.ts index a5d0af7a..2c1bd67d 100644 --- a/subprojects/frontend/src/editor/semanticHighlighting.ts +++ b/subprojects/frontend/src/editor/semanticHighlighting.ts | |||
@@ -1,7 +1,7 @@ | |||
1 | import { RangeSet, type TransactionSpec } from '@codemirror/state'; | 1 | import { RangeSet, type TransactionSpec } from '@codemirror/state'; |
2 | import { Decoration } from '@codemirror/view'; | 2 | import { Decoration } from '@codemirror/view'; |
3 | 3 | ||
4 | import { decorationSetExtension } from './decorationSetExtension'; | 4 | import defineDecorationSetExtension from './defineDecorationSetExtension'; |
5 | 5 | ||
6 | export interface IHighlightRange { | 6 | export interface IHighlightRange { |
7 | from: number; | 7 | from: number; |
@@ -11,13 +11,21 @@ export interface IHighlightRange { | |||
11 | classes: string[]; | 11 | classes: string[]; |
12 | } | 12 | } |
13 | 13 | ||
14 | const [setSemanticHighlightingInternal, semanticHighlighting] = decorationSetExtension(); | 14 | const [setSemanticHighlightingInternal, semanticHighlighting] = |
15 | defineDecorationSetExtension(); | ||
15 | 16 | ||
16 | export function setSemanticHighlighting(ranges: IHighlightRange[]): TransactionSpec { | 17 | export function setSemanticHighlighting( |
17 | const rangeSet = RangeSet.of(ranges.map(({ from, to, classes }) => Decoration.mark({ | 18 | ranges: IHighlightRange[], |
18 | class: classes.map((c) => `tok-problem-${c}`).join(' '), | 19 | ): TransactionSpec { |
19 | }).range(from, to)), true); | 20 | const rangeSet = RangeSet.of( |
21 | ranges.map(({ from, to, classes }) => | ||
22 | Decoration.mark({ | ||
23 | class: classes.map((c) => `tok-problem-${c}`).join(' '), | ||
24 | }).range(from, to), | ||
25 | ), | ||
26 | true, | ||
27 | ); | ||
20 | return setSemanticHighlightingInternal(rangeSet); | 28 | return setSemanticHighlightingInternal(rangeSet); |
21 | } | 29 | } |
22 | 30 | ||
23 | export { semanticHighlighting }; | 31 | export default semanticHighlighting; |
diff --git a/subprojects/frontend/src/global.d.ts b/subprojects/frontend/src/global.d.ts deleted file mode 100644 index 0533a46e..00000000 --- a/subprojects/frontend/src/global.d.ts +++ /dev/null | |||
@@ -1,11 +0,0 @@ | |||
1 | declare const DEBUG: boolean; | ||
2 | |||
3 | declare const PACKAGE_NAME: string; | ||
4 | |||
5 | declare const PACKAGE_VERSION: string; | ||
6 | |||
7 | declare module '*.module.scss' { | ||
8 | const cssVariables: { [key in string]?: string }; | ||
9 | // eslint-disable-next-line import/no-default-export | ||
10 | export default cssVariables; | ||
11 | } | ||
diff --git a/subprojects/frontend/src/index.html b/subprojects/frontend/src/index.html deleted file mode 100644 index f404aa8a..00000000 --- a/subprojects/frontend/src/index.html +++ /dev/null | |||
@@ -1,16 +0,0 @@ | |||
1 | <!DOCTYPE html> | ||
2 | <html lang="en-US"> | ||
3 | <head> | ||
4 | <meta charset="utf-8"> | ||
5 | <meta name="viewport" content="width=device-width, initial-scale=1"> | ||
6 | <title>Refinery</title> | ||
7 | </head> | ||
8 | <body> | ||
9 | <noscript> | ||
10 | <p> | ||
11 | This application requires JavaScript to run. | ||
12 | </p> | ||
13 | </noscript> | ||
14 | <div id="app"></div> | ||
15 | </body> | ||
16 | </html> | ||
diff --git a/subprojects/frontend/src/index.scss b/subprojects/frontend/src/index.scss deleted file mode 100644 index ad876aaf..00000000 --- a/subprojects/frontend/src/index.scss +++ /dev/null | |||
@@ -1,16 +0,0 @@ | |||
1 | @use '@fontsource/roboto/scss/mixins' as Roboto; | ||
2 | @use '@fontsource/jetbrains-mono/scss/mixins' as JetbrainsMono; | ||
3 | |||
4 | $fontWeights: 300, 400, 500, 700; | ||
5 | @each $weight in $fontWeights { | ||
6 | @include Roboto.fontFace($fontName: 'Roboto', $weight: $weight); | ||
7 | @include Roboto.fontFace($fontName: 'Roboto', $weight: $weight, $style: italic); | ||
8 | } | ||
9 | |||
10 | $monoFontWeights: 400, 700; | ||
11 | @each $weight in $monoFontWeights { | ||
12 | @include JetbrainsMono.fontFace($fontName: 'JetBrains Mono', $weight: $weight); | ||
13 | @include JetbrainsMono.fontFace($fontName: 'JetBrains Mono', $weight: $weight, $style: italic); | ||
14 | } | ||
15 | @include JetbrainsMono.fontFaceVariable($fontName: 'JetBrains MonoVariable'); | ||
16 | @include JetbrainsMono.fontFaceVariable($fontName: 'JetBrains MonoVariable', $style: italic); | ||
diff --git a/subprojects/frontend/src/index.tsx b/subprojects/frontend/src/index.tsx index 152c0bf7..2176b277 100644 --- a/subprojects/frontend/src/index.tsx +++ b/subprojects/frontend/src/index.tsx | |||
@@ -1,13 +1,25 @@ | |||
1 | import React from 'react'; | ||
2 | import { createRoot } from 'react-dom/client'; | ||
3 | import CssBaseline from '@mui/material/CssBaseline'; | 1 | import CssBaseline from '@mui/material/CssBaseline'; |
2 | import React, { Suspense, lazy } from 'react'; | ||
3 | import { createRoot } from 'react-dom/client'; | ||
4 | import '@fontsource/jetbrains-mono/400.css'; | ||
5 | import '@fontsource/jetbrains-mono/400-italic.css'; | ||
6 | import '@fontsource/jetbrains-mono/700.css'; | ||
7 | import '@fontsource/jetbrains-mono/700-italic.css'; | ||
8 | import '@fontsource/jetbrains-mono/variable.css'; | ||
9 | import '@fontsource/jetbrains-mono/variable-italic.css'; | ||
10 | import '@fontsource/roboto/300.css'; | ||
11 | import '@fontsource/roboto/300-italic.css'; | ||
12 | import '@fontsource/roboto/400.css'; | ||
13 | import '@fontsource/roboto/400-italic.css'; | ||
14 | import '@fontsource/roboto/500.css'; | ||
15 | import '@fontsource/roboto/500-italic.css'; | ||
16 | import '@fontsource/roboto/700.css'; | ||
17 | import '@fontsource/roboto/700-italic.css'; | ||
4 | 18 | ||
5 | import { App } from './App'; | 19 | import Loading from './Loading'; |
6 | import { RootStore, RootStoreProvider } from './RootStore'; | 20 | import RootStore, { RootStoreProvider } from './RootStore'; |
7 | import { ThemeProvider } from './theme/ThemeProvider'; | 21 | import ThemeProvider from './theme/ThemeProvider'; |
8 | import { getLogger } from './utils/logger'; | 22 | import getLogger from './utils/getLogger'; |
9 | |||
10 | import './index.scss'; | ||
11 | 23 | ||
12 | const log = getLogger('index'); | 24 | const log = getLogger('index'); |
13 | 25 | ||
@@ -60,13 +72,19 @@ scope Family = 1, Person += 5..10. | |||
60 | 72 | ||
61 | const rootStore = new RootStore(initialValue); | 73 | const rootStore = new RootStore(initialValue); |
62 | 74 | ||
75 | const App = lazy(() => import('./App.js')); | ||
76 | |||
63 | const app = ( | 77 | const app = ( |
64 | <RootStoreProvider rootStore={rootStore}> | 78 | <React.StrictMode> |
65 | <ThemeProvider> | 79 | <RootStoreProvider rootStore={rootStore}> |
66 | <CssBaseline /> | 80 | <ThemeProvider> |
67 | <App /> | 81 | <CssBaseline enableColorScheme /> |
68 | </ThemeProvider> | 82 | <Suspense fallback={<Loading />}> |
69 | </RootStoreProvider> | 83 | <App /> |
84 | </Suspense> | ||
85 | </ThemeProvider> | ||
86 | </RootStoreProvider> | ||
87 | </React.StrictMode> | ||
70 | ); | 88 | ); |
71 | 89 | ||
72 | const rootElement = document.getElementById('app'); | 90 | const rootElement = document.getElementById('app'); |
diff --git a/subprojects/frontend/src/language/folding.ts b/subprojects/frontend/src/language/folding.ts index 2560c183..9d1c04a3 100644 --- a/subprojects/frontend/src/language/folding.ts +++ b/subprojects/frontend/src/language/folding.ts | |||
@@ -1,7 +1,7 @@ | |||
1 | import { EditorState } from '@codemirror/state'; | 1 | import type { EditorState } from '@codemirror/state'; |
2 | import type { SyntaxNode } from '@lezer/common'; | 2 | import type { SyntaxNode } from '@lezer/common'; |
3 | 3 | ||
4 | export type FoldRange = { from: number, to: number }; | 4 | export type FoldRange = { from: number; to: number }; |
5 | 5 | ||
6 | /** | 6 | /** |
7 | * Folds a block comment between its delimiters. | 7 | * Folds a block comment between its delimiters. |
@@ -47,7 +47,10 @@ export function foldBlockComment(node: SyntaxNode): FoldRange { | |||
47 | * @param state the editor state | 47 | * @param state the editor state |
48 | * @returns the folding range or `null` is there is nothing to fold | 48 | * @returns the folding range or `null` is there is nothing to fold |
49 | */ | 49 | */ |
50 | export function foldDeclaration(node: SyntaxNode, state: EditorState): FoldRange | null { | 50 | export function foldDeclaration( |
51 | node: SyntaxNode, | ||
52 | state: EditorState, | ||
53 | ): FoldRange | null { | ||
51 | const { firstChild: open, lastChild: close } = node; | 54 | const { firstChild: open, lastChild: close } = node; |
52 | if (open === null || close === null) { | 55 | if (open === null || close === null) { |
53 | return null; | 56 | return null; |
diff --git a/subprojects/frontend/src/language/indentation.ts b/subprojects/frontend/src/language/indentation.ts index 1c38637f..0bd2423c 100644 --- a/subprojects/frontend/src/language/indentation.ts +++ b/subprojects/frontend/src/language/indentation.ts | |||
@@ -1,4 +1,4 @@ | |||
1 | import { TreeIndentContext } from '@codemirror/language'; | 1 | import type { TreeIndentContext } from '@codemirror/language'; |
2 | 2 | ||
3 | /** | 3 | /** |
4 | * Finds the `from` of first non-skipped token, if any, | 4 | * Finds the `from` of first non-skipped token, if any, |
@@ -11,18 +11,16 @@ import { TreeIndentContext } from '@codemirror/language'; | |||
11 | * @returns the alignment or `null` if there is no token after the opening keyword | 11 | * @returns the alignment or `null` if there is no token after the opening keyword |
12 | */ | 12 | */ |
13 | function findAlignmentAfterOpening(context: TreeIndentContext): number | null { | 13 | function findAlignmentAfterOpening(context: TreeIndentContext): number | null { |
14 | const { | 14 | const { node: tree, simulatedBreak } = context; |
15 | node: tree, | ||
16 | simulatedBreak, | ||
17 | } = context; | ||
18 | const openingToken = tree.childAfter(tree.from); | 15 | const openingToken = tree.childAfter(tree.from); |
19 | if (openingToken === null) { | 16 | if (openingToken === null) { |
20 | return null; | 17 | return null; |
21 | } | 18 | } |
22 | const openingLine = context.state.doc.lineAt(openingToken.from); | 19 | const openingLine = context.state.doc.lineAt(openingToken.from); |
23 | const lineEnd = simulatedBreak == null || simulatedBreak <= openingLine.from | 20 | const lineEnd = |
24 | ? openingLine.to | 21 | simulatedBreak == null || simulatedBreak <= openingLine.from |
25 | : Math.min(openingLine.to, simulatedBreak); | 22 | ? openingLine.to |
23 | : Math.min(openingLine.to, simulatedBreak); | ||
26 | const cursor = openingToken.cursor(); | 24 | const cursor = openingToken.cursor(); |
27 | while (cursor.next() && cursor.from < lineEnd) { | 25 | while (cursor.next() && cursor.from < lineEnd) { |
28 | if (!cursor.type.isSkipped) { | 26 | if (!cursor.type.isSkipped) { |
@@ -58,7 +56,10 @@ function findAlignmentAfterOpening(context: TreeIndentContext): number | null { | |||
58 | * @param units the number of units to indent | 56 | * @param units the number of units to indent |
59 | * @returns the desired indentation level | 57 | * @returns the desired indentation level |
60 | */ | 58 | */ |
61 | function indentDeclarationStrategy(context: TreeIndentContext, units: number): number { | 59 | function indentDeclarationStrategy( |
60 | context: TreeIndentContext, | ||
61 | units: number, | ||
62 | ): number { | ||
62 | const alignment = findAlignmentAfterOpening(context); | 63 | const alignment = findAlignmentAfterOpening(context); |
63 | if (alignment !== null) { | 64 | if (alignment !== null) { |
64 | return context.column(alignment); | 65 | return context.column(alignment); |
diff --git a/subprojects/frontend/src/language/problem.grammar b/subprojects/frontend/src/language/problem.grammar index ac0b0ea3..313df05d 100644 --- a/subprojects/frontend/src/language/problem.grammar +++ b/subprojects/frontend/src/language/problem.grammar | |||
@@ -1,6 +1,6 @@ | |||
1 | @detectDelim | 1 | @detectDelim |
2 | 2 | ||
3 | @external prop implicitCompletion from '../../../../src/language/props.ts' | 3 | @external prop implicitCompletion from './props' |
4 | 4 | ||
5 | @top Problem { statement* } | 5 | @top Problem { statement* } |
6 | 6 | ||
diff --git a/subprojects/frontend/src/language/problemLanguageSupport.ts b/subprojects/frontend/src/language/problemLanguageSupport.ts index 65fb50dc..246135d8 100644 --- a/subprojects/frontend/src/language/problemLanguageSupport.ts +++ b/subprojects/frontend/src/language/problemLanguageSupport.ts | |||
@@ -7,9 +7,7 @@ import { | |||
7 | LRLanguage, | 7 | LRLanguage, |
8 | } from '@codemirror/language'; | 8 | } from '@codemirror/language'; |
9 | import { styleTags, tags as t } from '@lezer/highlight'; | 9 | import { styleTags, tags as t } from '@lezer/highlight'; |
10 | import { LRParser } from '@lezer/lr'; | ||
11 | 10 | ||
12 | import { parser } from '../../build/generated/sources/lezer/problem'; | ||
13 | import { | 11 | import { |
14 | foldBlockComment, | 12 | foldBlockComment, |
15 | foldConjunction, | 13 | foldConjunction, |
@@ -21,8 +19,9 @@ import { | |||
21 | indentDeclaration, | 19 | indentDeclaration, |
22 | indentPredicateOrRule, | 20 | indentPredicateOrRule, |
23 | } from './indentation'; | 21 | } from './indentation'; |
22 | import { parser } from './problem.grammar'; | ||
24 | 23 | ||
25 | const parserWithMetadata = (parser as LRParser).configure({ | 24 | const parserWithMetadata = parser.configure({ |
26 | props: [ | 25 | props: [ |
27 | styleTags({ | 26 | styleTags({ |
28 | LineComment: t.lineComment, | 27 | LineComment: t.lineComment, |
@@ -86,8 +85,6 @@ const problemLanguage = LRLanguage.define({ | |||
86 | }, | 85 | }, |
87 | }); | 86 | }); |
88 | 87 | ||
89 | export function problemLanguageSupport(): LanguageSupport { | 88 | export default function problemLanguageSupport(): LanguageSupport { |
90 | return new LanguageSupport(problemLanguage, [ | 89 | return new LanguageSupport(problemLanguage, [indentUnit.of(' ')]); |
91 | indentUnit.of(' '), | ||
92 | ]); | ||
93 | } | 90 | } |
diff --git a/subprojects/frontend/src/language/props.ts b/subprojects/frontend/src/language/props.ts index 8e488bf5..65392e75 100644 --- a/subprojects/frontend/src/language/props.ts +++ b/subprojects/frontend/src/language/props.ts | |||
@@ -1,3 +1,5 @@ | |||
1 | /* eslint-disable import/prefer-default-export -- Lezer needs non-default exports */ | ||
2 | |||
1 | import { NodeProp } from '@lezer/common'; | 3 | import { NodeProp } from '@lezer/common'; |
2 | 4 | ||
3 | export const implicitCompletion = new NodeProp({ | 5 | export const implicitCompletion = new NodeProp({ |
diff --git a/subprojects/frontend/src/theme/EditorTheme.ts b/subprojects/frontend/src/theme/EditorTheme.ts index 294192fa..a16b4c3b 100644 --- a/subprojects/frontend/src/theme/EditorTheme.ts +++ b/subprojects/frontend/src/theme/EditorTheme.ts | |||
@@ -1,47 +1,7 @@ | |||
1 | import type { PaletteMode } from '@mui/material'; | 1 | enum EditorTheme { |
2 | |||
3 | import cssVariables from '../themeVariables.module.scss'; | ||
4 | |||
5 | export enum EditorTheme { | ||
6 | Light, | 2 | Light, |
7 | Dark, | 3 | Dark, |
4 | Default = EditorTheme.Dark, | ||
8 | } | 5 | } |
9 | 6 | ||
10 | export class EditorThemeData { | 7 | export default EditorTheme; |
11 | className: string; | ||
12 | |||
13 | paletteMode: PaletteMode; | ||
14 | |||
15 | toggleDarkMode: EditorTheme; | ||
16 | |||
17 | foreground!: string; | ||
18 | |||
19 | foregroundHighlight!: string; | ||
20 | |||
21 | background!: string; | ||
22 | |||
23 | primary!: string; | ||
24 | |||
25 | secondary!: string; | ||
26 | |||
27 | constructor(className: string, paletteMode: PaletteMode, toggleDarkMode: EditorTheme) { | ||
28 | this.className = className; | ||
29 | this.paletteMode = paletteMode; | ||
30 | this.toggleDarkMode = toggleDarkMode; | ||
31 | Reflect.ownKeys(this).forEach((key) => { | ||
32 | if (!Reflect.get(this, key)) { | ||
33 | const cssKey = `${this.className}--${key.toString()}`; | ||
34 | if (cssKey in cssVariables) { | ||
35 | Reflect.set(this, key, cssVariables[cssKey]); | ||
36 | } | ||
37 | } | ||
38 | }); | ||
39 | } | ||
40 | } | ||
41 | |||
42 | export const DEFAULT_THEME = EditorTheme.Dark; | ||
43 | |||
44 | export const EDITOR_THEMES: { [key in EditorTheme]: EditorThemeData } = { | ||
45 | [EditorTheme.Light]: new EditorThemeData('light', 'light', EditorTheme.Dark), | ||
46 | [EditorTheme.Dark]: new EditorThemeData('dark', 'dark', EditorTheme.Light), | ||
47 | }; | ||
diff --git a/subprojects/frontend/src/theme/ThemeProvider.tsx b/subprojects/frontend/src/theme/ThemeProvider.tsx index c6194c69..cf18e21c 100644 --- a/subprojects/frontend/src/theme/ThemeProvider.tsx +++ b/subprojects/frontend/src/theme/ThemeProvider.tsx | |||
@@ -1,15 +1,62 @@ | |||
1 | import { | ||
2 | createTheme, | ||
3 | responsiveFontSizes, | ||
4 | type ThemeOptions, | ||
5 | ThemeProvider as MaterialUiThemeProvider, | ||
6 | } from '@mui/material/styles'; | ||
1 | import { observer } from 'mobx-react-lite'; | 7 | import { observer } from 'mobx-react-lite'; |
2 | import { ThemeProvider as MaterialUiThemeProvider } from '@mui/material/styles'; | ||
3 | import React, { type ReactNode } from 'react'; | 8 | import React, { type ReactNode } from 'react'; |
4 | 9 | ||
5 | import { useRootStore } from '../RootStore'; | 10 | import { useRootStore } from '../RootStore'; |
6 | 11 | ||
7 | export const ThemeProvider: React.FC<{ children: ReactNode }> = observer(({ children }) => { | 12 | import EditorTheme from './EditorTheme'; |
8 | const { themeStore } = useRootStore(); | 13 | |
14 | function getMUIThemeOptions(currentTheme: EditorTheme): ThemeOptions { | ||
15 | switch (currentTheme) { | ||
16 | case EditorTheme.Light: | ||
17 | return { | ||
18 | palette: { | ||
19 | primary: { | ||
20 | main: '#56b6c2', | ||
21 | }, | ||
22 | }, | ||
23 | }; | ||
24 | case EditorTheme.Dark: | ||
25 | return { | ||
26 | palette: { | ||
27 | primary: { | ||
28 | main: '#56b6c2', | ||
29 | }, | ||
30 | }, | ||
31 | }; | ||
32 | default: | ||
33 | throw new Error(`Unknown theme: ${currentTheme}`); | ||
34 | } | ||
35 | } | ||
36 | |||
37 | function ThemeProvider({ children }: { children?: ReactNode }) { | ||
38 | const { | ||
39 | themeStore: { currentTheme, darkMode }, | ||
40 | } = useRootStore(); | ||
41 | |||
42 | const themeOptions = getMUIThemeOptions(currentTheme); | ||
43 | const theme = responsiveFontSizes( | ||
44 | createTheme({ | ||
45 | ...themeOptions, | ||
46 | palette: { | ||
47 | mode: darkMode ? 'dark' : 'light', | ||
48 | ...(themeOptions.palette ?? {}), | ||
49 | }, | ||
50 | }), | ||
51 | ); | ||
9 | 52 | ||
10 | return ( | 53 | return ( |
11 | <MaterialUiThemeProvider theme={themeStore.materialUiTheme}> | 54 | <MaterialUiThemeProvider theme={theme}>{children}</MaterialUiThemeProvider> |
12 | {children} | ||
13 | </MaterialUiThemeProvider> | ||
14 | ); | 55 | ); |
15 | }); | 56 | } |
57 | |||
58 | ThemeProvider.defaultProps = { | ||
59 | children: undefined, | ||
60 | }; | ||
61 | |||
62 | export default observer(ThemeProvider); | ||
diff --git a/subprojects/frontend/src/theme/ThemeStore.ts b/subprojects/frontend/src/theme/ThemeStore.ts index ffaf6dde..ded1f29a 100644 --- a/subprojects/frontend/src/theme/ThemeStore.ts +++ b/subprojects/frontend/src/theme/ThemeStore.ts | |||
@@ -1,64 +1,28 @@ | |||
1 | import { makeAutoObservable } from 'mobx'; | 1 | import { makeAutoObservable } from 'mobx'; |
2 | import { | ||
3 | Theme, | ||
4 | createTheme, | ||
5 | responsiveFontSizes, | ||
6 | } from '@mui/material/styles'; | ||
7 | 2 | ||
8 | import { | 3 | import EditorTheme from './EditorTheme'; |
9 | EditorTheme, | ||
10 | EditorThemeData, | ||
11 | DEFAULT_THEME, | ||
12 | EDITOR_THEMES, | ||
13 | } from './EditorTheme'; | ||
14 | 4 | ||
15 | export class ThemeStore { | 5 | export default class ThemeStore { |
16 | currentTheme: EditorTheme = DEFAULT_THEME; | 6 | currentTheme: EditorTheme = EditorTheme.Default; |
17 | 7 | ||
18 | constructor() { | 8 | constructor() { |
19 | makeAutoObservable(this); | 9 | makeAutoObservable(this); |
20 | } | 10 | } |
21 | 11 | ||
22 | toggleDarkMode(): void { | 12 | toggleDarkMode(): void { |
23 | this.currentTheme = this.currentThemeData.toggleDarkMode; | 13 | switch (this.currentTheme) { |
24 | } | 14 | case EditorTheme.Light: |
25 | 15 | this.currentTheme = EditorTheme.Dark; | |
26 | private get currentThemeData(): EditorThemeData { | 16 | break; |
27 | return EDITOR_THEMES[this.currentTheme]; | 17 | case EditorTheme.Dark: |
28 | } | 18 | this.currentTheme = EditorTheme.Light; |
29 | 19 | break; | |
30 | get materialUiTheme(): Theme { | 20 | default: |
31 | const themeData = this.currentThemeData; | 21 | throw new Error(`Unknown theme: ${this.currentTheme}`); |
32 | const materialUiTheme = createTheme({ | 22 | } |
33 | palette: { | ||
34 | mode: themeData.paletteMode, | ||
35 | background: { | ||
36 | default: themeData.background, | ||
37 | paper: themeData.background, | ||
38 | }, | ||
39 | primary: { | ||
40 | main: themeData.primary, | ||
41 | }, | ||
42 | secondary: { | ||
43 | main: themeData.secondary, | ||
44 | }, | ||
45 | error: { | ||
46 | main: themeData.secondary, | ||
47 | }, | ||
48 | text: { | ||
49 | primary: themeData.foregroundHighlight, | ||
50 | secondary: themeData.foreground, | ||
51 | }, | ||
52 | }, | ||
53 | }); | ||
54 | return responsiveFontSizes(materialUiTheme); | ||
55 | } | 23 | } |
56 | 24 | ||
57 | get darkMode(): boolean { | 25 | get darkMode(): boolean { |
58 | return this.currentThemeData.paletteMode === 'dark'; | 26 | return this.currentTheme === EditorTheme.Dark; |
59 | } | ||
60 | |||
61 | get className(): string { | ||
62 | return this.currentThemeData.className; | ||
63 | } | 27 | } |
64 | } | 28 | } |
diff --git a/subprojects/frontend/src/themeVariables.module.scss b/subprojects/frontend/src/themeVariables.module.scss deleted file mode 100644 index 85af4219..00000000 --- a/subprojects/frontend/src/themeVariables.module.scss +++ /dev/null | |||
@@ -1,9 +0,0 @@ | |||
1 | @import './themes'; | ||
2 | |||
3 | :export { | ||
4 | @each $themeName, $theme in $themes { | ||
5 | @each $variable, $value in $theme { | ||
6 | #{$themeName}--#{$variable}: $value, | ||
7 | } | ||
8 | } | ||
9 | } | ||
diff --git a/subprojects/frontend/src/themes.scss b/subprojects/frontend/src/themes.scss deleted file mode 100644 index a30f1de3..00000000 --- a/subprojects/frontend/src/themes.scss +++ /dev/null | |||
@@ -1,38 +0,0 @@ | |||
1 | $themes: ( | ||
2 | 'dark': ( | ||
3 | 'foreground': #abb2bf, | ||
4 | 'foregroundHighlight': #eeffff, | ||
5 | 'background': #212121, | ||
6 | 'primary': #56b6c2, | ||
7 | 'secondary': #ff5370, | ||
8 | 'keyword': #56b6c2, | ||
9 | 'predicate': #d6e9ff, | ||
10 | 'variable': #c8ae9d, | ||
11 | 'uniqueNode': #d6e9ff, | ||
12 | 'number': #6e88a6, | ||
13 | 'delimiter': #707787, | ||
14 | 'comment': #5c6370, | ||
15 | 'cursor': #56b6c2, | ||
16 | 'selection': #3e4452, | ||
17 | 'currentLine': rgba(0, 0, 0, 0.2), | ||
18 | 'lineNumber': #5c6370, | ||
19 | ), | ||
20 | 'light': ( | ||
21 | 'foreground': #abb2bf, | ||
22 | 'background': #282c34, | ||
23 | 'paper': #21252b, | ||
24 | 'primary': #56b6c2, | ||
25 | 'secondary': #ff5370, | ||
26 | 'keyword': #56b6c2, | ||
27 | 'predicate': #d6e9ff, | ||
28 | 'variable': #c8ae9d, | ||
29 | 'uniqueNode': #d6e9ff, | ||
30 | 'number': #6e88a6, | ||
31 | 'delimiter': #56606d, | ||
32 | 'comment': #55606d, | ||
33 | 'cursor': #f3efe7, | ||
34 | 'selection': #3e4452, | ||
35 | 'currentLine': #2c323c, | ||
36 | 'lineNumber': #5c6370, | ||
37 | ), | ||
38 | ); | ||
diff --git a/subprojects/frontend/src/utils/ConditionVariable.ts b/subprojects/frontend/src/utils/ConditionVariable.ts index 0910dfa6..c8fae9e8 100644 --- a/subprojects/frontend/src/utils/ConditionVariable.ts +++ b/subprojects/frontend/src/utils/ConditionVariable.ts | |||
@@ -1,11 +1,11 @@ | |||
1 | import { getLogger } from './logger'; | 1 | import PendingTask from './PendingTask'; |
2 | import { PendingTask } from './PendingTask'; | 2 | import getLogger from './getLogger'; |
3 | 3 | ||
4 | const log = getLogger('utils.ConditionVariable'); | 4 | const log = getLogger('utils.ConditionVariable'); |
5 | 5 | ||
6 | export type Condition = () => boolean; | 6 | export type Condition = () => boolean; |
7 | 7 | ||
8 | export class ConditionVariable { | 8 | export default class ConditionVariable { |
9 | condition: Condition; | 9 | condition: Condition; |
10 | 10 | ||
11 | defaultTimeout: number; | 11 | defaultTimeout: number; |
diff --git a/subprojects/frontend/src/utils/PendingTask.ts b/subprojects/frontend/src/utils/PendingTask.ts index 51b79fb0..086993d4 100644 --- a/subprojects/frontend/src/utils/PendingTask.ts +++ b/subprojects/frontend/src/utils/PendingTask.ts | |||
@@ -1,8 +1,8 @@ | |||
1 | import { getLogger } from './logger'; | 1 | import getLogger from './getLogger'; |
2 | 2 | ||
3 | const log = getLogger('utils.PendingTask'); | 3 | const log = getLogger('utils.PendingTask'); |
4 | 4 | ||
5 | export class PendingTask<T> { | 5 | export default class PendingTask<T> { |
6 | private readonly resolveCallback: (value: T) => void; | 6 | private readonly resolveCallback: (value: T) => void; |
7 | 7 | ||
8 | private readonly rejectCallback: (reason?: unknown) => void; | 8 | private readonly rejectCallback: (reason?: unknown) => void; |
diff --git a/subprojects/frontend/src/utils/Timer.ts b/subprojects/frontend/src/utils/Timer.ts index 8f653070..14e9eb81 100644 --- a/subprojects/frontend/src/utils/Timer.ts +++ b/subprojects/frontend/src/utils/Timer.ts | |||
@@ -1,4 +1,4 @@ | |||
1 | export class Timer { | 1 | export default class Timer { |
2 | readonly callback: () => void; | 2 | readonly callback: () => void; |
3 | 3 | ||
4 | readonly defaultTimeout: number; | 4 | readonly defaultTimeout: number; |
diff --git a/subprojects/frontend/src/utils/logger.ts b/subprojects/frontend/src/utils/getLogger.ts index 306d122c..301fd76d 100644 --- a/subprojects/frontend/src/utils/logger.ts +++ b/subprojects/frontend/src/utils/getLogger.ts | |||
@@ -1,6 +1,6 @@ | |||
1 | import styles, { CSPair } from 'ansi-styles'; | 1 | import styles, { type CSPair } from 'ansi-styles'; |
2 | import log from 'loglevel'; | 2 | import log from 'loglevel'; |
3 | import * as prefix from 'loglevel-plugin-prefix'; | 3 | import prefix from 'loglevel-plugin-prefix'; |
4 | 4 | ||
5 | const colors: Partial<Record<string, CSPair>> = { | 5 | const colors: Partial<Record<string, CSPair>> = { |
6 | TRACE: styles.magenta, | 6 | TRACE: styles.magenta, |
@@ -12,7 +12,7 @@ const colors: Partial<Record<string, CSPair>> = { | |||
12 | 12 | ||
13 | prefix.reg(log); | 13 | prefix.reg(log); |
14 | 14 | ||
15 | if (DEBUG) { | 15 | if (import.meta.env.DEV) { |
16 | log.setLevel(log.levels.DEBUG); | 16 | log.setLevel(log.levels.DEBUG); |
17 | } else { | 17 | } else { |
18 | log.setLevel(log.levels.WARN); | 18 | log.setLevel(log.levels.WARN); |
@@ -22,10 +22,14 @@ if ('chrome' in window) { | |||
22 | // Only Chromium supports console ANSI escape sequences. | 22 | // Only Chromium supports console ANSI escape sequences. |
23 | prefix.apply(log, { | 23 | prefix.apply(log, { |
24 | format(level, name, timestamp) { | 24 | format(level, name, timestamp) { |
25 | const formattedTimestamp = `${styles.gray.open}[${timestamp.toString()}]${styles.gray.close}`; | 25 | const formattedTimestamp = `${styles.gray.open}[${timestamp.toString()}]${ |
26 | styles.gray.close | ||
27 | }`; | ||
26 | const levelColor = colors[level.toUpperCase()] || styles.red; | 28 | const levelColor = colors[level.toUpperCase()] || styles.red; |
27 | const formattedLevel = `${levelColor.open}${level}${levelColor.close}`; | 29 | const formattedLevel = `${levelColor.open}${level}${levelColor.close}`; |
28 | const formattedName = `${styles.green.open}(${name || 'root'})${styles.green.close}`; | 30 | const formattedName = `${styles.green.open}(${name || 'root'})${ |
31 | styles.green.close | ||
32 | }`; | ||
29 | return `${formattedTimestamp} ${formattedLevel} ${formattedName}`; | 33 | return `${formattedTimestamp} ${formattedLevel} ${formattedName}`; |
30 | }, | 34 | }, |
31 | }); | 35 | }); |
@@ -35,15 +39,17 @@ if ('chrome' in window) { | |||
35 | }); | 39 | }); |
36 | } | 40 | } |
37 | 41 | ||
38 | const appLogger = log.getLogger(PACKAGE_NAME); | 42 | const appLogger = log.getLogger(import.meta.env.VITE_PACKAGE_NAME); |
39 | 43 | ||
40 | appLogger.info('Version:', PACKAGE_NAME, PACKAGE_VERSION); | 44 | appLogger.info( |
41 | appLogger.info('Debug mode:', DEBUG); | 45 | 'Version:', |
46 | import.meta.env.VITE_PACKAGE_NAME, | ||
47 | import.meta.env.VITE_PACKAGE_VERSION, | ||
48 | ); | ||
49 | appLogger.info('Debug mode:', import.meta.env.DEV); | ||
42 | 50 | ||
43 | export function getLoggerFromRoot(name: string | symbol): log.Logger { | 51 | export default function getLogger(name: string | symbol): log.Logger { |
44 | return log.getLogger(name); | 52 | return log.getLogger( |
45 | } | 53 | `${import.meta.env.VITE_PACKAGE_NAME}.${name.toString()}`, |
46 | 54 | ); | |
47 | export function getLogger(name: string | symbol): log.Logger { | ||
48 | return getLoggerFromRoot(`${PACKAGE_NAME}.${name.toString()}`); | ||
49 | } | 55 | } |
diff --git a/subprojects/frontend/src/xtext/ContentAssistService.ts b/subprojects/frontend/src/xtext/ContentAssistService.ts index bedd3b5c..dce2a902 100644 --- a/subprojects/frontend/src/xtext/ContentAssistService.ts +++ b/subprojects/frontend/src/xtext/ContentAssistService.ts | |||
@@ -8,8 +8,9 @@ import type { Transaction } from '@codemirror/state'; | |||
8 | import escapeStringRegexp from 'escape-string-regexp'; | 8 | import escapeStringRegexp from 'escape-string-regexp'; |
9 | 9 | ||
10 | import { implicitCompletion } from '../language/props'; | 10 | import { implicitCompletion } from '../language/props'; |
11 | import type { UpdateService } from './UpdateService'; | 11 | import getLogger from '../utils/getLogger'; |
12 | import { getLogger } from '../utils/logger'; | 12 | |
13 | import type UpdateService from './UpdateService'; | ||
13 | import type { ContentAssistEntry } from './xtextServiceResults'; | 14 | import type { ContentAssistEntry } from './xtextServiceResults'; |
14 | 15 | ||
15 | const PROPOSALS_LIMIT = 1000; | 16 | const PROPOSALS_LIMIT = 1000; |
@@ -48,10 +49,13 @@ function findToken({ pos, state }: CompletionContext): IFoundToken | null { | |||
48 | }; | 49 | }; |
49 | } | 50 | } |
50 | 51 | ||
51 | function shouldCompleteImplicitly(token: IFoundToken | null, context: CompletionContext): boolean { | 52 | function shouldCompleteImplicitly( |
52 | return token !== null | 53 | token: IFoundToken | null, |
53 | && token.implicitCompletion | 54 | context: CompletionContext, |
54 | && context.pos - token.from >= 2; | 55 | ): boolean { |
56 | return ( | ||
57 | token !== null && token.implicitCompletion && context.pos - token.from >= 2 | ||
58 | ); | ||
55 | } | 59 | } |
56 | 60 | ||
57 | function computeSpan(prefix: string, entryCount: number): RegExp { | 61 | function computeSpan(prefix: string, entryCount: number): RegExp { |
@@ -78,23 +82,29 @@ function createCompletion(entry: ContentAssistEntry): Completion { | |||
78 | case 'SNIPPET': | 82 | case 'SNIPPET': |
79 | boost = -90; | 83 | boost = -90; |
80 | break; | 84 | break; |
81 | default: { | 85 | default: |
82 | // Penalize qualified names (vs available unqualified names). | 86 | { |
83 | const extraSegments = entry.proposal.match(/::/g)?.length || 0; | 87 | // Penalize qualified names (vs available unqualified names). |
84 | boost = Math.max(-5 * extraSegments, -50); | 88 | const extraSegments = entry.proposal.match(/::/g)?.length || 0; |
85 | } | 89 | boost = Math.max(-5 * extraSegments, -50); |
90 | } | ||
86 | break; | 91 | break; |
87 | } | 92 | } |
88 | return { | 93 | const completion: Completion = { |
89 | label: entry.proposal, | 94 | label: entry.proposal, |
90 | detail: entry.description, | ||
91 | info: entry.documentation, | ||
92 | type: entry.kind?.toLowerCase(), | 95 | type: entry.kind?.toLowerCase(), |
93 | boost, | 96 | boost, |
94 | }; | 97 | }; |
98 | if (entry.documentation !== undefined) { | ||
99 | completion.info = entry.documentation; | ||
100 | } | ||
101 | if (entry.description !== undefined) { | ||
102 | completion.detail = entry.description; | ||
103 | } | ||
104 | return completion; | ||
95 | } | 105 | } |
96 | 106 | ||
97 | export class ContentAssistService { | 107 | export default class ContentAssistService { |
98 | private readonly updateService: UpdateService; | 108 | private readonly updateService: UpdateService; |
99 | 109 | ||
100 | private lastCompletion: CompletionResult | null = null; | 110 | private lastCompletion: CompletionResult | null = null; |
@@ -117,7 +127,7 @@ export class ContentAssistService { | |||
117 | options: [], | 127 | options: [], |
118 | }; | 128 | }; |
119 | } | 129 | } |
120 | let range: { from: number, to: number }; | 130 | let range: { from: number; to: number }; |
121 | let prefix = ''; | 131 | let prefix = ''; |
122 | if (tokenBefore === null) { | 132 | if (tokenBefore === null) { |
123 | range = { | 133 | range = { |
@@ -139,17 +149,20 @@ export class ContentAssistService { | |||
139 | log.trace('Returning cached completion result'); | 149 | log.trace('Returning cached completion result'); |
140 | // Postcondition of `shouldReturnCachedCompletion`: `lastCompletion !== null` | 150 | // Postcondition of `shouldReturnCachedCompletion`: `lastCompletion !== null` |
141 | return { | 151 | return { |
142 | ...this.lastCompletion as CompletionResult, | 152 | ...(this.lastCompletion as CompletionResult), |
143 | ...range, | 153 | ...range, |
144 | }; | 154 | }; |
145 | } | 155 | } |
146 | this.lastCompletion = null; | 156 | this.lastCompletion = null; |
147 | const entries = await this.updateService.fetchContentAssist({ | 157 | const entries = await this.updateService.fetchContentAssist( |
148 | resource: this.updateService.resourceName, | 158 | { |
149 | serviceType: 'assist', | 159 | resource: this.updateService.resourceName, |
150 | caretOffset: context.pos, | 160 | serviceType: 'assist', |
151 | proposalsLimit: PROPOSALS_LIMIT, | 161 | caretOffset: context.pos, |
152 | }, context); | 162 | proposalsLimit: PROPOSALS_LIMIT, |
163 | }, | ||
164 | context, | ||
165 | ); | ||
153 | if (context.aborted) { | 166 | if (context.aborted) { |
154 | return { | 167 | return { |
155 | ...range, | 168 | ...range, |
@@ -175,7 +188,7 @@ export class ContentAssistService { | |||
175 | } | 188 | } |
176 | 189 | ||
177 | private shouldReturnCachedCompletion( | 190 | private shouldReturnCachedCompletion( |
178 | token: { from: number, to: number, text: string } | null, | 191 | token: { from: number; to: number; text: string } | null, |
179 | ): boolean { | 192 | ): boolean { |
180 | if (token === null || this.lastCompletion === null) { | 193 | if (token === null || this.lastCompletion === null) { |
181 | return false; | 194 | return false; |
@@ -185,11 +198,16 @@ export class ContentAssistService { | |||
185 | if (!lastTo) { | 198 | if (!lastTo) { |
186 | return true; | 199 | return true; |
187 | } | 200 | } |
188 | const [transformedFrom, transformedTo] = this.mapRangeInclusive(lastFrom, lastTo); | 201 | const [transformedFrom, transformedTo] = this.mapRangeInclusive( |
189 | return from >= transformedFrom | 202 | lastFrom, |
190 | && to <= transformedTo | 203 | lastTo, |
191 | && validFor instanceof RegExp | 204 | ); |
192 | && validFor.exec(text) !== null; | 205 | return ( |
206 | from >= transformedFrom && | ||
207 | to <= transformedTo && | ||
208 | validFor instanceof RegExp && | ||
209 | validFor.exec(text) !== null | ||
210 | ); | ||
193 | } | 211 | } |
194 | 212 | ||
195 | private shouldInvalidateCachedCompletion(transaction: Transaction): boolean { | 213 | private shouldInvalidateCachedCompletion(transaction: Transaction): boolean { |
@@ -200,7 +218,10 @@ export class ContentAssistService { | |||
200 | if (!lastTo) { | 218 | if (!lastTo) { |
201 | return true; | 219 | return true; |
202 | } | 220 | } |
203 | const [transformedFrom, transformedTo] = this.mapRangeInclusive(lastFrom, lastTo); | 221 | const [transformedFrom, transformedTo] = this.mapRangeInclusive( |
222 | lastFrom, | ||
223 | lastTo, | ||
224 | ); | ||
204 | let invalidate = false; | 225 | let invalidate = false; |
205 | transaction.changes.iterChangedRanges((fromA, toA) => { | 226 | transaction.changes.iterChangedRanges((fromA, toA) => { |
206 | if (fromA < transformedFrom || toA > transformedTo) { | 227 | if (fromA < transformedFrom || toA > transformedTo) { |
@@ -210,7 +231,10 @@ export class ContentAssistService { | |||
210 | return invalidate; | 231 | return invalidate; |
211 | } | 232 | } |
212 | 233 | ||
213 | private mapRangeInclusive(lastFrom: number, lastTo: number): [number, number] { | 234 | private mapRangeInclusive( |
235 | lastFrom: number, | ||
236 | lastTo: number, | ||
237 | ): [number, number] { | ||
214 | const changes = this.updateService.computeChangesSinceLastUpdate(); | 238 | const changes = this.updateService.computeChangesSinceLastUpdate(); |
215 | const transformedFrom = changes.mapPos(lastFrom); | 239 | const transformedFrom = changes.mapPos(lastFrom); |
216 | const transformedTo = changes.mapPos(lastTo, 1); | 240 | const transformedTo = changes.mapPos(lastTo, 1); |
diff --git a/subprojects/frontend/src/xtext/HighlightingService.ts b/subprojects/frontend/src/xtext/HighlightingService.ts index dfbb4a19..cf618b96 100644 --- a/subprojects/frontend/src/xtext/HighlightingService.ts +++ b/subprojects/frontend/src/xtext/HighlightingService.ts | |||
@@ -1,9 +1,10 @@ | |||
1 | import type { EditorStore } from '../editor/EditorStore'; | 1 | import type EditorStore from '../editor/EditorStore'; |
2 | import type { IHighlightRange } from '../editor/semanticHighlighting'; | 2 | import type { IHighlightRange } from '../editor/semanticHighlighting'; |
3 | import type { UpdateService } from './UpdateService'; | 3 | |
4 | import type UpdateService from './UpdateService'; | ||
4 | import { highlightingResult } from './xtextServiceResults'; | 5 | import { highlightingResult } from './xtextServiceResults'; |
5 | 6 | ||
6 | export class HighlightingService { | 7 | export default class HighlightingService { |
7 | private readonly store: EditorStore; | 8 | private readonly store: EditorStore; |
8 | 9 | ||
9 | private readonly updateService: UpdateService; | 10 | private readonly updateService: UpdateService; |
diff --git a/subprojects/frontend/src/xtext/OccurrencesService.ts b/subprojects/frontend/src/xtext/OccurrencesService.ts index bc865537..21fe8644 100644 --- a/subprojects/frontend/src/xtext/OccurrencesService.ts +++ b/subprojects/frontend/src/xtext/OccurrencesService.ts | |||
@@ -1,15 +1,16 @@ | |||
1 | import { Transaction } from '@codemirror/state'; | 1 | import { Transaction } from '@codemirror/state'; |
2 | 2 | ||
3 | import type { EditorStore } from '../editor/EditorStore'; | 3 | import type EditorStore from '../editor/EditorStore'; |
4 | import type { IOccurrence } from '../editor/findOccurrences'; | 4 | import type { IOccurrence } from '../editor/findOccurrences'; |
5 | import type { UpdateService } from './UpdateService'; | 5 | import Timer from '../utils/Timer'; |
6 | import { getLogger } from '../utils/logger'; | 6 | import getLogger from '../utils/getLogger'; |
7 | import { Timer } from '../utils/Timer'; | 7 | |
8 | import { XtextWebSocketClient } from './XtextWebSocketClient'; | 8 | import type UpdateService from './UpdateService'; |
9 | import type XtextWebSocketClient from './XtextWebSocketClient'; | ||
9 | import { | 10 | import { |
10 | isConflictResult, | 11 | isConflictResult, |
11 | occurrencesResult, | 12 | OccurrencesResult, |
12 | TextRegion, | 13 | type TextRegion, |
13 | } from './xtextServiceResults'; | 14 | } from './xtextServiceResults'; |
14 | 15 | ||
15 | const FIND_OCCURRENCES_TIMEOUT_MS = 1000; | 16 | const FIND_OCCURRENCES_TIMEOUT_MS = 1000; |
@@ -33,7 +34,7 @@ function transformOccurrences(regions: TextRegion[]): IOccurrence[] { | |||
33 | return occurrences; | 34 | return occurrences; |
34 | } | 35 | } |
35 | 36 | ||
36 | export class OccurrencesService { | 37 | export default class OccurrencesService { |
37 | private readonly store: EditorStore; | 38 | private readonly store: EditorStore; |
38 | 39 | ||
39 | private readonly webSocketClient: XtextWebSocketClient; | 40 | private readonly webSocketClient: XtextWebSocketClient; |
@@ -94,7 +95,7 @@ export class OccurrencesService { | |||
94 | this.findOccurrencesTimer.schedule(); | 95 | this.findOccurrencesTimer.schedule(); |
95 | return; | 96 | return; |
96 | } | 97 | } |
97 | const parsedOccurrencesResult = occurrencesResult.safeParse(result); | 98 | const parsedOccurrencesResult = OccurrencesResult.safeParse(result); |
98 | if (!parsedOccurrencesResult.success) { | 99 | if (!parsedOccurrencesResult.success) { |
99 | log.error( | 100 | log.error( |
100 | 'Unexpected occurences result', | 101 | 'Unexpected occurences result', |
@@ -107,14 +108,25 @@ export class OccurrencesService { | |||
107 | } | 108 | } |
108 | const { stateId, writeRegions, readRegions } = parsedOccurrencesResult.data; | 109 | const { stateId, writeRegions, readRegions } = parsedOccurrencesResult.data; |
109 | if (stateId !== this.updateService.xtextStateId) { | 110 | if (stateId !== this.updateService.xtextStateId) { |
110 | log.error('Unexpected state id, expected:', this.updateService.xtextStateId, 'got:', stateId); | 111 | log.error( |
112 | 'Unexpected state id, expected:', | ||
113 | this.updateService.xtextStateId, | ||
114 | 'got:', | ||
115 | stateId, | ||
116 | ); | ||
111 | this.clearOccurrences(); | 117 | this.clearOccurrences(); |
112 | return; | 118 | return; |
113 | } | 119 | } |
114 | const write = transformOccurrences(writeRegions); | 120 | const write = transformOccurrences(writeRegions); |
115 | const read = transformOccurrences(readRegions); | 121 | const read = transformOccurrences(readRegions); |
116 | this.hasOccurrences = write.length > 0 || read.length > 0; | 122 | this.hasOccurrences = write.length > 0 || read.length > 0; |
117 | log.debug('Found', write.length, 'write and', read.length, 'read occurrences'); | 123 | log.debug( |
124 | 'Found', | ||
125 | write.length, | ||
126 | 'write and', | ||
127 | read.length, | ||
128 | 'read occurrences', | ||
129 | ); | ||
118 | this.store.updateOccurrences(write, read); | 130 | this.store.updateOccurrences(write, read); |
119 | } | 131 | } |
120 | 132 | ||
diff --git a/subprojects/frontend/src/xtext/UpdateService.ts b/subprojects/frontend/src/xtext/UpdateService.ts index e78944a9..2994b11b 100644 --- a/subprojects/frontend/src/xtext/UpdateService.ts +++ b/subprojects/frontend/src/xtext/UpdateService.ts | |||
@@ -1,22 +1,23 @@ | |||
1 | import { | 1 | import { |
2 | ChangeDesc, | 2 | type ChangeDesc, |
3 | ChangeSet, | 3 | ChangeSet, |
4 | ChangeSpec, | 4 | type ChangeSpec, |
5 | StateEffect, | 5 | StateEffect, |
6 | Transaction, | 6 | type Transaction, |
7 | } from '@codemirror/state'; | 7 | } from '@codemirror/state'; |
8 | import { nanoid } from 'nanoid'; | 8 | import { nanoid } from 'nanoid'; |
9 | 9 | ||
10 | import type { EditorStore } from '../editor/EditorStore'; | 10 | import type EditorStore from '../editor/EditorStore'; |
11 | import type { XtextWebSocketClient } from './XtextWebSocketClient'; | 11 | import ConditionVariable from '../utils/ConditionVariable'; |
12 | import { ConditionVariable } from '../utils/ConditionVariable'; | 12 | import Timer from '../utils/Timer'; |
13 | import { getLogger } from '../utils/logger'; | 13 | import getLogger from '../utils/getLogger'; |
14 | import { Timer } from '../utils/Timer'; | 14 | |
15 | import type XtextWebSocketClient from './XtextWebSocketClient'; | ||
15 | import { | 16 | import { |
16 | ContentAssistEntry, | 17 | type ContentAssistEntry, |
17 | contentAssistResult, | 18 | ContentAssistResult, |
18 | documentStateResult, | 19 | DocumentStateResult, |
19 | formattingResult, | 20 | FormattingResult, |
20 | isConflictResult, | 21 | isConflictResult, |
21 | } from './xtextServiceResults'; | 22 | } from './xtextServiceResults'; |
22 | 23 | ||
@@ -32,7 +33,7 @@ export interface IAbortSignal { | |||
32 | aborted: boolean; | 33 | aborted: boolean; |
33 | } | 34 | } |
34 | 35 | ||
35 | export class UpdateService { | 36 | export default class UpdateService { |
36 | resourceName: string; | 37 | resourceName: string; |
37 | 38 | ||
38 | xtextStateId: string | null = null; | 39 | xtextStateId: string | null = null; |
@@ -76,8 +77,8 @@ export class UpdateService { | |||
76 | } | 77 | } |
77 | 78 | ||
78 | onTransaction(transaction: Transaction): void { | 79 | onTransaction(transaction: Transaction): void { |
79 | const setDirtyChangesEffect = transaction.effects.find( | 80 | const setDirtyChangesEffect = transaction.effects.find((effect) => |
80 | (effect) => effect.is(setDirtyChanges), | 81 | effect.is(setDirtyChanges), |
81 | ) as StateEffect<ChangeSet> | undefined; | 82 | ) as StateEffect<ChangeSet> | undefined; |
82 | if (setDirtyChangesEffect) { | 83 | if (setDirtyChangesEffect) { |
83 | const { value } = setDirtyChangesEffect; | 84 | const { value } = setDirtyChangesEffect; |
@@ -102,7 +103,10 @@ export class UpdateService { | |||
102 | * @return the summary of changes since the last update | 103 | * @return the summary of changes since the last update |
103 | */ | 104 | */ |
104 | computeChangesSinceLastUpdate(): ChangeDesc { | 105 | computeChangesSinceLastUpdate(): ChangeDesc { |
105 | return this.pendingUpdate?.composeDesc(this.dirtyChanges.desc) || this.dirtyChanges.desc; | 106 | return ( |
107 | this.pendingUpdate?.composeDesc(this.dirtyChanges.desc) || | ||
108 | this.dirtyChanges.desc | ||
109 | ); | ||
106 | } | 110 | } |
107 | 111 | ||
108 | private handleIdleUpdate() { | 112 | private handleIdleUpdate() { |
@@ -131,7 +135,7 @@ export class UpdateService { | |||
131 | serviceType: 'update', | 135 | serviceType: 'update', |
132 | fullText: this.store.state.doc.sliceString(0), | 136 | fullText: this.store.state.doc.sliceString(0), |
133 | }); | 137 | }); |
134 | const { stateId } = documentStateResult.parse(result); | 138 | const { stateId } = DocumentStateResult.parse(result); |
135 | return [stateId, undefined]; | 139 | return [stateId, undefined]; |
136 | } | 140 | } |
137 | 141 | ||
@@ -158,7 +162,7 @@ export class UpdateService { | |||
158 | requiredStateId: this.xtextStateId, | 162 | requiredStateId: this.xtextStateId, |
159 | ...delta, | 163 | ...delta, |
160 | }); | 164 | }); |
161 | const parsedDocumentStateResult = documentStateResult.safeParse(result); | 165 | const parsedDocumentStateResult = DocumentStateResult.safeParse(result); |
162 | if (parsedDocumentStateResult.success) { | 166 | if (parsedDocumentStateResult.success) { |
163 | return [parsedDocumentStateResult.data.stateId, undefined]; | 167 | return [parsedDocumentStateResult.data.stateId, undefined]; |
164 | } | 168 | } |
@@ -197,9 +201,10 @@ export class UpdateService { | |||
197 | requiredStateId: this.xtextStateId, | 201 | requiredStateId: this.xtextStateId, |
198 | ...delta, | 202 | ...delta, |
199 | }); | 203 | }); |
200 | const parsedContentAssistResult = contentAssistResult.safeParse(result); | 204 | const parsedContentAssistResult = ContentAssistResult.safeParse(result); |
201 | if (parsedContentAssistResult.success) { | 205 | if (parsedContentAssistResult.success) { |
202 | const { stateId, entries: resultEntries } = parsedContentAssistResult.data; | 206 | const { stateId, entries: resultEntries } = |
207 | parsedContentAssistResult.data; | ||
203 | return [stateId, resultEntries]; | 208 | return [stateId, resultEntries]; |
204 | } | 209 | } |
205 | if (isConflictResult(result, 'invalidStateId')) { | 210 | if (isConflictResult(result, 'invalidStateId')) { |
@@ -223,14 +228,19 @@ export class UpdateService { | |||
223 | return this.doFetchContentAssist(params, this.xtextStateId as string); | 228 | return this.doFetchContentAssist(params, this.xtextStateId as string); |
224 | } | 229 | } |
225 | 230 | ||
226 | private async doFetchContentAssist(params: Record<string, unknown>, expectedStateId: string) { | 231 | private async doFetchContentAssist( |
232 | params: Record<string, unknown>, | ||
233 | expectedStateId: string, | ||
234 | ) { | ||
227 | const result = await this.webSocketClient.send({ | 235 | const result = await this.webSocketClient.send({ |
228 | ...params, | 236 | ...params, |
229 | requiredStateId: expectedStateId, | 237 | requiredStateId: expectedStateId, |
230 | }); | 238 | }); |
231 | const { stateId, entries } = contentAssistResult.parse(result); | 239 | const { stateId, entries } = ContentAssistResult.parse(result); |
232 | if (stateId !== expectedStateId) { | 240 | if (stateId !== expectedStateId) { |
233 | throw new Error(`Unexpected state id, expected: ${expectedStateId} got: ${stateId}`); | 241 | throw new Error( |
242 | `Unexpected state id, expected: ${expectedStateId} got: ${stateId}`, | ||
243 | ); | ||
234 | } | 244 | } |
235 | return entries; | 245 | return entries; |
236 | } | 246 | } |
@@ -250,7 +260,7 @@ export class UpdateService { | |||
250 | selectionStart: from, | 260 | selectionStart: from, |
251 | selectionEnd: to, | 261 | selectionEnd: to, |
252 | }); | 262 | }); |
253 | const { stateId, formattedText } = formattingResult.parse(result); | 263 | const { stateId, formattedText } = FormattingResult.parse(result); |
254 | this.applyBeforeDirtyChanges({ | 264 | this.applyBeforeDirtyChanges({ |
255 | from, | 265 | from, |
256 | to, | 266 | to, |
@@ -282,16 +292,15 @@ export class UpdateService { | |||
282 | } | 292 | } |
283 | 293 | ||
284 | private applyBeforeDirtyChanges(changeSpec: ChangeSpec) { | 294 | private applyBeforeDirtyChanges(changeSpec: ChangeSpec) { |
285 | const pendingChanges = this.pendingUpdate?.compose(this.dirtyChanges) || this.dirtyChanges; | 295 | const pendingChanges = |
296 | this.pendingUpdate?.compose(this.dirtyChanges) || this.dirtyChanges; | ||
286 | const revertChanges = pendingChanges.invert(this.store.state.doc); | 297 | const revertChanges = pendingChanges.invert(this.store.state.doc); |
287 | const applyBefore = ChangeSet.of(changeSpec, revertChanges.newLength); | 298 | const applyBefore = ChangeSet.of(changeSpec, revertChanges.newLength); |
288 | const redoChanges = pendingChanges.map(applyBefore.desc); | 299 | const redoChanges = pendingChanges.map(applyBefore.desc); |
289 | const changeSet = revertChanges.compose(applyBefore).compose(redoChanges); | 300 | const changeSet = revertChanges.compose(applyBefore).compose(redoChanges); |
290 | this.store.dispatch({ | 301 | this.store.dispatch({ |
291 | changes: changeSet, | 302 | changes: changeSet, |
292 | effects: [ | 303 | effects: [setDirtyChanges.of(redoChanges)], |
293 | setDirtyChanges.of(redoChanges), | ||
294 | ], | ||
295 | }); | 304 | }); |
296 | } | 305 | } |
297 | 306 | ||
@@ -316,7 +325,9 @@ export class UpdateService { | |||
316 | * @param callback the asynchronous callback that updates the server state | 325 | * @param callback the asynchronous callback that updates the server state |
317 | * @return a promise resolving to the second value returned by `callback` | 326 | * @return a promise resolving to the second value returned by `callback` |
318 | */ | 327 | */ |
319 | private async withUpdate<T>(callback: () => Promise<[string, T]>): Promise<T> { | 328 | private async withUpdate<T>( |
329 | callback: () => Promise<[string, T]>, | ||
330 | ): Promise<T> { | ||
320 | if (this.pendingUpdate !== null) { | 331 | if (this.pendingUpdate !== null) { |
321 | throw new Error('Another update is pending, will not perform update'); | 332 | throw new Error('Another update is pending, will not perform update'); |
322 | } | 333 | } |
diff --git a/subprojects/frontend/src/xtext/ValidationService.ts b/subprojects/frontend/src/xtext/ValidationService.ts index ff7d3700..a0b27251 100644 --- a/subprojects/frontend/src/xtext/ValidationService.ts +++ b/subprojects/frontend/src/xtext/ValidationService.ts | |||
@@ -1,10 +1,11 @@ | |||
1 | import type { Diagnostic } from '@codemirror/lint'; | 1 | import type { Diagnostic } from '@codemirror/lint'; |
2 | 2 | ||
3 | import type { EditorStore } from '../editor/EditorStore'; | 3 | import type EditorStore from '../editor/EditorStore'; |
4 | import type { UpdateService } from './UpdateService'; | ||
5 | import { validationResult } from './xtextServiceResults'; | ||
6 | 4 | ||
7 | export class ValidationService { | 5 | import type UpdateService from './UpdateService'; |
6 | import { ValidationResult } from './xtextServiceResults'; | ||
7 | |||
8 | export default class ValidationService { | ||
8 | private readonly store: EditorStore; | 9 | private readonly store: EditorStore; |
9 | 10 | ||
10 | private readonly updateService: UpdateService; | 11 | private readonly updateService: UpdateService; |
@@ -15,15 +16,10 @@ export class ValidationService { | |||
15 | } | 16 | } |
16 | 17 | ||
17 | onPush(push: unknown): void { | 18 | onPush(push: unknown): void { |
18 | const { issues } = validationResult.parse(push); | 19 | const { issues } = ValidationResult.parse(push); |
19 | const allChanges = this.updateService.computeChangesSinceLastUpdate(); | 20 | const allChanges = this.updateService.computeChangesSinceLastUpdate(); |
20 | const diagnostics: Diagnostic[] = []; | 21 | const diagnostics: Diagnostic[] = []; |
21 | issues.forEach(({ | 22 | issues.forEach(({ offset, length, severity, description }) => { |
22 | offset, | ||
23 | length, | ||
24 | severity, | ||
25 | description, | ||
26 | }) => { | ||
27 | if (severity === 'ignore') { | 23 | if (severity === 'ignore') { |
28 | return; | 24 | return; |
29 | } | 25 | } |
diff --git a/subprojects/frontend/src/xtext/XtextClient.ts b/subprojects/frontend/src/xtext/XtextClient.ts index 0898e725..7297c674 100644 --- a/subprojects/frontend/src/xtext/XtextClient.ts +++ b/subprojects/frontend/src/xtext/XtextClient.ts | |||
@@ -4,19 +4,20 @@ import type { | |||
4 | } from '@codemirror/autocomplete'; | 4 | } from '@codemirror/autocomplete'; |
5 | import type { Transaction } from '@codemirror/state'; | 5 | import type { Transaction } from '@codemirror/state'; |
6 | 6 | ||
7 | import type { EditorStore } from '../editor/EditorStore'; | 7 | import type EditorStore from '../editor/EditorStore'; |
8 | import { ContentAssistService } from './ContentAssistService'; | 8 | import getLogger from '../utils/getLogger'; |
9 | import { HighlightingService } from './HighlightingService'; | 9 | |
10 | import { OccurrencesService } from './OccurrencesService'; | 10 | import ContentAssistService from './ContentAssistService'; |
11 | import { UpdateService } from './UpdateService'; | 11 | import HighlightingService from './HighlightingService'; |
12 | import { getLogger } from '../utils/logger'; | 12 | import OccurrencesService from './OccurrencesService'; |
13 | import { ValidationService } from './ValidationService'; | 13 | import UpdateService from './UpdateService'; |
14 | import { XtextWebSocketClient } from './XtextWebSocketClient'; | 14 | import ValidationService from './ValidationService'; |
15 | import { XtextWebPushService } from './xtextMessages'; | 15 | import XtextWebSocketClient from './XtextWebSocketClient'; |
16 | import type { XtextWebPushService } from './xtextMessages'; | ||
16 | 17 | ||
17 | const log = getLogger('xtext.XtextClient'); | 18 | const log = getLogger('xtext.XtextClient'); |
18 | 19 | ||
19 | export class XtextClient { | 20 | export default class XtextClient { |
20 | private readonly webSocketClient: XtextWebSocketClient; | 21 | private readonly webSocketClient: XtextWebSocketClient; |
21 | 22 | ||
22 | private readonly updateService: UpdateService; | 23 | private readonly updateService: UpdateService; |
@@ -32,11 +33,15 @@ export class XtextClient { | |||
32 | constructor(store: EditorStore) { | 33 | constructor(store: EditorStore) { |
33 | this.webSocketClient = new XtextWebSocketClient( | 34 | this.webSocketClient = new XtextWebSocketClient( |
34 | () => this.updateService.onReconnect(), | 35 | () => this.updateService.onReconnect(), |
35 | (resource, stateId, service, push) => this.onPush(resource, stateId, service, push), | 36 | (resource, stateId, service, push) => |
37 | this.onPush(resource, stateId, service, push), | ||
36 | ); | 38 | ); |
37 | this.updateService = new UpdateService(store, this.webSocketClient); | 39 | this.updateService = new UpdateService(store, this.webSocketClient); |
38 | this.contentAssistService = new ContentAssistService(this.updateService); | 40 | this.contentAssistService = new ContentAssistService(this.updateService); |
39 | this.highlightingService = new HighlightingService(store, this.updateService); | 41 | this.highlightingService = new HighlightingService( |
42 | store, | ||
43 | this.updateService, | ||
44 | ); | ||
40 | this.validationService = new ValidationService(store, this.updateService); | 45 | this.validationService = new ValidationService(store, this.updateService); |
41 | this.occurrencesService = new OccurrencesService( | 46 | this.occurrencesService = new OccurrencesService( |
42 | store, | 47 | store, |
@@ -53,14 +58,29 @@ export class XtextClient { | |||
53 | this.occurrencesService.onTransaction(transaction); | 58 | this.occurrencesService.onTransaction(transaction); |
54 | } | 59 | } |
55 | 60 | ||
56 | private onPush(resource: string, stateId: string, service: XtextWebPushService, push: unknown) { | 61 | private onPush( |
62 | resource: string, | ||
63 | stateId: string, | ||
64 | service: XtextWebPushService, | ||
65 | push: unknown, | ||
66 | ) { | ||
57 | const { resourceName, xtextStateId } = this.updateService; | 67 | const { resourceName, xtextStateId } = this.updateService; |
58 | if (resource !== resourceName) { | 68 | if (resource !== resourceName) { |
59 | log.error('Unknown resource name: expected:', resourceName, 'got:', resource); | 69 | log.error( |
70 | 'Unknown resource name: expected:', | ||
71 | resourceName, | ||
72 | 'got:', | ||
73 | resource, | ||
74 | ); | ||
60 | return; | 75 | return; |
61 | } | 76 | } |
62 | if (stateId !== xtextStateId) { | 77 | if (stateId !== xtextStateId) { |
63 | log.error('Unexpected xtext state id: expected:', xtextStateId, 'got:', stateId); | 78 | log.error( |
79 | 'Unexpected xtext state id: expected:', | ||
80 | xtextStateId, | ||
81 | 'got:', | ||
82 | stateId, | ||
83 | ); | ||
64 | // The current push message might be stale (referring to a previous state), | 84 | // The current push message might be stale (referring to a previous state), |
65 | // so this is not neccessarily an error and there is no need to force-reconnect. | 85 | // so this is not neccessarily an error and there is no need to force-reconnect. |
66 | return; | 86 | return; |
@@ -71,6 +91,9 @@ export class XtextClient { | |||
71 | return; | 91 | return; |
72 | case 'validate': | 92 | case 'validate': |
73 | this.validationService.onPush(push); | 93 | this.validationService.onPush(push); |
94 | return; | ||
95 | default: | ||
96 | throw new Error('Unknown service'); | ||
74 | } | 97 | } |
75 | } | 98 | } |
76 | 99 | ||
diff --git a/subprojects/frontend/src/xtext/XtextWebSocketClient.ts b/subprojects/frontend/src/xtext/XtextWebSocketClient.ts index 2ce20a54..ceb1f3fd 100644 --- a/subprojects/frontend/src/xtext/XtextWebSocketClient.ts +++ b/subprojects/frontend/src/xtext/XtextWebSocketClient.ts | |||
@@ -1,16 +1,17 @@ | |||
1 | import { nanoid } from 'nanoid'; | 1 | import { nanoid } from 'nanoid'; |
2 | 2 | ||
3 | import { getLogger } from '../utils/logger'; | 3 | import PendingTask from '../utils/PendingTask'; |
4 | import { PendingTask } from '../utils/PendingTask'; | 4 | import Timer from '../utils/Timer'; |
5 | import { Timer } from '../utils/Timer'; | 5 | import getLogger from '../utils/getLogger'; |
6 | |||
6 | import { | 7 | import { |
7 | xtextWebErrorResponse, | 8 | XtextWebErrorResponse, |
8 | XtextWebRequest, | 9 | XtextWebRequest, |
9 | xtextWebOkResponse, | 10 | XtextWebOkResponse, |
10 | xtextWebPushMessage, | 11 | XtextWebPushMessage, |
11 | XtextWebPushService, | 12 | XtextWebPushService, |
12 | } from './xtextMessages'; | 13 | } from './xtextMessages'; |
13 | import { pongResult } from './xtextServiceResults'; | 14 | import { PongResult } from './xtextServiceResults'; |
14 | 15 | ||
15 | const XTEXT_SUBPROTOCOL_V1 = 'tools.refinery.language.web.xtext.v1'; | 16 | const XTEXT_SUBPROTOCOL_V1 = 'tools.refinery.language.web.xtext.v1'; |
16 | 17 | ||
@@ -18,7 +19,8 @@ const WEBSOCKET_CLOSE_OK = 1000; | |||
18 | 19 | ||
19 | const RECONNECT_DELAY_MS = [200, 1000, 5000, 30_000]; | 20 | const RECONNECT_DELAY_MS = [200, 1000, 5000, 30_000]; |
20 | 21 | ||
21 | const MAX_RECONNECT_DELAY_MS = RECONNECT_DELAY_MS[RECONNECT_DELAY_MS.length - 1]; | 22 | const MAX_RECONNECT_DELAY_MS = |
23 | RECONNECT_DELAY_MS[RECONNECT_DELAY_MS.length - 1]; | ||
22 | 24 | ||
23 | const BACKGROUND_IDLE_TIMEOUT_MS = 5 * 60 * 1000; | 25 | const BACKGROUND_IDLE_TIMEOUT_MS = 5 * 60 * 1000; |
24 | 26 | ||
@@ -47,7 +49,7 @@ enum State { | |||
47 | TimedOut, | 49 | TimedOut, |
48 | } | 50 | } |
49 | 51 | ||
50 | export class XtextWebSocketClient { | 52 | export default class XtextWebSocketClient { |
51 | private nextMessageId = 0; | 53 | private nextMessageId = 0; |
52 | 54 | ||
53 | private connection!: WebSocket; | 55 | private connection!: WebSocket; |
@@ -88,9 +90,11 @@ export class XtextWebSocketClient { | |||
88 | } | 90 | } |
89 | 91 | ||
90 | get isOpen(): boolean { | 92 | get isOpen(): boolean { |
91 | return this.state === State.TabVisible | 93 | return ( |
92 | || this.state === State.TabHiddenIdle | 94 | this.state === State.TabVisible || |
93 | || this.state === State.TabHiddenWaiting; | 95 | this.state === State.TabHiddenIdle || |
96 | this.state === State.TabHiddenWaiting | ||
97 | ); | ||
94 | } | 98 | } |
95 | 99 | ||
96 | private reconnect() { | 100 | private reconnect() { |
@@ -104,7 +108,11 @@ export class XtextWebSocketClient { | |||
104 | this.connection = new WebSocket(webSocketUrl, XTEXT_SUBPROTOCOL_V1); | 108 | this.connection = new WebSocket(webSocketUrl, XTEXT_SUBPROTOCOL_V1); |
105 | this.connection.addEventListener('open', () => { | 109 | this.connection.addEventListener('open', () => { |
106 | if (this.connection.protocol !== XTEXT_SUBPROTOCOL_V1) { | 110 | if (this.connection.protocol !== XTEXT_SUBPROTOCOL_V1) { |
107 | log.error('Unknown subprotocol', this.connection.protocol, 'selected by server'); | 111 | log.error( |
112 | 'Unknown subprotocol', | ||
113 | this.connection.protocol, | ||
114 | 'selected by server', | ||
115 | ); | ||
108 | this.forceReconnectOnError(); | 116 | this.forceReconnectOnError(); |
109 | } | 117 | } |
110 | if (document.visibilityState === 'hidden') { | 118 | if (document.visibilityState === 'hidden') { |
@@ -126,8 +134,11 @@ export class XtextWebSocketClient { | |||
126 | this.handleMessage(event.data); | 134 | this.handleMessage(event.data); |
127 | }); | 135 | }); |
128 | this.connection.addEventListener('close', (event) => { | 136 | this.connection.addEventListener('close', (event) => { |
129 | if (this.isLogicallyClosed && event.code === WEBSOCKET_CLOSE_OK | 137 | if ( |
130 | && this.pendingRequests.size === 0) { | 138 | this.isLogicallyClosed && |
139 | event.code === WEBSOCKET_CLOSE_OK && | ||
140 | this.pendingRequests.size === 0 | ||
141 | ) { | ||
131 | log.info('Websocket closed'); | 142 | log.info('Websocket closed'); |
132 | return; | 143 | return; |
133 | } | 144 | } |
@@ -144,7 +155,10 @@ export class XtextWebSocketClient { | |||
144 | return; | 155 | return; |
145 | } | 156 | } |
146 | this.idleTimer.cancel(); | 157 | this.idleTimer.cancel(); |
147 | if (this.state === State.TabHiddenIdle || this.state === State.TabHiddenWaiting) { | 158 | if ( |
159 | this.state === State.TabHiddenIdle || | ||
160 | this.state === State.TabHiddenWaiting | ||
161 | ) { | ||
148 | this.handleTabVisibleConnected(); | 162 | this.handleTabVisibleConnected(); |
149 | return; | 163 | return; |
150 | } | 164 | } |
@@ -183,7 +197,11 @@ export class XtextWebSocketClient { | |||
183 | this.closeConnection(1000, 'idle timeout'); | 197 | this.closeConnection(1000, 'idle timeout'); |
184 | return; | 198 | return; |
185 | } | 199 | } |
186 | log.info('Waiting for', pending, 'pending requests before closing websocket'); | 200 | log.info( |
201 | 'Waiting for', | ||
202 | pending, | ||
203 | 'pending requests before closing websocket', | ||
204 | ); | ||
187 | } | 205 | } |
188 | 206 | ||
189 | private sendPing() { | 207 | private sendPing() { |
@@ -192,19 +210,21 @@ export class XtextWebSocketClient { | |||
192 | } | 210 | } |
193 | const ping = nanoid(); | 211 | const ping = nanoid(); |
194 | log.trace('Ping', ping); | 212 | log.trace('Ping', ping); |
195 | this.send({ ping }).then((result) => { | 213 | this.send({ ping }) |
196 | const parsedPongResult = pongResult.safeParse(result); | 214 | .then((result) => { |
197 | if (parsedPongResult.success && parsedPongResult.data.pong === ping) { | 215 | const parsedPongResult = PongResult.safeParse(result); |
198 | log.trace('Pong', ping); | 216 | if (parsedPongResult.success && parsedPongResult.data.pong === ping) { |
199 | this.pingTimer.schedule(); | 217 | log.trace('Pong', ping); |
200 | } else { | 218 | this.pingTimer.schedule(); |
201 | log.error('Invalid pong:', parsedPongResult, 'expected:', ping); | 219 | } else { |
220 | log.error('Invalid pong:', parsedPongResult, 'expected:', ping); | ||
221 | this.forceReconnectOnError(); | ||
222 | } | ||
223 | }) | ||
224 | .catch((error) => { | ||
225 | log.error('Error while waiting for ping', error); | ||
202 | this.forceReconnectOnError(); | 226 | this.forceReconnectOnError(); |
203 | } | 227 | }); |
204 | }).catch((error) => { | ||
205 | log.error('Error while waiting for ping', error); | ||
206 | this.forceReconnectOnError(); | ||
207 | }); | ||
208 | } | 228 | } |
209 | 229 | ||
210 | send(request: unknown): Promise<unknown> { | 230 | send(request: unknown): Promise<unknown> { |
@@ -250,13 +270,13 @@ export class XtextWebSocketClient { | |||
250 | this.forceReconnectOnError(); | 270 | this.forceReconnectOnError(); |
251 | return; | 271 | return; |
252 | } | 272 | } |
253 | const okResponse = xtextWebOkResponse.safeParse(message); | 273 | const okResponse = XtextWebOkResponse.safeParse(message); |
254 | if (okResponse.success) { | 274 | if (okResponse.success) { |
255 | const { id, response } = okResponse.data; | 275 | const { id, response } = okResponse.data; |
256 | this.resolveRequest(id, response); | 276 | this.resolveRequest(id, response); |
257 | return; | 277 | return; |
258 | } | 278 | } |
259 | const errorResponse = xtextWebErrorResponse.safeParse(message); | 279 | const errorResponse = XtextWebErrorResponse.safeParse(message); |
260 | if (errorResponse.success) { | 280 | if (errorResponse.success) { |
261 | const { id, error, message: errorMessage } = errorResponse.data; | 281 | const { id, error, message: errorMessage } = errorResponse.data; |
262 | this.rejectRequest(id, new Error(`${error} error: ${errorMessage}`)); | 282 | this.rejectRequest(id, new Error(`${error} error: ${errorMessage}`)); |
@@ -266,14 +286,9 @@ export class XtextWebSocketClient { | |||
266 | } | 286 | } |
267 | return; | 287 | return; |
268 | } | 288 | } |
269 | const pushMessage = xtextWebPushMessage.safeParse(message); | 289 | const pushMessage = XtextWebPushMessage.safeParse(message); |
270 | if (pushMessage.success) { | 290 | if (pushMessage.success) { |
271 | const { | 291 | const { resource, stateId, service, push } = pushMessage.data; |
272 | resource, | ||
273 | stateId, | ||
274 | service, | ||
275 | push, | ||
276 | } = pushMessage.data; | ||
277 | this.onPush(resource, stateId, service, push); | 292 | this.onPush(resource, stateId, service, push); |
278 | } else { | 293 | } else { |
279 | log.error( | 294 | log.error( |
@@ -343,7 +358,8 @@ export class XtextWebSocketClient { | |||
343 | private handleErrorState() { | 358 | private handleErrorState() { |
344 | this.state = State.Error; | 359 | this.state = State.Error; |
345 | this.reconnectTryCount += 1; | 360 | this.reconnectTryCount += 1; |
346 | const delay = RECONNECT_DELAY_MS[this.reconnectTryCount - 1] || MAX_RECONNECT_DELAY_MS; | 361 | const delay = |
362 | RECONNECT_DELAY_MS[this.reconnectTryCount - 1] || MAX_RECONNECT_DELAY_MS; | ||
347 | log.info('Reconnecting in', delay, 'ms'); | 363 | log.info('Reconnecting in', delay, 'ms'); |
348 | this.reconnectTimer.schedule(delay); | 364 | this.reconnectTimer.schedule(delay); |
349 | } | 365 | } |
diff --git a/subprojects/frontend/src/xtext/xtextMessages.ts b/subprojects/frontend/src/xtext/xtextMessages.ts index 4bf49c17..c4d0c676 100644 --- a/subprojects/frontend/src/xtext/xtextMessages.ts +++ b/subprojects/frontend/src/xtext/xtextMessages.ts | |||
@@ -1,40 +1,42 @@ | |||
1 | /* eslint-disable @typescript-eslint/no-redeclare -- Declare types with their companion objects */ | ||
2 | |||
1 | import { z } from 'zod'; | 3 | import { z } from 'zod'; |
2 | 4 | ||
3 | export const xtextWebRequest = z.object({ | 5 | export const XtextWebRequest = z.object({ |
4 | id: z.string().min(1), | 6 | id: z.string().min(1), |
5 | request: z.unknown(), | 7 | request: z.unknown(), |
6 | }); | 8 | }); |
7 | 9 | ||
8 | export type XtextWebRequest = z.infer<typeof xtextWebRequest>; | 10 | export type XtextWebRequest = z.infer<typeof XtextWebRequest>; |
9 | 11 | ||
10 | export const xtextWebOkResponse = z.object({ | 12 | export const XtextWebOkResponse = z.object({ |
11 | id: z.string().min(1), | 13 | id: z.string().min(1), |
12 | response: z.unknown(), | 14 | response: z.unknown(), |
13 | }); | 15 | }); |
14 | 16 | ||
15 | export type XtextWebOkResponse = z.infer<typeof xtextWebOkResponse>; | 17 | export type XtextWebOkResponse = z.infer<typeof XtextWebOkResponse>; |
16 | 18 | ||
17 | export const xtextWebErrorKind = z.enum(['request', 'server']); | 19 | export const XtextWebErrorKind = z.enum(['request', 'server']); |
18 | 20 | ||
19 | export type XtextWebErrorKind = z.infer<typeof xtextWebErrorKind>; | 21 | export type XtextWebErrorKind = z.infer<typeof XtextWebErrorKind>; |
20 | 22 | ||
21 | export const xtextWebErrorResponse = z.object({ | 23 | export const XtextWebErrorResponse = z.object({ |
22 | id: z.string().min(1), | 24 | id: z.string().min(1), |
23 | error: xtextWebErrorKind, | 25 | error: XtextWebErrorKind, |
24 | message: z.string(), | 26 | message: z.string(), |
25 | }); | 27 | }); |
26 | 28 | ||
27 | export type XtextWebErrorResponse = z.infer<typeof xtextWebErrorResponse>; | 29 | export type XtextWebErrorResponse = z.infer<typeof XtextWebErrorResponse>; |
28 | 30 | ||
29 | export const xtextWebPushService = z.enum(['highlight', 'validate']); | 31 | export const XtextWebPushService = z.enum(['highlight', 'validate']); |
30 | 32 | ||
31 | export type XtextWebPushService = z.infer<typeof xtextWebPushService>; | 33 | export type XtextWebPushService = z.infer<typeof XtextWebPushService>; |
32 | 34 | ||
33 | export const xtextWebPushMessage = z.object({ | 35 | export const XtextWebPushMessage = z.object({ |
34 | resource: z.string().min(1), | 36 | resource: z.string().min(1), |
35 | stateId: z.string().min(1), | 37 | stateId: z.string().min(1), |
36 | service: xtextWebPushService, | 38 | service: XtextWebPushService, |
37 | push: z.unknown(), | 39 | push: z.unknown(), |
38 | }); | 40 | }); |
39 | 41 | ||
40 | export type XtextWebPushMessage = z.infer<typeof xtextWebPushMessage>; | 42 | export type XtextWebPushMessage = z.infer<typeof XtextWebPushMessage>; |
diff --git a/subprojects/frontend/src/xtext/xtextServiceResults.ts b/subprojects/frontend/src/xtext/xtextServiceResults.ts index 8b0dbbfb..4cfb9c33 100644 --- a/subprojects/frontend/src/xtext/xtextServiceResults.ts +++ b/subprojects/frontend/src/xtext/xtextServiceResults.ts | |||
@@ -1,112 +1,120 @@ | |||
1 | /* eslint-disable @typescript-eslint/no-redeclare -- Declare types with their companion objects */ | ||
2 | |||
1 | import { z } from 'zod'; | 3 | import { z } from 'zod'; |
2 | 4 | ||
3 | export const pongResult = z.object({ | 5 | export const PongResult = z.object({ |
4 | pong: z.string().min(1), | 6 | pong: z.string().min(1), |
5 | }); | 7 | }); |
6 | 8 | ||
7 | export type PongResult = z.infer<typeof pongResult>; | 9 | export type PongResult = z.infer<typeof PongResult>; |
8 | 10 | ||
9 | export const documentStateResult = z.object({ | 11 | export const DocumentStateResult = z.object({ |
10 | stateId: z.string().min(1), | 12 | stateId: z.string().min(1), |
11 | }); | 13 | }); |
12 | 14 | ||
13 | export type DocumentStateResult = z.infer<typeof documentStateResult>; | 15 | export type DocumentStateResult = z.infer<typeof DocumentStateResult>; |
14 | 16 | ||
15 | export const conflict = z.enum(['invalidStateId', 'canceled']); | 17 | export const Conflict = z.enum(['invalidStateId', 'canceled']); |
16 | 18 | ||
17 | export type Conflict = z.infer<typeof conflict>; | 19 | export type Conflict = z.infer<typeof Conflict>; |
18 | 20 | ||
19 | export const serviceConflictResult = z.object({ | 21 | export const ServiceConflictResult = z.object({ |
20 | conflict, | 22 | conflict: Conflict, |
21 | }); | 23 | }); |
22 | 24 | ||
23 | export type ServiceConflictResult = z.infer<typeof serviceConflictResult>; | 25 | export type ServiceConflictResult = z.infer<typeof ServiceConflictResult>; |
24 | 26 | ||
25 | export function isConflictResult(result: unknown, conflictType: Conflict): boolean { | 27 | export function isConflictResult( |
26 | const parsedConflictResult = serviceConflictResult.safeParse(result); | 28 | result: unknown, |
27 | return parsedConflictResult.success && parsedConflictResult.data.conflict === conflictType; | 29 | conflictType: Conflict, |
30 | ): boolean { | ||
31 | const parsedConflictResult = ServiceConflictResult.safeParse(result); | ||
32 | return ( | ||
33 | parsedConflictResult.success && | ||
34 | parsedConflictResult.data.conflict === conflictType | ||
35 | ); | ||
28 | } | 36 | } |
29 | 37 | ||
30 | export const severity = z.enum(['error', 'warning', 'info', 'ignore']); | 38 | export const Severity = z.enum(['error', 'warning', 'info', 'ignore']); |
31 | 39 | ||
32 | export type Severity = z.infer<typeof severity>; | 40 | export type Severity = z.infer<typeof Severity>; |
33 | 41 | ||
34 | export const issue = z.object({ | 42 | export const Issue = z.object({ |
35 | description: z.string().min(1), | 43 | description: z.string().min(1), |
36 | severity, | 44 | severity: Severity, |
37 | line: z.number().int(), | 45 | line: z.number().int(), |
38 | column: z.number().int().nonnegative(), | 46 | column: z.number().int().nonnegative(), |
39 | offset: z.number().int().nonnegative(), | 47 | offset: z.number().int().nonnegative(), |
40 | length: z.number().int().nonnegative(), | 48 | length: z.number().int().nonnegative(), |
41 | }); | 49 | }); |
42 | 50 | ||
43 | export type Issue = z.infer<typeof issue>; | 51 | export type Issue = z.infer<typeof Issue>; |
44 | 52 | ||
45 | export const validationResult = z.object({ | 53 | export const ValidationResult = z.object({ |
46 | issues: issue.array(), | 54 | issues: Issue.array(), |
47 | }); | 55 | }); |
48 | 56 | ||
49 | export type ValidationResult = z.infer<typeof validationResult>; | 57 | export type ValidationResult = z.infer<typeof ValidationResult>; |
50 | 58 | ||
51 | export const replaceRegion = z.object({ | 59 | export const ReplaceRegion = z.object({ |
52 | offset: z.number().int().nonnegative(), | 60 | offset: z.number().int().nonnegative(), |
53 | length: z.number().int().nonnegative(), | 61 | length: z.number().int().nonnegative(), |
54 | text: z.string(), | 62 | text: z.string(), |
55 | }); | 63 | }); |
56 | 64 | ||
57 | export type ReplaceRegion = z.infer<typeof replaceRegion>; | 65 | export type ReplaceRegion = z.infer<typeof ReplaceRegion>; |
58 | 66 | ||
59 | export const textRegion = z.object({ | 67 | export const TextRegion = z.object({ |
60 | offset: z.number().int().nonnegative(), | 68 | offset: z.number().int().nonnegative(), |
61 | length: z.number().int().nonnegative(), | 69 | length: z.number().int().nonnegative(), |
62 | }); | 70 | }); |
63 | 71 | ||
64 | export type TextRegion = z.infer<typeof textRegion>; | 72 | export type TextRegion = z.infer<typeof TextRegion>; |
65 | 73 | ||
66 | export const contentAssistEntry = z.object({ | 74 | export const ContentAssistEntry = z.object({ |
67 | prefix: z.string(), | 75 | prefix: z.string(), |
68 | proposal: z.string().min(1), | 76 | proposal: z.string().min(1), |
69 | label: z.string().optional(), | 77 | label: z.string().optional(), |
70 | description: z.string().min(1).optional(), | 78 | description: z.string().min(1).optional(), |
71 | documentation: z.string().min(1).optional(), | 79 | documentation: z.string().min(1).optional(), |
72 | escapePosition: z.number().int().nonnegative().optional(), | 80 | escapePosition: z.number().int().nonnegative().optional(), |
73 | textReplacements: replaceRegion.array(), | 81 | textReplacements: ReplaceRegion.array(), |
74 | editPositions: textRegion.array(), | 82 | editPositions: TextRegion.array(), |
75 | kind: z.string().min(1), | 83 | kind: z.string().min(1), |
76 | }); | 84 | }); |
77 | 85 | ||
78 | export type ContentAssistEntry = z.infer<typeof contentAssistEntry>; | 86 | export type ContentAssistEntry = z.infer<typeof ContentAssistEntry>; |
79 | 87 | ||
80 | export const contentAssistResult = documentStateResult.extend({ | 88 | export const ContentAssistResult = DocumentStateResult.extend({ |
81 | entries: contentAssistEntry.array(), | 89 | entries: ContentAssistEntry.array(), |
82 | }); | 90 | }); |
83 | 91 | ||
84 | export type ContentAssistResult = z.infer<typeof contentAssistResult>; | 92 | export type ContentAssistResult = z.infer<typeof ContentAssistResult>; |
85 | 93 | ||
86 | export const highlightingRegion = z.object({ | 94 | export const HighlightingRegion = z.object({ |
87 | offset: z.number().int().nonnegative(), | 95 | offset: z.number().int().nonnegative(), |
88 | length: z.number().int().nonnegative(), | 96 | length: z.number().int().nonnegative(), |
89 | styleClasses: z.string().min(1).array(), | 97 | styleClasses: z.string().min(1).array(), |
90 | }); | 98 | }); |
91 | 99 | ||
92 | export type HighlightingRegion = z.infer<typeof highlightingRegion>; | 100 | export type HighlightingRegion = z.infer<typeof HighlightingRegion>; |
93 | 101 | ||
94 | export const highlightingResult = z.object({ | 102 | export const highlightingResult = z.object({ |
95 | regions: highlightingRegion.array(), | 103 | regions: HighlightingRegion.array(), |
96 | }); | 104 | }); |
97 | 105 | ||
98 | export type HighlightingResult = z.infer<typeof highlightingResult>; | 106 | export type HighlightingResult = z.infer<typeof highlightingResult>; |
99 | 107 | ||
100 | export const occurrencesResult = documentStateResult.extend({ | 108 | export const OccurrencesResult = DocumentStateResult.extend({ |
101 | writeRegions: textRegion.array(), | 109 | writeRegions: TextRegion.array(), |
102 | readRegions: textRegion.array(), | 110 | readRegions: TextRegion.array(), |
103 | }); | 111 | }); |
104 | 112 | ||
105 | export type OccurrencesResult = z.infer<typeof occurrencesResult>; | 113 | export type OccurrencesResult = z.infer<typeof OccurrencesResult>; |
106 | 114 | ||
107 | export const formattingResult = documentStateResult.extend({ | 115 | export const FormattingResult = DocumentStateResult.extend({ |
108 | formattedText: z.string(), | 116 | formattedText: z.string(), |
109 | replaceRegion: textRegion, | 117 | replaceRegion: TextRegion, |
110 | }); | 118 | }); |
111 | 119 | ||
112 | export type FormattingResult = z.infer<typeof formattingResult>; | 120 | export type FormattingResult = z.infer<typeof FormattingResult>; |