aboutsummaryrefslogtreecommitdiffstats
path: root/subprojects/frontend
diff options
context:
space:
mode:
Diffstat (limited to 'subprojects/frontend')
-rw-r--r--subprojects/frontend/src/editor/AnalysisErrorNotification.tsx74
-rw-r--r--subprojects/frontend/src/editor/AnimatedButton.tsx9
-rw-r--r--subprojects/frontend/src/editor/EditorButtons.tsx6
-rw-r--r--subprojects/frontend/src/editor/EditorErrors.tsx93
-rw-r--r--subprojects/frontend/src/editor/EditorPane.tsx2
-rw-r--r--subprojects/frontend/src/editor/EditorStore.ts39
-rw-r--r--subprojects/frontend/src/editor/EditorTheme.ts4
-rw-r--r--subprojects/frontend/src/editor/GenerateButton.tsx48
-rw-r--r--subprojects/frontend/src/xtext/SemanticsService.ts28
-rw-r--r--subprojects/frontend/src/xtext/UpdateService.ts2
-rw-r--r--subprojects/frontend/src/xtext/ValidationService.ts44
-rw-r--r--subprojects/frontend/src/xtext/XtextClient.ts7
-rw-r--r--subprojects/frontend/src/xtext/xtextServiceResults.ts7
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
7import { reaction } from 'mobx';
8import { type SnackbarKey, useSnackbar } from 'notistack';
9import { useEffect, useState } from 'react';
10
11import type EditorStore from './EditorStore';
12
13function 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
40export 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
7import type { Diagnostic } from '@codemirror/lint'; 7import type { Diagnostic } from '@codemirror/lint';
8import CancelIcon from '@mui/icons-material/Cancel';
8import CheckIcon from '@mui/icons-material/Check'; 9import CheckIcon from '@mui/icons-material/Check';
9import ErrorIcon from '@mui/icons-material/Error';
10import FormatListNumberedIcon from '@mui/icons-material/FormatListNumbered'; 10import FormatListNumberedIcon from '@mui/icons-material/FormatListNumbered';
11import FormatPaint from '@mui/icons-material/FormatPaint'; 11import FormatPaint from '@mui/icons-material/FormatPaint';
12import InfoOutlinedIcon from '@mui/icons-material/InfoOutlined'; 12import InfoOutlinedIcon from '@mui/icons-material/InfoOutlined';
@@ -28,7 +28,7 @@ import type EditorStore from './EditorStore';
28function getLintIcon(severity: Diagnostic['severity'] | undefined) { 28function 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
7import { Diagnostic } from '@codemirror/lint';
8import { type IReactionDisposer, makeAutoObservable, reaction } from 'mobx';
9
10import type EditorStore from './EditorStore';
11
12const HYSTERESIS_TIME_MS = 250;
13
14export interface State {
15 analyzing: boolean;
16 errorCount: number;
17 warningCount: number;
18 infoCount: number;
19 semanticsError: string | undefined;
20}
21
22export 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
14import { useRootStore } from '../RootStoreProvider'; 14import { useRootStore } from '../RootStoreProvider';
15 15
16import AnalysisErrorNotification from './AnalysisErrorNotification';
16import ConnectionStatusNotification from './ConnectionStatusNotification'; 17import ConnectionStatusNotification from './ConnectionStatusNotification';
17import EditorArea from './EditorArea'; 18import EditorArea from './EditorArea';
18import EditorButtons from './EditorButtons'; 19import 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';
29import getLogger from '../utils/getLogger'; 29import getLogger from '../utils/getLogger';
30import type XtextClient from '../xtext/XtextClient'; 30import type XtextClient from '../xtext/XtextClient';
31 31
32import EditorErrors from './EditorErrors';
32import LintPanelStore from './LintPanelStore'; 33import LintPanelStore from './LintPanelStore';
33import SearchPanelStore from './SearchPanelStore'; 34import SearchPanelStore from './SearchPanelStore';
34import createEditorState from './createEditorState'; 35import 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
7import errorSVG from '@material-icons/svg/svg/error/baseline.svg?raw'; 7import cancelSVG from '@material-icons/svg/svg/cancel/baseline.svg?raw';
8import expandMoreSVG from '@material-icons/svg/svg/expand_more/baseline.svg?raw'; 8import expandMoreSVG from '@material-icons/svg/svg/expand_more/baseline.svg?raw';
9import infoSVG from '@material-icons/svg/svg/info/baseline.svg?raw'; 9import infoSVG from '@material-icons/svg/svg/info/baseline.svg?raw';
10import warningSVG from '@material-icons/svg/svg/warning/baseline.svg?raw'; 10import 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
7import DangerousOutlinedIcon from '@mui/icons-material/DangerousOutlined'; 7import CancelIcon from '@mui/icons-material/Cancel';
8import PlayArrowIcon from '@mui/icons-material/PlayArrow'; 8import PlayArrowIcon from '@mui/icons-material/PlayArrow';
9import Button from '@mui/material/Button';
10import type { SxProps, Theme } from '@mui/material/styles';
11import { observer } from 'mobx-react-lite'; 9import { observer } from 'mobx-react-lite';
12 10
13import AnimatedButton from './AnimatedButton'; 11import AnimatedButton from './AnimatedButton';
@@ -18,26 +16,45 @@ const GENERATE_LABEL = 'Generate';
18const GenerateButton = observer(function GenerateButton({ 16const 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&hellip; 26 Loading&hellip;
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&hellip;
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
77GenerateButton.defaultProps = { 92GenerateButton.defaultProps = {
78 hideWarnings: false, 93 hideWarnings: false,
79 sx: undefined,
80}; 94};
81 95
82export default GenerateButton; 96export 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
7import type EditorStore from '../editor/EditorStore';
8
9import type ValidationService from './ValidationService';
10import { SemanticsResult } from './xtextServiceResults';
11
12export 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';
9import type EditorStore from '../editor/EditorStore'; 9import type EditorStore from '../editor/EditorStore';
10 10
11import type UpdateService from './UpdateService'; 11import type UpdateService from './UpdateService';
12import { ValidationResult } from './xtextServiceResults'; 12import { Issue, ValidationResult } from './xtextServiceResults';
13 13
14export default class ValidationService { 14export 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';
17import ContentAssistService from './ContentAssistService'; 17import ContentAssistService from './ContentAssistService';
18import HighlightingService from './HighlightingService'; 18import HighlightingService from './HighlightingService';
19import OccurrencesService from './OccurrencesService'; 19import OccurrencesService from './OccurrencesService';
20import SemanticsService from './SemanticsService';
20import UpdateService from './UpdateService'; 21import UpdateService from './UpdateService';
21import ValidationService from './ValidationService'; 22import ValidationService from './ValidationService';
22import XtextWebSocketClient from './XtextWebSocketClient'; 23import 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
127export type FormattingResult = z.infer<typeof FormattingResult>; 127export type FormattingResult = z.infer<typeof FormattingResult>;
128
129export const SemanticsResult = z.object({
130 error: z.string().optional(),
131 issues: Issue.array().optional(),
132});
133
134export type SemanticsResult = z.infer<typeof SemanticsResult>;