diff options
author | Kristóf Marussy <kristof@marussy.com> | 2023-08-20 19:41:32 +0200 |
---|---|---|
committer | Kristóf Marussy <kristof@marussy.com> | 2023-08-20 20:29:02 +0200 |
commit | a3f1e6872f4f768d14899a1e70bbdc14f32e478d (patch) | |
tree | b2daf0c81724f31ee190f5d63eb42988086dabf2 /subprojects/frontend | |
parent | fix: nullary model initialization (diff) | |
download | refinery-a3f1e6872f4f768d14899a1e70bbdc14f32e478d.tar.gz refinery-a3f1e6872f4f768d14899a1e70bbdc14f32e478d.tar.zst refinery-a3f1e6872f4f768d14899a1e70bbdc14f32e478d.zip |
feat: improve semantics error reporting
Also makes model seeds cancellable to reduce server load during semantic
analysis.
Diffstat (limited to 'subprojects/frontend')
-rw-r--r-- | subprojects/frontend/src/editor/AnalysisErrorNotification.tsx | 74 | ||||
-rw-r--r-- | subprojects/frontend/src/editor/AnimatedButton.tsx | 9 | ||||
-rw-r--r-- | subprojects/frontend/src/editor/EditorButtons.tsx | 6 | ||||
-rw-r--r-- | subprojects/frontend/src/editor/EditorErrors.tsx | 93 | ||||
-rw-r--r-- | subprojects/frontend/src/editor/EditorPane.tsx | 2 | ||||
-rw-r--r-- | subprojects/frontend/src/editor/EditorStore.ts | 39 | ||||
-rw-r--r-- | subprojects/frontend/src/editor/EditorTheme.ts | 4 | ||||
-rw-r--r-- | subprojects/frontend/src/editor/GenerateButton.tsx | 48 | ||||
-rw-r--r-- | subprojects/frontend/src/xtext/SemanticsService.ts | 28 | ||||
-rw-r--r-- | subprojects/frontend/src/xtext/UpdateService.ts | 2 | ||||
-rw-r--r-- | subprojects/frontend/src/xtext/ValidationService.ts | 44 | ||||
-rw-r--r-- | subprojects/frontend/src/xtext/XtextClient.ts | 7 | ||||
-rw-r--r-- | subprojects/frontend/src/xtext/xtextServiceResults.ts | 7 |
13 files changed, 317 insertions, 46 deletions
diff --git a/subprojects/frontend/src/editor/AnalysisErrorNotification.tsx b/subprojects/frontend/src/editor/AnalysisErrorNotification.tsx new file mode 100644 index 00000000..591a3600 --- /dev/null +++ b/subprojects/frontend/src/editor/AnalysisErrorNotification.tsx | |||
@@ -0,0 +1,74 @@ | |||
1 | /* | ||
2 | * SPDX-FileCopyrightText: 2021-2023 The Refinery Authors <https://refinery.tools/> | ||
3 | * | ||
4 | * SPDX-License-Identifier: EPL-2.0 | ||
5 | */ | ||
6 | |||
7 | import { reaction } from 'mobx'; | ||
8 | import { type SnackbarKey, useSnackbar } from 'notistack'; | ||
9 | import { useEffect, useState } from 'react'; | ||
10 | |||
11 | import type EditorStore from './EditorStore'; | ||
12 | |||
13 | function MessageObserver({ | ||
14 | editorStore, | ||
15 | }: { | ||
16 | editorStore: EditorStore; | ||
17 | }): React.ReactNode { | ||
18 | const [message, setMessage] = useState( | ||
19 | editorStore.delayedErrors.semanticsError ?? '', | ||
20 | ); | ||
21 | // Instead of making this component an `observer`, | ||
22 | // we only update the message is one is present to make sure that the | ||
23 | // disappear animation has a chance to complete. | ||
24 | useEffect( | ||
25 | () => | ||
26 | reaction( | ||
27 | () => editorStore.delayedErrors.semanticsError, | ||
28 | (newMessage) => { | ||
29 | if (newMessage !== undefined) { | ||
30 | setMessage(newMessage); | ||
31 | } | ||
32 | }, | ||
33 | { fireImmediately: false }, | ||
34 | ), | ||
35 | [editorStore], | ||
36 | ); | ||
37 | return message; | ||
38 | } | ||
39 | |||
40 | export default function AnalysisErrorNotification({ | ||
41 | editorStore, | ||
42 | }: { | ||
43 | editorStore: EditorStore; | ||
44 | }): null { | ||
45 | const { enqueueSnackbar, closeSnackbar } = useSnackbar(); | ||
46 | useEffect(() => { | ||
47 | let key: SnackbarKey | undefined; | ||
48 | const disposer = reaction( | ||
49 | () => editorStore.delayedErrors.semanticsError !== undefined, | ||
50 | (hasError) => { | ||
51 | if (hasError) { | ||
52 | if (key === undefined) { | ||
53 | key = enqueueSnackbar({ | ||
54 | message: <MessageObserver editorStore={editorStore} />, | ||
55 | variant: 'error', | ||
56 | persist: true, | ||
57 | }); | ||
58 | } | ||
59 | } else if (key !== undefined) { | ||
60 | closeSnackbar(key); | ||
61 | key = undefined; | ||
62 | } | ||
63 | }, | ||
64 | { fireImmediately: true }, | ||
65 | ); | ||
66 | return () => { | ||
67 | disposer(); | ||
68 | if (key !== undefined) { | ||
69 | closeSnackbar(key); | ||
70 | } | ||
71 | }; | ||
72 | }, [editorStore, enqueueSnackbar, closeSnackbar]); | ||
73 | return null; | ||
74 | } | ||
diff --git a/subprojects/frontend/src/editor/AnimatedButton.tsx b/subprojects/frontend/src/editor/AnimatedButton.tsx index dbbda618..24ec69be 100644 --- a/subprojects/frontend/src/editor/AnimatedButton.tsx +++ b/subprojects/frontend/src/editor/AnimatedButton.tsx | |||
@@ -48,7 +48,7 @@ export default function AnimatedButton({ | |||
48 | onClick?: () => void; | 48 | onClick?: () => void; |
49 | color: 'error' | 'warning' | 'primary' | 'inherit'; | 49 | color: 'error' | 'warning' | 'primary' | 'inherit'; |
50 | disabled?: boolean; | 50 | disabled?: boolean; |
51 | startIcon: JSX.Element; | 51 | startIcon?: JSX.Element; |
52 | sx?: SxProps<Theme> | undefined; | 52 | sx?: SxProps<Theme> | undefined; |
53 | children?: ReactNode; | 53 | children?: ReactNode; |
54 | }): JSX.Element { | 54 | }): JSX.Element { |
@@ -79,7 +79,11 @@ export default function AnimatedButton({ | |||
79 | className="rounded shaded" | 79 | className="rounded shaded" |
80 | disabled={disabled ?? false} | 80 | disabled={disabled ?? false} |
81 | startIcon={startIcon} | 81 | startIcon={startIcon} |
82 | width={width === undefined ? 'auto' : `calc(${width} + 50px)`} | 82 | width={ |
83 | width === undefined | ||
84 | ? 'auto' | ||
85 | : `calc(${width} + ${startIcon === undefined ? 28 : 50}px)` | ||
86 | } | ||
83 | > | 87 | > |
84 | <Box | 88 | <Box |
85 | display="flex" | 89 | display="flex" |
@@ -100,6 +104,7 @@ AnimatedButton.defaultProps = { | |||
100 | 'aria-label': undefined, | 104 | 'aria-label': undefined, |
101 | onClick: undefined, | 105 | onClick: undefined, |
102 | disabled: false, | 106 | disabled: false, |
107 | startIcon: undefined, | ||
103 | sx: undefined, | 108 | sx: undefined, |
104 | children: undefined, | 109 | children: undefined, |
105 | }; | 110 | }; |
diff --git a/subprojects/frontend/src/editor/EditorButtons.tsx b/subprojects/frontend/src/editor/EditorButtons.tsx index 9b187e5c..ca51f975 100644 --- a/subprojects/frontend/src/editor/EditorButtons.tsx +++ b/subprojects/frontend/src/editor/EditorButtons.tsx | |||
@@ -5,8 +5,8 @@ | |||
5 | */ | 5 | */ |
6 | 6 | ||
7 | import type { Diagnostic } from '@codemirror/lint'; | 7 | import type { Diagnostic } from '@codemirror/lint'; |
8 | import CancelIcon from '@mui/icons-material/Cancel'; | ||
8 | import CheckIcon from '@mui/icons-material/Check'; | 9 | import CheckIcon from '@mui/icons-material/Check'; |
9 | import ErrorIcon from '@mui/icons-material/Error'; | ||
10 | import FormatListNumberedIcon from '@mui/icons-material/FormatListNumbered'; | 10 | import FormatListNumberedIcon from '@mui/icons-material/FormatListNumbered'; |
11 | import FormatPaint from '@mui/icons-material/FormatPaint'; | 11 | import FormatPaint from '@mui/icons-material/FormatPaint'; |
12 | import InfoOutlinedIcon from '@mui/icons-material/InfoOutlined'; | 12 | import InfoOutlinedIcon from '@mui/icons-material/InfoOutlined'; |
@@ -28,7 +28,7 @@ import type EditorStore from './EditorStore'; | |||
28 | function getLintIcon(severity: Diagnostic['severity'] | undefined) { | 28 | function getLintIcon(severity: Diagnostic['severity'] | undefined) { |
29 | switch (severity) { | 29 | switch (severity) { |
30 | case 'error': | 30 | case 'error': |
31 | return <ErrorIcon fontSize="small" />; | 31 | return <CancelIcon fontSize="small" />; |
32 | case 'warning': | 32 | case 'warning': |
33 | return <WarningIcon fontSize="small" />; | 33 | return <WarningIcon fontSize="small" />; |
34 | case 'info': | 34 | case 'info': |
@@ -95,7 +95,7 @@ export default observer(function EditorButtons({ | |||
95 | })} | 95 | })} |
96 | value="show-lint-panel" | 96 | value="show-lint-panel" |
97 | > | 97 | > |
98 | {getLintIcon(editorStore?.highestDiagnosticLevel)} | 98 | {getLintIcon(editorStore?.delayedErrors?.highestDiagnosticLevel)} |
99 | </ToggleButton> | 99 | </ToggleButton> |
100 | </ToggleButtonGroup> | 100 | </ToggleButtonGroup> |
101 | <IconButton | 101 | <IconButton |
diff --git a/subprojects/frontend/src/editor/EditorErrors.tsx b/subprojects/frontend/src/editor/EditorErrors.tsx new file mode 100644 index 00000000..40becf7e --- /dev/null +++ b/subprojects/frontend/src/editor/EditorErrors.tsx | |||
@@ -0,0 +1,93 @@ | |||
1 | /* | ||
2 | * SPDX-FileCopyrightText: 2021-2023 The Refinery Authors <https://refinery.tools/> | ||
3 | * | ||
4 | * SPDX-License-Identifier: EPL-2.0 | ||
5 | */ | ||
6 | |||
7 | import { Diagnostic } from '@codemirror/lint'; | ||
8 | import { type IReactionDisposer, makeAutoObservable, reaction } from 'mobx'; | ||
9 | |||
10 | import type EditorStore from './EditorStore'; | ||
11 | |||
12 | const HYSTERESIS_TIME_MS = 250; | ||
13 | |||
14 | export interface State { | ||
15 | analyzing: boolean; | ||
16 | errorCount: number; | ||
17 | warningCount: number; | ||
18 | infoCount: number; | ||
19 | semanticsError: string | undefined; | ||
20 | } | ||
21 | |||
22 | export default class EditorErrors implements State { | ||
23 | private readonly disposer: IReactionDisposer; | ||
24 | |||
25 | private timer: number | undefined; | ||
26 | |||
27 | analyzing = false; | ||
28 | |||
29 | errorCount = 0; | ||
30 | |||
31 | warningCount = 0; | ||
32 | |||
33 | infoCount = 0; | ||
34 | |||
35 | semanticsError: string | undefined; | ||
36 | |||
37 | constructor(private readonly store: EditorStore) { | ||
38 | this.updateImmediately(this.getNextState()); | ||
39 | makeAutoObservable<EditorErrors, 'disposer' | 'timer'>(this, { | ||
40 | disposer: false, | ||
41 | timer: false, | ||
42 | }); | ||
43 | this.disposer = reaction( | ||
44 | () => this.getNextState(), | ||
45 | (nextState) => { | ||
46 | if (this.timer !== undefined) { | ||
47 | clearTimeout(this.timer); | ||
48 | this.timer = undefined; | ||
49 | } | ||
50 | if (nextState.analyzing) { | ||
51 | this.timer = setTimeout( | ||
52 | () => this.updateImmediately(nextState), | ||
53 | HYSTERESIS_TIME_MS, | ||
54 | ); | ||
55 | } else { | ||
56 | this.updateImmediately(nextState); | ||
57 | } | ||
58 | }, | ||
59 | { fireImmediately: true }, | ||
60 | ); | ||
61 | } | ||
62 | |||
63 | get highestDiagnosticLevel(): Diagnostic['severity'] | undefined { | ||
64 | if (this.errorCount > 0) { | ||
65 | return 'error'; | ||
66 | } | ||
67 | if (this.warningCount > 0) { | ||
68 | return 'warning'; | ||
69 | } | ||
70 | if (this.infoCount > 0) { | ||
71 | return 'info'; | ||
72 | } | ||
73 | return undefined; | ||
74 | } | ||
75 | |||
76 | private getNextState(): State { | ||
77 | return { | ||
78 | analyzing: this.store.analyzing, | ||
79 | errorCount: this.store.errorCount, | ||
80 | warningCount: this.store.warningCount, | ||
81 | infoCount: this.store.infoCount, | ||
82 | semanticsError: this.store.semanticsError, | ||
83 | }; | ||
84 | } | ||
85 | |||
86 | private updateImmediately(nextState: State) { | ||
87 | Object.assign(this, nextState); | ||
88 | } | ||
89 | |||
90 | dispose() { | ||
91 | this.disposer(); | ||
92 | } | ||
93 | } | ||
diff --git a/subprojects/frontend/src/editor/EditorPane.tsx b/subprojects/frontend/src/editor/EditorPane.tsx index c9f86496..1125a0ec 100644 --- a/subprojects/frontend/src/editor/EditorPane.tsx +++ b/subprojects/frontend/src/editor/EditorPane.tsx | |||
@@ -13,6 +13,7 @@ import { useState } from 'react'; | |||
13 | 13 | ||
14 | import { useRootStore } from '../RootStoreProvider'; | 14 | import { useRootStore } from '../RootStoreProvider'; |
15 | 15 | ||
16 | import AnalysisErrorNotification from './AnalysisErrorNotification'; | ||
16 | import ConnectionStatusNotification from './ConnectionStatusNotification'; | 17 | import ConnectionStatusNotification from './ConnectionStatusNotification'; |
17 | import EditorArea from './EditorArea'; | 18 | import EditorArea from './EditorArea'; |
18 | import EditorButtons from './EditorButtons'; | 19 | import EditorButtons from './EditorButtons'; |
@@ -48,6 +49,7 @@ export default observer(function EditorPane(): JSX.Element { | |||
48 | <EditorLoading /> | 49 | <EditorLoading /> |
49 | ) : ( | 50 | ) : ( |
50 | <> | 51 | <> |
52 | <AnalysisErrorNotification editorStore={editorStore} /> | ||
51 | <ConnectionStatusNotification editorStore={editorStore} /> | 53 | <ConnectionStatusNotification editorStore={editorStore} /> |
52 | <SearchPanelPortal editorStore={editorStore} /> | 54 | <SearchPanelPortal editorStore={editorStore} /> |
53 | <EditorArea editorStore={editorStore} /> | 55 | <EditorArea editorStore={editorStore} /> |
diff --git a/subprojects/frontend/src/editor/EditorStore.ts b/subprojects/frontend/src/editor/EditorStore.ts index c79f6ec1..563725bb 100644 --- a/subprojects/frontend/src/editor/EditorStore.ts +++ b/subprojects/frontend/src/editor/EditorStore.ts | |||
@@ -29,6 +29,7 @@ import type PWAStore from '../PWAStore'; | |||
29 | import getLogger from '../utils/getLogger'; | 29 | import getLogger from '../utils/getLogger'; |
30 | import type XtextClient from '../xtext/XtextClient'; | 30 | import type XtextClient from '../xtext/XtextClient'; |
31 | 31 | ||
32 | import EditorErrors from './EditorErrors'; | ||
32 | import LintPanelStore from './LintPanelStore'; | 33 | import LintPanelStore from './LintPanelStore'; |
33 | import SearchPanelStore from './SearchPanelStore'; | 34 | import SearchPanelStore from './SearchPanelStore'; |
34 | import createEditorState from './createEditorState'; | 35 | import createEditorState from './createEditorState'; |
@@ -54,15 +55,22 @@ export default class EditorStore { | |||
54 | 55 | ||
55 | readonly lintPanel: LintPanelStore; | 56 | readonly lintPanel: LintPanelStore; |
56 | 57 | ||
58 | readonly delayedErrors: EditorErrors; | ||
59 | |||
57 | showLineNumbers = false; | 60 | showLineNumbers = false; |
58 | 61 | ||
59 | disposed = false; | 62 | disposed = false; |
60 | 63 | ||
64 | analyzing = false; | ||
65 | |||
66 | semanticsError: string | undefined; | ||
67 | |||
61 | semantics: unknown = {}; | 68 | semantics: unknown = {}; |
62 | 69 | ||
63 | constructor(initialValue: string, pwaStore: PWAStore) { | 70 | constructor(initialValue: string, pwaStore: PWAStore) { |
64 | this.id = nanoid(); | 71 | this.id = nanoid(); |
65 | this.state = createEditorState(initialValue, this); | 72 | this.state = createEditorState(initialValue, this); |
73 | this.delayedErrors = new EditorErrors(this); | ||
66 | this.searchPanel = new SearchPanelStore(this); | 74 | this.searchPanel = new SearchPanelStore(this); |
67 | this.lintPanel = new LintPanelStore(this); | 75 | this.lintPanel = new LintPanelStore(this); |
68 | (async () => { | 76 | (async () => { |
@@ -82,6 +90,7 @@ export default class EditorStore { | |||
82 | state: observable.ref, | 90 | state: observable.ref, |
83 | client: observable.ref, | 91 | client: observable.ref, |
84 | view: observable.ref, | 92 | view: observable.ref, |
93 | semantics: observable.ref, | ||
85 | searchPanel: false, | 94 | searchPanel: false, |
86 | lintPanel: false, | 95 | lintPanel: false, |
87 | contentAssist: false, | 96 | contentAssist: false, |
@@ -215,19 +224,6 @@ export default class EditorStore { | |||
215 | this.doCommand(nextDiagnostic); | 224 | this.doCommand(nextDiagnostic); |
216 | } | 225 | } |
217 | 226 | ||
218 | get highestDiagnosticLevel(): Diagnostic['severity'] | undefined { | ||
219 | if (this.errorCount > 0) { | ||
220 | return 'error'; | ||
221 | } | ||
222 | if (this.warningCount > 0) { | ||
223 | return 'warning'; | ||
224 | } | ||
225 | if (this.infoCount > 0) { | ||
226 | return 'info'; | ||
227 | } | ||
228 | return undefined; | ||
229 | } | ||
230 | |||
231 | updateSemanticHighlighting(ranges: IHighlightRange[]): void { | 227 | updateSemanticHighlighting(ranges: IHighlightRange[]): void { |
232 | this.dispatch(setSemanticHighlighting(ranges)); | 228 | this.dispatch(setSemanticHighlighting(ranges)); |
233 | } | 229 | } |
@@ -284,12 +280,29 @@ export default class EditorStore { | |||
284 | return true; | 280 | return true; |
285 | } | 281 | } |
286 | 282 | ||
283 | analysisStarted() { | ||
284 | this.analyzing = true; | ||
285 | } | ||
286 | |||
287 | analysisCompleted(semanticAnalysisSkipped = false) { | ||
288 | this.analyzing = false; | ||
289 | if (semanticAnalysisSkipped) { | ||
290 | this.semanticsError = undefined; | ||
291 | } | ||
292 | } | ||
293 | |||
294 | setSemanticsError(semanticsError: string) { | ||
295 | this.semanticsError = semanticsError; | ||
296 | } | ||
297 | |||
287 | setSemantics(semantics: unknown) { | 298 | setSemantics(semantics: unknown) { |
299 | this.semanticsError = undefined; | ||
288 | this.semantics = semantics; | 300 | this.semantics = semantics; |
289 | } | 301 | } |
290 | 302 | ||
291 | dispose(): void { | 303 | dispose(): void { |
292 | this.client?.dispose(); | 304 | this.client?.dispose(); |
305 | this.delayedErrors.dispose(); | ||
293 | this.disposed = true; | 306 | this.disposed = true; |
294 | } | 307 | } |
295 | } | 308 | } |
diff --git a/subprojects/frontend/src/editor/EditorTheme.ts b/subprojects/frontend/src/editor/EditorTheme.ts index 4afb93e6..dd551a52 100644 --- a/subprojects/frontend/src/editor/EditorTheme.ts +++ b/subprojects/frontend/src/editor/EditorTheme.ts | |||
@@ -4,7 +4,7 @@ | |||
4 | * SPDX-License-Identifier: EPL-2.0 | 4 | * SPDX-License-Identifier: EPL-2.0 |
5 | */ | 5 | */ |
6 | 6 | ||
7 | import errorSVG from '@material-icons/svg/svg/error/baseline.svg?raw'; | 7 | import cancelSVG from '@material-icons/svg/svg/cancel/baseline.svg?raw'; |
8 | import expandMoreSVG from '@material-icons/svg/svg/expand_more/baseline.svg?raw'; | 8 | import expandMoreSVG from '@material-icons/svg/svg/expand_more/baseline.svg?raw'; |
9 | import infoSVG from '@material-icons/svg/svg/info/baseline.svg?raw'; | 9 | import infoSVG from '@material-icons/svg/svg/info/baseline.svg?raw'; |
10 | import warningSVG from '@material-icons/svg/svg/warning/baseline.svg?raw'; | 10 | import warningSVG from '@material-icons/svg/svg/warning/baseline.svg?raw'; |
@@ -331,7 +331,7 @@ export default styled('div', { | |||
331 | '.cm-lintRange-active': { | 331 | '.cm-lintRange-active': { |
332 | background: theme.palette.highlight.activeLintRange, | 332 | background: theme.palette.highlight.activeLintRange, |
333 | }, | 333 | }, |
334 | ...lintSeverityStyle('error', errorSVG, 120), | 334 | ...lintSeverityStyle('error', cancelSVG, 120), |
335 | ...lintSeverityStyle('warning', warningSVG, 110), | 335 | ...lintSeverityStyle('warning', warningSVG, 110), |
336 | ...lintSeverityStyle('info', infoSVG, 100), | 336 | ...lintSeverityStyle('info', infoSVG, 100), |
337 | }; | 337 | }; |
diff --git a/subprojects/frontend/src/editor/GenerateButton.tsx b/subprojects/frontend/src/editor/GenerateButton.tsx index 3837ef8e..5bac0464 100644 --- a/subprojects/frontend/src/editor/GenerateButton.tsx +++ b/subprojects/frontend/src/editor/GenerateButton.tsx | |||
@@ -4,10 +4,8 @@ | |||
4 | * SPDX-License-Identifier: EPL-2.0 | 4 | * SPDX-License-Identifier: EPL-2.0 |
5 | */ | 5 | */ |
6 | 6 | ||
7 | import DangerousOutlinedIcon from '@mui/icons-material/DangerousOutlined'; | 7 | import CancelIcon from '@mui/icons-material/Cancel'; |
8 | import PlayArrowIcon from '@mui/icons-material/PlayArrow'; | 8 | import PlayArrowIcon from '@mui/icons-material/PlayArrow'; |
9 | import Button from '@mui/material/Button'; | ||
10 | import type { SxProps, Theme } from '@mui/material/styles'; | ||
11 | import { observer } from 'mobx-react-lite'; | 9 | import { observer } from 'mobx-react-lite'; |
12 | 10 | ||
13 | import AnimatedButton from './AnimatedButton'; | 11 | import AnimatedButton from './AnimatedButton'; |
@@ -18,26 +16,45 @@ const GENERATE_LABEL = 'Generate'; | |||
18 | const GenerateButton = observer(function GenerateButton({ | 16 | const GenerateButton = observer(function GenerateButton({ |
19 | editorStore, | 17 | editorStore, |
20 | hideWarnings, | 18 | hideWarnings, |
21 | sx, | ||
22 | }: { | 19 | }: { |
23 | editorStore: EditorStore | undefined; | 20 | editorStore: EditorStore | undefined; |
24 | hideWarnings?: boolean | undefined; | 21 | hideWarnings?: boolean | undefined; |
25 | sx?: SxProps<Theme> | undefined; | ||
26 | }): JSX.Element { | 22 | }): JSX.Element { |
27 | if (editorStore === undefined) { | 23 | if (editorStore === undefined) { |
28 | return ( | 24 | return ( |
29 | <Button | 25 | <AnimatedButton color="inherit" disabled> |
30 | color="inherit" | ||
31 | className="rounded shaded" | ||
32 | disabled | ||
33 | {...(sx === undefined ? {} : { sx })} | ||
34 | > | ||
35 | Loading… | 26 | Loading… |
36 | </Button> | 27 | </AnimatedButton> |
28 | ); | ||
29 | } | ||
30 | |||
31 | const { analyzing, errorCount, warningCount, semanticsError } = | ||
32 | editorStore.delayedErrors; | ||
33 | |||
34 | if (analyzing) { | ||
35 | return ( | ||
36 | <AnimatedButton color="inherit" disabled> | ||
37 | Analyzing… | ||
38 | </AnimatedButton> | ||
37 | ); | 39 | ); |
38 | } | 40 | } |
39 | 41 | ||
40 | const { errorCount, warningCount } = editorStore; | 42 | if (semanticsError !== undefined && editorStore.opened) { |
43 | return ( | ||
44 | <AnimatedButton | ||
45 | color="error" | ||
46 | disabled | ||
47 | startIcon={<CancelIcon />} | ||
48 | sx={(theme) => ({ | ||
49 | '&.Mui-disabled': { | ||
50 | color: `${theme.palette.error.main} !important`, | ||
51 | }, | ||
52 | })} | ||
53 | > | ||
54 | Analysis error | ||
55 | </AnimatedButton> | ||
56 | ); | ||
57 | } | ||
41 | 58 | ||
42 | const diagnostics: string[] = []; | 59 | const diagnostics: string[] = []; |
43 | if (errorCount > 0) { | 60 | if (errorCount > 0) { |
@@ -54,8 +71,7 @@ const GenerateButton = observer(function GenerateButton({ | |||
54 | aria-label={`Select next diagnostic out of ${summary}`} | 71 | aria-label={`Select next diagnostic out of ${summary}`} |
55 | onClick={() => editorStore.nextDiagnostic()} | 72 | onClick={() => editorStore.nextDiagnostic()} |
56 | color="error" | 73 | color="error" |
57 | startIcon={<DangerousOutlinedIcon />} | 74 | startIcon={<CancelIcon />} |
58 | {...(sx === undefined ? {} : { sx })} | ||
59 | > | 75 | > |
60 | {summary} | 76 | {summary} |
61 | </AnimatedButton> | 77 | </AnimatedButton> |
@@ -67,7 +83,6 @@ const GenerateButton = observer(function GenerateButton({ | |||
67 | disabled={!editorStore.opened} | 83 | disabled={!editorStore.opened} |
68 | color={warningCount > 0 ? 'warning' : 'primary'} | 84 | color={warningCount > 0 ? 'warning' : 'primary'} |
69 | startIcon={<PlayArrowIcon />} | 85 | startIcon={<PlayArrowIcon />} |
70 | {...(sx === undefined ? {} : { sx })} | ||
71 | > | 86 | > |
72 | {summary === '' ? GENERATE_LABEL : `${GENERATE_LABEL} (${summary})`} | 87 | {summary === '' ? GENERATE_LABEL : `${GENERATE_LABEL} (${summary})`} |
73 | </AnimatedButton> | 88 | </AnimatedButton> |
@@ -76,7 +91,6 @@ const GenerateButton = observer(function GenerateButton({ | |||
76 | 91 | ||
77 | GenerateButton.defaultProps = { | 92 | GenerateButton.defaultProps = { |
78 | hideWarnings: false, | 93 | hideWarnings: false, |
79 | sx: undefined, | ||
80 | }; | 94 | }; |
81 | 95 | ||
82 | export default GenerateButton; | 96 | export default GenerateButton; |
diff --git a/subprojects/frontend/src/xtext/SemanticsService.ts b/subprojects/frontend/src/xtext/SemanticsService.ts new file mode 100644 index 00000000..50ec371a --- /dev/null +++ b/subprojects/frontend/src/xtext/SemanticsService.ts | |||
@@ -0,0 +1,28 @@ | |||
1 | /* | ||
2 | * SPDX-FileCopyrightText: 2021-2023 The Refinery Authors <https://refinery.tools/> | ||
3 | * | ||
4 | * SPDX-License-Identifier: EPL-2.0 | ||
5 | */ | ||
6 | |||
7 | import type EditorStore from '../editor/EditorStore'; | ||
8 | |||
9 | import type ValidationService from './ValidationService'; | ||
10 | import { SemanticsResult } from './xtextServiceResults'; | ||
11 | |||
12 | export default class SemanticsService { | ||
13 | constructor( | ||
14 | private readonly store: EditorStore, | ||
15 | private readonly validationService: ValidationService, | ||
16 | ) {} | ||
17 | |||
18 | onPush(push: unknown): void { | ||
19 | const result = SemanticsResult.parse(push); | ||
20 | this.validationService.setSemanticsIssues(result.issues ?? []); | ||
21 | if (result.error !== undefined) { | ||
22 | this.store.setSemanticsError(result.error); | ||
23 | } else { | ||
24 | this.store.setSemantics(push); | ||
25 | } | ||
26 | this.store.analysisCompleted(); | ||
27 | } | ||
28 | } | ||
diff --git a/subprojects/frontend/src/xtext/UpdateService.ts b/subprojects/frontend/src/xtext/UpdateService.ts index ee5ebde2..1ac722e1 100644 --- a/subprojects/frontend/src/xtext/UpdateService.ts +++ b/subprojects/frontend/src/xtext/UpdateService.ts | |||
@@ -133,6 +133,7 @@ export default class UpdateService { | |||
133 | return; | 133 | return; |
134 | } | 134 | } |
135 | log.trace('Editor delta', delta); | 135 | log.trace('Editor delta', delta); |
136 | this.store.analysisStarted(); | ||
136 | const result = await this.webSocketClient.send({ | 137 | const result = await this.webSocketClient.send({ |
137 | resource: this.resourceName, | 138 | resource: this.resourceName, |
138 | serviceType: 'update', | 139 | serviceType: 'update', |
@@ -157,6 +158,7 @@ export default class UpdateService { | |||
157 | private async updateFullTextExclusive(): Promise<void> { | 158 | private async updateFullTextExclusive(): Promise<void> { |
158 | log.debug('Performing full text update'); | 159 | log.debug('Performing full text update'); |
159 | this.tracker.prepareFullTextUpdateExclusive(); | 160 | this.tracker.prepareFullTextUpdateExclusive(); |
161 | this.store.analysisStarted(); | ||
160 | const result = await this.webSocketClient.send({ | 162 | const result = await this.webSocketClient.send({ |
161 | resource: this.resourceName, | 163 | resource: this.resourceName, |
162 | serviceType: 'update', | 164 | serviceType: 'update', |
diff --git a/subprojects/frontend/src/xtext/ValidationService.ts b/subprojects/frontend/src/xtext/ValidationService.ts index 64fb63eb..1a896db3 100644 --- a/subprojects/frontend/src/xtext/ValidationService.ts +++ b/subprojects/frontend/src/xtext/ValidationService.ts | |||
@@ -9,7 +9,7 @@ import type { Diagnostic } from '@codemirror/lint'; | |||
9 | import type EditorStore from '../editor/EditorStore'; | 9 | import type EditorStore from '../editor/EditorStore'; |
10 | 10 | ||
11 | import type UpdateService from './UpdateService'; | 11 | import type UpdateService from './UpdateService'; |
12 | import { ValidationResult } from './xtextServiceResults'; | 12 | import { Issue, ValidationResult } from './xtextServiceResults'; |
13 | 13 | ||
14 | export default class ValidationService { | 14 | export default class ValidationService { |
15 | constructor( | 15 | constructor( |
@@ -17,11 +17,41 @@ export default class ValidationService { | |||
17 | private readonly updateService: UpdateService, | 17 | private readonly updateService: UpdateService, |
18 | ) {} | 18 | ) {} |
19 | 19 | ||
20 | private lastValidationIssues: Issue[] = []; | ||
21 | |||
22 | private lastSemanticsIssues: Issue[] = []; | ||
23 | |||
20 | onPush(push: unknown): void { | 24 | onPush(push: unknown): void { |
21 | const { issues } = ValidationResult.parse(push); | 25 | ({ issues: this.lastValidationIssues } = ValidationResult.parse(push)); |
26 | this.lastSemanticsIssues = []; | ||
27 | this.updateDiagnostics(); | ||
28 | if ( | ||
29 | this.lastValidationIssues.some(({ severity }) => severity === 'error') | ||
30 | ) { | ||
31 | this.store.analysisCompleted(true); | ||
32 | } | ||
33 | } | ||
34 | |||
35 | onDisconnect(): void { | ||
36 | this.store.updateDiagnostics([]); | ||
37 | this.lastValidationIssues = []; | ||
38 | this.lastSemanticsIssues = []; | ||
39 | } | ||
40 | |||
41 | setSemanticsIssues(issues: Issue[]): void { | ||
42 | this.lastSemanticsIssues = issues; | ||
43 | this.updateDiagnostics(); | ||
44 | } | ||
45 | |||
46 | private updateDiagnostics(): void { | ||
22 | const allChanges = this.updateService.computeChangesSinceLastUpdate(); | 47 | const allChanges = this.updateService.computeChangesSinceLastUpdate(); |
23 | const diagnostics: Diagnostic[] = []; | 48 | const diagnostics: Diagnostic[] = []; |
24 | issues.forEach(({ offset, length, severity, description }) => { | 49 | function createDiagnostic({ |
50 | offset, | ||
51 | length, | ||
52 | severity, | ||
53 | description, | ||
54 | }: Issue): void { | ||
25 | if (severity === 'ignore') { | 55 | if (severity === 'ignore') { |
26 | return; | 56 | return; |
27 | } | 57 | } |
@@ -31,11 +61,9 @@ export default class ValidationService { | |||
31 | severity, | 61 | severity, |
32 | message: description, | 62 | message: description, |
33 | }); | 63 | }); |
34 | }); | 64 | } |
65 | this.lastValidationIssues.forEach(createDiagnostic); | ||
66 | this.lastSemanticsIssues.forEach(createDiagnostic); | ||
35 | this.store.updateDiagnostics(diagnostics); | 67 | this.store.updateDiagnostics(diagnostics); |
36 | } | 68 | } |
37 | |||
38 | onDisconnect(): void { | ||
39 | this.store.updateDiagnostics([]); | ||
40 | } | ||
41 | } | 69 | } |
diff --git a/subprojects/frontend/src/xtext/XtextClient.ts b/subprojects/frontend/src/xtext/XtextClient.ts index d145cd30..87778084 100644 --- a/subprojects/frontend/src/xtext/XtextClient.ts +++ b/subprojects/frontend/src/xtext/XtextClient.ts | |||
@@ -17,6 +17,7 @@ import getLogger from '../utils/getLogger'; | |||
17 | import ContentAssistService from './ContentAssistService'; | 17 | import ContentAssistService from './ContentAssistService'; |
18 | import HighlightingService from './HighlightingService'; | 18 | import HighlightingService from './HighlightingService'; |
19 | import OccurrencesService from './OccurrencesService'; | 19 | import OccurrencesService from './OccurrencesService'; |
20 | import SemanticsService from './SemanticsService'; | ||
20 | import UpdateService from './UpdateService'; | 21 | import UpdateService from './UpdateService'; |
21 | import ValidationService from './ValidationService'; | 22 | import ValidationService from './ValidationService'; |
22 | import XtextWebSocketClient from './XtextWebSocketClient'; | 23 | import XtextWebSocketClient from './XtextWebSocketClient'; |
@@ -37,6 +38,8 @@ export default class XtextClient { | |||
37 | 38 | ||
38 | private readonly occurrencesService: OccurrencesService; | 39 | private readonly occurrencesService: OccurrencesService; |
39 | 40 | ||
41 | private readonly semanticsService: SemanticsService; | ||
42 | |||
40 | constructor( | 43 | constructor( |
41 | private readonly store: EditorStore, | 44 | private readonly store: EditorStore, |
42 | private readonly pwaStore: PWAStore, | 45 | private readonly pwaStore: PWAStore, |
@@ -54,6 +57,7 @@ export default class XtextClient { | |||
54 | ); | 57 | ); |
55 | this.validationService = new ValidationService(store, this.updateService); | 58 | this.validationService = new ValidationService(store, this.updateService); |
56 | this.occurrencesService = new OccurrencesService(store, this.updateService); | 59 | this.occurrencesService = new OccurrencesService(store, this.updateService); |
60 | this.semanticsService = new SemanticsService(store, this.validationService); | ||
57 | } | 61 | } |
58 | 62 | ||
59 | start(): void { | 63 | start(): void { |
@@ -67,6 +71,7 @@ export default class XtextClient { | |||
67 | } | 71 | } |
68 | 72 | ||
69 | private onDisconnect(): void { | 73 | private onDisconnect(): void { |
74 | this.store.analysisCompleted(true); | ||
70 | this.highlightingService.onDisconnect(); | 75 | this.highlightingService.onDisconnect(); |
71 | this.validationService.onDisconnect(); | 76 | this.validationService.onDisconnect(); |
72 | this.occurrencesService.onDisconnect(); | 77 | this.occurrencesService.onDisconnect(); |
@@ -115,7 +120,7 @@ export default class XtextClient { | |||
115 | this.validationService.onPush(push); | 120 | this.validationService.onPush(push); |
116 | return; | 121 | return; |
117 | case 'semantics': | 122 | case 'semantics': |
118 | this.store.setSemantics(push); | 123 | this.semanticsService.onPush(push); |
119 | return; | 124 | return; |
120 | default: | 125 | default: |
121 | throw new Error('Unknown service'); | 126 | throw new Error('Unknown service'); |
diff --git a/subprojects/frontend/src/xtext/xtextServiceResults.ts b/subprojects/frontend/src/xtext/xtextServiceResults.ts index d3b467ad..cae95771 100644 --- a/subprojects/frontend/src/xtext/xtextServiceResults.ts +++ b/subprojects/frontend/src/xtext/xtextServiceResults.ts | |||
@@ -125,3 +125,10 @@ export const FormattingResult = DocumentStateResult.extend({ | |||
125 | }); | 125 | }); |
126 | 126 | ||
127 | export type FormattingResult = z.infer<typeof FormattingResult>; | 127 | export type FormattingResult = z.infer<typeof FormattingResult>; |
128 | |||
129 | export const SemanticsResult = z.object({ | ||
130 | error: z.string().optional(), | ||
131 | issues: Issue.array().optional(), | ||
132 | }); | ||
133 | |||
134 | export type SemanticsResult = z.infer<typeof SemanticsResult>; | ||