aboutsummaryrefslogtreecommitdiffstats
path: root/subprojects/frontend/src
diff options
context:
space:
mode:
authorLibravatar Kristóf Marussy <kristof@marussy.com>2022-08-12 19:54:46 +0200
committerLibravatar Kristóf Marussy <kristof@marussy.com>2022-08-12 19:54:46 +0200
commitd22c3b0c257f5daf5b401988a35ab9ce981a2341 (patch)
tree0a661c927c37b52197326d1c05e211daf9bd19e5 /subprojects/frontend/src
parentfix(language): rule parsing test (diff)
downloadrefinery-d22c3b0c257f5daf5b401988a35ab9ce981a2341.tar.gz
refinery-d22c3b0c257f5daf5b401988a35ab9ce981a2341.tar.zst
refinery-d22c3b0c257f5daf5b401988a35ab9ce981a2341.zip
refactor(frontend): move from Webpack to Vite
Also overhaulds the building and linting for frontend assets.
Diffstat (limited to 'subprojects/frontend/src')
-rw-r--r--subprojects/frontend/src/App.tsx33
-rw-r--r--subprojects/frontend/src/Loading.tsx19
-rw-r--r--subprojects/frontend/src/RootStore.tsx15
-rw-r--r--subprojects/frontend/src/editor/EditorArea.tsx41
-rw-r--r--subprojects/frontend/src/editor/EditorButtons.tsx32
-rw-r--r--subprojects/frontend/src/editor/EditorParent.ts74
-rw-r--r--subprojects/frontend/src/editor/EditorStore.ts81
-rw-r--r--subprojects/frontend/src/editor/GenerateButton.tsx10
-rw-r--r--subprojects/frontend/src/editor/defineDecorationSetExtension.ts (renamed from subprojects/frontend/src/editor/decorationSetExtension.ts)19
-rw-r--r--subprojects/frontend/src/editor/findOccurrences.ts13
-rw-r--r--subprojects/frontend/src/editor/semanticHighlighting.ts22
-rw-r--r--subprojects/frontend/src/global.d.ts11
-rw-r--r--subprojects/frontend/src/index.html16
-rw-r--r--subprojects/frontend/src/index.scss16
-rw-r--r--subprojects/frontend/src/index.tsx46
-rw-r--r--subprojects/frontend/src/language/folding.ts9
-rw-r--r--subprojects/frontend/src/language/indentation.ts19
-rw-r--r--subprojects/frontend/src/language/problem.grammar2
-rw-r--r--subprojects/frontend/src/language/problemLanguageSupport.ts11
-rw-r--r--subprojects/frontend/src/language/props.ts2
-rw-r--r--subprojects/frontend/src/theme/EditorTheme.ts46
-rw-r--r--subprojects/frontend/src/theme/ThemeProvider.tsx61
-rw-r--r--subprojects/frontend/src/theme/ThemeStore.ts64
-rw-r--r--subprojects/frontend/src/themeVariables.module.scss9
-rw-r--r--subprojects/frontend/src/themes.scss38
-rw-r--r--subprojects/frontend/src/utils/ConditionVariable.ts6
-rw-r--r--subprojects/frontend/src/utils/PendingTask.ts4
-rw-r--r--subprojects/frontend/src/utils/Timer.ts2
-rw-r--r--subprojects/frontend/src/utils/getLogger.ts (renamed from subprojects/frontend/src/utils/logger.ts)34
-rw-r--r--subprojects/frontend/src/xtext/ContentAssistService.ts86
-rw-r--r--subprojects/frontend/src/xtext/HighlightingService.ts7
-rw-r--r--subprojects/frontend/src/xtext/OccurrencesService.ts34
-rw-r--r--subprojects/frontend/src/xtext/UpdateService.ts69
-rw-r--r--subprojects/frontend/src/xtext/ValidationService.ts18
-rw-r--r--subprojects/frontend/src/xtext/XtextClient.ts53
-rw-r--r--subprojects/frontend/src/xtext/XtextWebSocketClient.ts94
-rw-r--r--subprojects/frontend/src/xtext/xtextMessages.ts30
-rw-r--r--subprojects/frontend/src/xtext/xtextServiceResults.ts92
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 @@
1import MenuIcon from '@mui/icons-material/Menu';
1import AppBar from '@mui/material/AppBar'; 2import AppBar from '@mui/material/AppBar';
2import Box from '@mui/material/Box'; 3import Box from '@mui/material/Box';
3import IconButton from '@mui/material/IconButton'; 4import IconButton from '@mui/material/IconButton';
4import Toolbar from '@mui/material/Toolbar'; 5import Toolbar from '@mui/material/Toolbar';
5import Typography from '@mui/material/Typography'; 6import Typography from '@mui/material/Typography';
6import MenuIcon from '@mui/icons-material/Menu';
7import React from 'react'; 7import React from 'react';
8 8
9import { EditorArea } from './editor/EditorArea'; 9import EditorArea from './editor/EditorArea';
10import { EditorButtons } from './editor/EditorButtons'; 10import EditorButtons from './editor/EditorButtons';
11import { GenerateButton } from './editor/GenerateButton'; 11import GenerateButton from './editor/GenerateButton';
12 12
13export function App(): JSX.Element { 13export 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 @@
1import CircularProgress from '@mui/material/CircularProgress';
2import { styled } from '@mui/material/styles';
3import React from 'react';
4
5const LoadingRoot = styled('div')({
6 width: '100vw',
7 height: '100vh',
8 display: 'flex',
9 alignItems: 'center',
10 justifyContent: 'center',
11});
12
13export 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 @@
1import React, { createContext, useContext } from 'react'; 1import React, { createContext, useContext } from 'react';
2 2
3import { EditorStore } from './editor/EditorStore'; 3import EditorStore from './editor/EditorStore';
4import { ThemeStore } from './theme/ThemeStore'; 4import ThemeStore from './theme/ThemeStore';
5 5
6export class RootStore { 6export 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
25export function RootStoreProvider({ children, rootStore }: RootStoreProviderProps): JSX.Element { 25export 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 @@
1import { Command, EditorView } from '@codemirror/view';
2import { closeSearchPanel, openSearchPanel } from '@codemirror/search';
3import { closeLintPanel, openLintPanel } from '@codemirror/lint'; 1import { closeLintPanel, openLintPanel } from '@codemirror/lint';
2import { closeSearchPanel, openSearchPanel } from '@codemirror/search';
3import { type Command, EditorView } from '@codemirror/view';
4import { observer } from 'mobx-react-lite'; 4import { observer } from 'mobx-react-lite';
5import React, { 5import React, { useCallback, useEffect, useRef, useState } from 'react';
6 useCallback,
7 useEffect,
8 useRef,
9 useState,
10} from 'react';
11 6
12import { EditorParent } from './EditorParent';
13import { useRootStore } from '../RootStore'; 7import { useRootStore } from '../RootStore';
14import { getLogger } from '../utils/logger'; 8import getLogger from '../utils/getLogger';
9
10import EditorParent from './EditorParent';
15 11
16const log = getLogger('editor.EditorArea'); 12const 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
73export const EditorArea = observer(() => { 69function 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
149export 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 @@
1import type { Diagnostic } from '@codemirror/lint'; 1import type { Diagnostic } from '@codemirror/lint';
2import { observer } from 'mobx-react-lite';
3import IconButton from '@mui/material/IconButton';
4import Stack from '@mui/material/Stack';
5import ToggleButton from '@mui/material/ToggleButton';
6import ToggleButtonGroup from '@mui/material/ToggleButtonGroup';
7import CheckIcon from '@mui/icons-material/Check'; 2import CheckIcon from '@mui/icons-material/Check';
8import ErrorIcon from '@mui/icons-material/Error'; 3import ErrorIcon from '@mui/icons-material/Error';
9import FormatListNumberedIcon from '@mui/icons-material/FormatListNumbered'; 4import FormatListNumberedIcon from '@mui/icons-material/FormatListNumbered';
@@ -13,6 +8,11 @@ import RedoIcon from '@mui/icons-material/Redo';
13import SearchIcon from '@mui/icons-material/Search'; 8import SearchIcon from '@mui/icons-material/Search';
14import UndoIcon from '@mui/icons-material/Undo'; 9import UndoIcon from '@mui/icons-material/Undo';
15import WarningIcon from '@mui/icons-material/Warning'; 10import WarningIcon from '@mui/icons-material/Warning';
11import IconButton from '@mui/material/IconButton';
12import Stack from '@mui/material/Stack';
13import ToggleButton from '@mui/material/ToggleButton';
14import ToggleButtonGroup from '@mui/material/ToggleButtonGroup';
15import { observer } from 'mobx-react-lite';
16import React from 'react'; 16import React from 'react';
17 17
18import { useRootStore } from '../RootStore'; 18import { 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
35export const EditorButtons = observer(() => { 35function 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
92export 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 @@
1import { styled } from '@mui/material/styles'; 1import { 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
20export const EditorParent = styled('div')(({ theme }) => { 20export 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';
24import { 24import { type Diagnostic, lintKeymap, setDiagnostics } from '@codemirror/lint';
25 Diagnostic,
26 lintKeymap,
27 setDiagnostics,
28} from '@codemirror/lint';
29import { search, searchKeymap } from '@codemirror/search'; 25import { search, searchKeymap } from '@codemirror/search';
30import { 26import {
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';
37import { 33import {
38 drawSelection, 34 drawSelection,
@@ -45,26 +41,25 @@ import {
45 rectangularSelection, 41 rectangularSelection,
46} from '@codemirror/view'; 42} from '@codemirror/view';
47import { classHighlighter } from '@lezer/highlight'; 43import { classHighlighter } from '@lezer/highlight';
48import { 44import { makeAutoObservable, observable, reaction } from 'mobx';
49 makeAutoObservable, 45
50 observable, 46import problemLanguageSupport from '../language/problemLanguageSupport';
51 reaction, 47import type ThemeStore from '../theme/ThemeStore';
52} from 'mobx'; 48import getLogger from '../utils/getLogger';
53 49import XtextClient from '../xtext/XtextClient';
54import { findOccurrences, IOccurrence, setOccurrences } from './findOccurrences'; 50
55import { problemLanguageSupport } from '../language/problemLanguageSupport'; 51import findOccurrences, {
56import { 52 type IOccurrence,
57 IHighlightRange, 53 setOccurrences,
58 semanticHighlighting, 54} from './findOccurrences';
55import semanticHighlighting, {
56 type IHighlightRange,
59 setSemanticHighlighting, 57 setSemanticHighlighting,
60} from './semanticHighlighting'; 58} from './semanticHighlighting';
61import type { ThemeStore } from '../theme/ThemeStore';
62import { getLogger } from '../utils/logger';
63import { XtextClient } from '../xtext/XtextClient';
64 59
65const log = getLogger('editor.EditorStore'); 60const log = getLogger('editor.EditorStore');
66 61
67export class EditorStore { 62export 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 @@
1import { observer } from 'mobx-react-lite';
2import Button from '@mui/material/Button';
3import PlayArrowIcon from '@mui/icons-material/PlayArrow'; 1import PlayArrowIcon from '@mui/icons-material/PlayArrow';
2import Button from '@mui/material/Button';
3import { observer } from 'mobx-react-lite';
4import React from 'react'; 4import React from 'react';
5 5
6import { useRootStore } from '../RootStore'; 6import { useRootStore } from '../RootStore';
7 7
8const GENERATE_LABEL = 'Generate'; 8const GENERATE_LABEL = 'Generate';
9 9
10export const GenerateButton = observer(() => { 10function 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
46export 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 @@
1import { StateEffect, StateField, TransactionSpec } from '@codemirror/state'; 1import { StateEffect, StateField, TransactionSpec } from '@codemirror/state';
2import { EditorView, Decoration, DecorationSet } from '@codemirror/view'; 2import { EditorView, Decoration, DecorationSet } from '@codemirror/view';
3 3
4export type TransactionSpecFactory = (decorations: DecorationSet) => TransactionSpec; 4export type TransactionSpecFactory = (
5 decorations: DecorationSet,
6) => TransactionSpec;
5 7
6export function decorationSetExtension(): [TransactionSpecFactory, StateField<DecorationSet>] { 8export 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 @@
1import { Range, RangeSet, type TransactionSpec } from '@codemirror/state'; 1import { type Range, RangeSet, type TransactionSpec } from '@codemirror/state';
2import { Decoration } from '@codemirror/view'; 2import { Decoration } from '@codemirror/view';
3 3
4import { decorationSetExtension } from './decorationSetExtension'; 4import defineDecorationSetExtension from './defineDecorationSetExtension';
5 5
6export interface IOccurrence { 6export 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
12const [setOccurrencesInteral, findOccurrences] = decorationSetExtension(); 12const [setOccurrencesInteral, findOccurrences] = defineDecorationSetExtension();
13 13
14const writeDecoration = Decoration.mark({ 14const 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
22export function setOccurrences(write: IOccurrence[], read: IOccurrence[]): TransactionSpec { 22export 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
34export { findOccurrences }; 37export 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 @@
1import { RangeSet, type TransactionSpec } from '@codemirror/state'; 1import { RangeSet, type TransactionSpec } from '@codemirror/state';
2import { Decoration } from '@codemirror/view'; 2import { Decoration } from '@codemirror/view';
3 3
4import { decorationSetExtension } from './decorationSetExtension'; 4import defineDecorationSetExtension from './defineDecorationSetExtension';
5 5
6export interface IHighlightRange { 6export 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
14const [setSemanticHighlightingInternal, semanticHighlighting] = decorationSetExtension(); 14const [setSemanticHighlightingInternal, semanticHighlighting] =
15 defineDecorationSetExtension();
15 16
16export function setSemanticHighlighting(ranges: IHighlightRange[]): TransactionSpec { 17export 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
23export { semanticHighlighting }; 31export 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 @@
1declare const DEBUG: boolean;
2
3declare const PACKAGE_NAME: string;
4
5declare const PACKAGE_VERSION: string;
6
7declare 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 @@
1import React from 'react';
2import { createRoot } from 'react-dom/client';
3import CssBaseline from '@mui/material/CssBaseline'; 1import CssBaseline from '@mui/material/CssBaseline';
2import React, { Suspense, lazy } from 'react';
3import { createRoot } from 'react-dom/client';
4import '@fontsource/jetbrains-mono/400.css';
5import '@fontsource/jetbrains-mono/400-italic.css';
6import '@fontsource/jetbrains-mono/700.css';
7import '@fontsource/jetbrains-mono/700-italic.css';
8import '@fontsource/jetbrains-mono/variable.css';
9import '@fontsource/jetbrains-mono/variable-italic.css';
10import '@fontsource/roboto/300.css';
11import '@fontsource/roboto/300-italic.css';
12import '@fontsource/roboto/400.css';
13import '@fontsource/roboto/400-italic.css';
14import '@fontsource/roboto/500.css';
15import '@fontsource/roboto/500-italic.css';
16import '@fontsource/roboto/700.css';
17import '@fontsource/roboto/700-italic.css';
4 18
5import { App } from './App'; 19import Loading from './Loading';
6import { RootStore, RootStoreProvider } from './RootStore'; 20import RootStore, { RootStoreProvider } from './RootStore';
7import { ThemeProvider } from './theme/ThemeProvider'; 21import ThemeProvider from './theme/ThemeProvider';
8import { getLogger } from './utils/logger'; 22import getLogger from './utils/getLogger';
9
10import './index.scss';
11 23
12const log = getLogger('index'); 24const log = getLogger('index');
13 25
@@ -60,13 +72,19 @@ scope Family = 1, Person += 5..10.
60 72
61const rootStore = new RootStore(initialValue); 73const rootStore = new RootStore(initialValue);
62 74
75const App = lazy(() => import('./App.js'));
76
63const app = ( 77const 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
72const rootElement = document.getElementById('app'); 90const 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 @@
1import { EditorState } from '@codemirror/state'; 1import type { EditorState } from '@codemirror/state';
2import type { SyntaxNode } from '@lezer/common'; 2import type { SyntaxNode } from '@lezer/common';
3 3
4export type FoldRange = { from: number, to: number }; 4export 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 */
50export function foldDeclaration(node: SyntaxNode, state: EditorState): FoldRange | null { 50export 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 @@
1import { TreeIndentContext } from '@codemirror/language'; 1import 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 */
13function findAlignmentAfterOpening(context: TreeIndentContext): number | null { 13function 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 */
61function indentDeclarationStrategy(context: TreeIndentContext, units: number): number { 59function 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';
9import { styleTags, tags as t } from '@lezer/highlight'; 9import { styleTags, tags as t } from '@lezer/highlight';
10import { LRParser } from '@lezer/lr';
11 10
12import { parser } from '../../build/generated/sources/lezer/problem';
13import { 11import {
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';
22import { parser } from './problem.grammar';
24 23
25const parserWithMetadata = (parser as LRParser).configure({ 24const 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
89export function problemLanguageSupport(): LanguageSupport { 88export 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
1import { NodeProp } from '@lezer/common'; 3import { NodeProp } from '@lezer/common';
2 4
3export const implicitCompletion = new NodeProp({ 5export 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 @@
1import type { PaletteMode } from '@mui/material'; 1enum EditorTheme {
2
3import cssVariables from '../themeVariables.module.scss';
4
5export enum EditorTheme {
6 Light, 2 Light,
7 Dark, 3 Dark,
4 Default = EditorTheme.Dark,
8} 5}
9 6
10export class EditorThemeData { 7export 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
42export const DEFAULT_THEME = EditorTheme.Dark;
43
44export 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 @@
1import {
2 createTheme,
3 responsiveFontSizes,
4 type ThemeOptions,
5 ThemeProvider as MaterialUiThemeProvider,
6} from '@mui/material/styles';
1import { observer } from 'mobx-react-lite'; 7import { observer } from 'mobx-react-lite';
2import { ThemeProvider as MaterialUiThemeProvider } from '@mui/material/styles';
3import React, { type ReactNode } from 'react'; 8import React, { type ReactNode } from 'react';
4 9
5import { useRootStore } from '../RootStore'; 10import { useRootStore } from '../RootStore';
6 11
7export const ThemeProvider: React.FC<{ children: ReactNode }> = observer(({ children }) => { 12import EditorTheme from './EditorTheme';
8 const { themeStore } = useRootStore(); 13
14function 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
37function 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
58ThemeProvider.defaultProps = {
59 children: undefined,
60};
61
62export 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 @@
1import { makeAutoObservable } from 'mobx'; 1import { makeAutoObservable } from 'mobx';
2import {
3 Theme,
4 createTheme,
5 responsiveFontSizes,
6} from '@mui/material/styles';
7 2
8import { 3import EditorTheme from './EditorTheme';
9 EditorTheme,
10 EditorThemeData,
11 DEFAULT_THEME,
12 EDITOR_THEMES,
13} from './EditorTheme';
14 4
15export class ThemeStore { 5export 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 @@
1import { getLogger } from './logger'; 1import PendingTask from './PendingTask';
2import { PendingTask } from './PendingTask'; 2import getLogger from './getLogger';
3 3
4const log = getLogger('utils.ConditionVariable'); 4const log = getLogger('utils.ConditionVariable');
5 5
6export type Condition = () => boolean; 6export type Condition = () => boolean;
7 7
8export class ConditionVariable { 8export 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 @@
1import { getLogger } from './logger'; 1import getLogger from './getLogger';
2 2
3const log = getLogger('utils.PendingTask'); 3const log = getLogger('utils.PendingTask');
4 4
5export class PendingTask<T> { 5export 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 @@
1export class Timer { 1export 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 @@
1import styles, { CSPair } from 'ansi-styles'; 1import styles, { type CSPair } from 'ansi-styles';
2import log from 'loglevel'; 2import log from 'loglevel';
3import * as prefix from 'loglevel-plugin-prefix'; 3import prefix from 'loglevel-plugin-prefix';
4 4
5const colors: Partial<Record<string, CSPair>> = { 5const 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
13prefix.reg(log); 13prefix.reg(log);
14 14
15if (DEBUG) { 15if (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
38const appLogger = log.getLogger(PACKAGE_NAME); 42const appLogger = log.getLogger(import.meta.env.VITE_PACKAGE_NAME);
39 43
40appLogger.info('Version:', PACKAGE_NAME, PACKAGE_VERSION); 44appLogger.info(
41appLogger.info('Debug mode:', DEBUG); 45 'Version:',
46 import.meta.env.VITE_PACKAGE_NAME,
47 import.meta.env.VITE_PACKAGE_VERSION,
48);
49appLogger.info('Debug mode:', import.meta.env.DEV);
42 50
43export function getLoggerFromRoot(name: string | symbol): log.Logger { 51export 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 );
47export 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';
8import escapeStringRegexp from 'escape-string-regexp'; 8import escapeStringRegexp from 'escape-string-regexp';
9 9
10import { implicitCompletion } from '../language/props'; 10import { implicitCompletion } from '../language/props';
11import type { UpdateService } from './UpdateService'; 11import getLogger from '../utils/getLogger';
12import { getLogger } from '../utils/logger'; 12
13import type UpdateService from './UpdateService';
13import type { ContentAssistEntry } from './xtextServiceResults'; 14import type { ContentAssistEntry } from './xtextServiceResults';
14 15
15const PROPOSALS_LIMIT = 1000; 16const PROPOSALS_LIMIT = 1000;
@@ -48,10 +49,13 @@ function findToken({ pos, state }: CompletionContext): IFoundToken | null {
48 }; 49 };
49} 50}
50 51
51function shouldCompleteImplicitly(token: IFoundToken | null, context: CompletionContext): boolean { 52function 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
57function computeSpan(prefix: string, entryCount: number): RegExp { 61function 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
97export class ContentAssistService { 107export 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 @@
1import type { EditorStore } from '../editor/EditorStore'; 1import type EditorStore from '../editor/EditorStore';
2import type { IHighlightRange } from '../editor/semanticHighlighting'; 2import type { IHighlightRange } from '../editor/semanticHighlighting';
3import type { UpdateService } from './UpdateService'; 3
4import type UpdateService from './UpdateService';
4import { highlightingResult } from './xtextServiceResults'; 5import { highlightingResult } from './xtextServiceResults';
5 6
6export class HighlightingService { 7export 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 @@
1import { Transaction } from '@codemirror/state'; 1import { Transaction } from '@codemirror/state';
2 2
3import type { EditorStore } from '../editor/EditorStore'; 3import type EditorStore from '../editor/EditorStore';
4import type { IOccurrence } from '../editor/findOccurrences'; 4import type { IOccurrence } from '../editor/findOccurrences';
5import type { UpdateService } from './UpdateService'; 5import Timer from '../utils/Timer';
6import { getLogger } from '../utils/logger'; 6import getLogger from '../utils/getLogger';
7import { Timer } from '../utils/Timer'; 7
8import { XtextWebSocketClient } from './XtextWebSocketClient'; 8import type UpdateService from './UpdateService';
9import type XtextWebSocketClient from './XtextWebSocketClient';
9import { 10import {
10 isConflictResult, 11 isConflictResult,
11 occurrencesResult, 12 OccurrencesResult,
12 TextRegion, 13 type TextRegion,
13} from './xtextServiceResults'; 14} from './xtextServiceResults';
14 15
15const FIND_OCCURRENCES_TIMEOUT_MS = 1000; 16const FIND_OCCURRENCES_TIMEOUT_MS = 1000;
@@ -33,7 +34,7 @@ function transformOccurrences(regions: TextRegion[]): IOccurrence[] {
33 return occurrences; 34 return occurrences;
34} 35}
35 36
36export class OccurrencesService { 37export 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 @@
1import { 1import {
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';
8import { nanoid } from 'nanoid'; 8import { nanoid } from 'nanoid';
9 9
10import type { EditorStore } from '../editor/EditorStore'; 10import type EditorStore from '../editor/EditorStore';
11import type { XtextWebSocketClient } from './XtextWebSocketClient'; 11import ConditionVariable from '../utils/ConditionVariable';
12import { ConditionVariable } from '../utils/ConditionVariable'; 12import Timer from '../utils/Timer';
13import { getLogger } from '../utils/logger'; 13import getLogger from '../utils/getLogger';
14import { Timer } from '../utils/Timer'; 14
15import type XtextWebSocketClient from './XtextWebSocketClient';
15import { 16import {
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
35export class UpdateService { 36export 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 @@
1import type { Diagnostic } from '@codemirror/lint'; 1import type { Diagnostic } from '@codemirror/lint';
2 2
3import type { EditorStore } from '../editor/EditorStore'; 3import type EditorStore from '../editor/EditorStore';
4import type { UpdateService } from './UpdateService';
5import { validationResult } from './xtextServiceResults';
6 4
7export class ValidationService { 5import type UpdateService from './UpdateService';
6import { ValidationResult } from './xtextServiceResults';
7
8export 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';
5import type { Transaction } from '@codemirror/state'; 5import type { Transaction } from '@codemirror/state';
6 6
7import type { EditorStore } from '../editor/EditorStore'; 7import type EditorStore from '../editor/EditorStore';
8import { ContentAssistService } from './ContentAssistService'; 8import getLogger from '../utils/getLogger';
9import { HighlightingService } from './HighlightingService'; 9
10import { OccurrencesService } from './OccurrencesService'; 10import ContentAssistService from './ContentAssistService';
11import { UpdateService } from './UpdateService'; 11import HighlightingService from './HighlightingService';
12import { getLogger } from '../utils/logger'; 12import OccurrencesService from './OccurrencesService';
13import { ValidationService } from './ValidationService'; 13import UpdateService from './UpdateService';
14import { XtextWebSocketClient } from './XtextWebSocketClient'; 14import ValidationService from './ValidationService';
15import { XtextWebPushService } from './xtextMessages'; 15import XtextWebSocketClient from './XtextWebSocketClient';
16import type { XtextWebPushService } from './xtextMessages';
16 17
17const log = getLogger('xtext.XtextClient'); 18const log = getLogger('xtext.XtextClient');
18 19
19export class XtextClient { 20export 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 @@
1import { nanoid } from 'nanoid'; 1import { nanoid } from 'nanoid';
2 2
3import { getLogger } from '../utils/logger'; 3import PendingTask from '../utils/PendingTask';
4import { PendingTask } from '../utils/PendingTask'; 4import Timer from '../utils/Timer';
5import { Timer } from '../utils/Timer'; 5import getLogger from '../utils/getLogger';
6
6import { 7import {
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';
13import { pongResult } from './xtextServiceResults'; 14import { PongResult } from './xtextServiceResults';
14 15
15const XTEXT_SUBPROTOCOL_V1 = 'tools.refinery.language.web.xtext.v1'; 16const XTEXT_SUBPROTOCOL_V1 = 'tools.refinery.language.web.xtext.v1';
16 17
@@ -18,7 +19,8 @@ const WEBSOCKET_CLOSE_OK = 1000;
18 19
19const RECONNECT_DELAY_MS = [200, 1000, 5000, 30_000]; 20const RECONNECT_DELAY_MS = [200, 1000, 5000, 30_000];
20 21
21const MAX_RECONNECT_DELAY_MS = RECONNECT_DELAY_MS[RECONNECT_DELAY_MS.length - 1]; 22const MAX_RECONNECT_DELAY_MS =
23 RECONNECT_DELAY_MS[RECONNECT_DELAY_MS.length - 1];
22 24
23const BACKGROUND_IDLE_TIMEOUT_MS = 5 * 60 * 1000; 25const BACKGROUND_IDLE_TIMEOUT_MS = 5 * 60 * 1000;
24 26
@@ -47,7 +49,7 @@ enum State {
47 TimedOut, 49 TimedOut,
48} 50}
49 51
50export class XtextWebSocketClient { 52export 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
1import { z } from 'zod'; 3import { z } from 'zod';
2 4
3export const xtextWebRequest = z.object({ 5export 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
8export type XtextWebRequest = z.infer<typeof xtextWebRequest>; 10export type XtextWebRequest = z.infer<typeof XtextWebRequest>;
9 11
10export const xtextWebOkResponse = z.object({ 12export 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
15export type XtextWebOkResponse = z.infer<typeof xtextWebOkResponse>; 17export type XtextWebOkResponse = z.infer<typeof XtextWebOkResponse>;
16 18
17export const xtextWebErrorKind = z.enum(['request', 'server']); 19export const XtextWebErrorKind = z.enum(['request', 'server']);
18 20
19export type XtextWebErrorKind = z.infer<typeof xtextWebErrorKind>; 21export type XtextWebErrorKind = z.infer<typeof XtextWebErrorKind>;
20 22
21export const xtextWebErrorResponse = z.object({ 23export 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
27export type XtextWebErrorResponse = z.infer<typeof xtextWebErrorResponse>; 29export type XtextWebErrorResponse = z.infer<typeof XtextWebErrorResponse>;
28 30
29export const xtextWebPushService = z.enum(['highlight', 'validate']); 31export const XtextWebPushService = z.enum(['highlight', 'validate']);
30 32
31export type XtextWebPushService = z.infer<typeof xtextWebPushService>; 33export type XtextWebPushService = z.infer<typeof XtextWebPushService>;
32 34
33export const xtextWebPushMessage = z.object({ 35export 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
40export type XtextWebPushMessage = z.infer<typeof xtextWebPushMessage>; 42export 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
1import { z } from 'zod'; 3import { z } from 'zod';
2 4
3export const pongResult = z.object({ 5export const PongResult = z.object({
4 pong: z.string().min(1), 6 pong: z.string().min(1),
5}); 7});
6 8
7export type PongResult = z.infer<typeof pongResult>; 9export type PongResult = z.infer<typeof PongResult>;
8 10
9export const documentStateResult = z.object({ 11export const DocumentStateResult = z.object({
10 stateId: z.string().min(1), 12 stateId: z.string().min(1),
11}); 13});
12 14
13export type DocumentStateResult = z.infer<typeof documentStateResult>; 15export type DocumentStateResult = z.infer<typeof DocumentStateResult>;
14 16
15export const conflict = z.enum(['invalidStateId', 'canceled']); 17export const Conflict = z.enum(['invalidStateId', 'canceled']);
16 18
17export type Conflict = z.infer<typeof conflict>; 19export type Conflict = z.infer<typeof Conflict>;
18 20
19export const serviceConflictResult = z.object({ 21export const ServiceConflictResult = z.object({
20 conflict, 22 conflict: Conflict,
21}); 23});
22 24
23export type ServiceConflictResult = z.infer<typeof serviceConflictResult>; 25export type ServiceConflictResult = z.infer<typeof ServiceConflictResult>;
24 26
25export function isConflictResult(result: unknown, conflictType: Conflict): boolean { 27export 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
30export const severity = z.enum(['error', 'warning', 'info', 'ignore']); 38export const Severity = z.enum(['error', 'warning', 'info', 'ignore']);
31 39
32export type Severity = z.infer<typeof severity>; 40export type Severity = z.infer<typeof Severity>;
33 41
34export const issue = z.object({ 42export 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
43export type Issue = z.infer<typeof issue>; 51export type Issue = z.infer<typeof Issue>;
44 52
45export const validationResult = z.object({ 53export const ValidationResult = z.object({
46 issues: issue.array(), 54 issues: Issue.array(),
47}); 55});
48 56
49export type ValidationResult = z.infer<typeof validationResult>; 57export type ValidationResult = z.infer<typeof ValidationResult>;
50 58
51export const replaceRegion = z.object({ 59export 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
57export type ReplaceRegion = z.infer<typeof replaceRegion>; 65export type ReplaceRegion = z.infer<typeof ReplaceRegion>;
58 66
59export const textRegion = z.object({ 67export 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
64export type TextRegion = z.infer<typeof textRegion>; 72export type TextRegion = z.infer<typeof TextRegion>;
65 73
66export const contentAssistEntry = z.object({ 74export 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
78export type ContentAssistEntry = z.infer<typeof contentAssistEntry>; 86export type ContentAssistEntry = z.infer<typeof ContentAssistEntry>;
79 87
80export const contentAssistResult = documentStateResult.extend({ 88export const ContentAssistResult = DocumentStateResult.extend({
81 entries: contentAssistEntry.array(), 89 entries: ContentAssistEntry.array(),
82}); 90});
83 91
84export type ContentAssistResult = z.infer<typeof contentAssistResult>; 92export type ContentAssistResult = z.infer<typeof ContentAssistResult>;
85 93
86export const highlightingRegion = z.object({ 94export 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
92export type HighlightingRegion = z.infer<typeof highlightingRegion>; 100export type HighlightingRegion = z.infer<typeof HighlightingRegion>;
93 101
94export const highlightingResult = z.object({ 102export const highlightingResult = z.object({
95 regions: highlightingRegion.array(), 103 regions: HighlightingRegion.array(),
96}); 104});
97 105
98export type HighlightingResult = z.infer<typeof highlightingResult>; 106export type HighlightingResult = z.infer<typeof highlightingResult>;
99 107
100export const occurrencesResult = documentStateResult.extend({ 108export const OccurrencesResult = DocumentStateResult.extend({
101 writeRegions: textRegion.array(), 109 writeRegions: TextRegion.array(),
102 readRegions: textRegion.array(), 110 readRegions: TextRegion.array(),
103}); 111});
104 112
105export type OccurrencesResult = z.infer<typeof occurrencesResult>; 113export type OccurrencesResult = z.infer<typeof OccurrencesResult>;
106 114
107export const formattingResult = documentStateResult.extend({ 115export const FormattingResult = DocumentStateResult.extend({
108 formattedText: z.string(), 116 formattedText: z.string(),
109 replaceRegion: textRegion, 117 replaceRegion: TextRegion,
110}); 118});
111 119
112export type FormattingResult = z.infer<typeof formattingResult>; 120export type FormattingResult = z.infer<typeof FormattingResult>;