aboutsummaryrefslogtreecommitdiffstats
path: root/language-web/src/main/js
diff options
context:
space:
mode:
authorLibravatar Kristóf Marussy <marussy@mit.bme.hu>2021-11-16 17:09:58 +0100
committerLibravatar GitHub <noreply@github.com>2021-11-16 17:09:58 +0100
commitb4ba1dc1fec50a8114d49879dcb30891b8a8107e (patch)
treec40e4af57988becdcf7d363a2fb7107f668b8ba0 /language-web/src/main/js
parentchore(lang): fix Sonar issue (diff)
parentfeat(web): xtext formatter client (diff)
downloadrefinery-b4ba1dc1fec50a8114d49879dcb30891b8a8107e.tar.gz
refinery-b4ba1dc1fec50a8114d49879dcb30891b8a8107e.tar.zst
refinery-b4ba1dc1fec50a8114d49879dcb30891b8a8107e.zip
Merge pull request #13 from kris7t/xtext-formatter
Xtext formatter support
Diffstat (limited to 'language-web/src/main/js')
-rw-r--r--language-web/src/main/js/editor/EditorButtons.tsx9
-rw-r--r--language-web/src/main/js/editor/EditorStore.ts6
-rw-r--r--language-web/src/main/js/xtext/ContentAssistService.ts6
-rw-r--r--language-web/src/main/js/xtext/HighlightingService.ts12
-rw-r--r--language-web/src/main/js/xtext/OccurrencesService.ts31
-rw-r--r--language-web/src/main/js/xtext/UpdateService.ts125
-rw-r--r--language-web/src/main/js/xtext/ValidationService.ts11
-rw-r--r--language-web/src/main/js/xtext/XtextClient.ts13
-rw-r--r--language-web/src/main/js/xtext/XtextWebSocketClient.ts67
-rw-r--r--language-web/src/main/js/xtext/xtextMessages.ts78
-rw-r--r--language-web/src/main/js/xtext/xtextServiceResults.ts287
11 files changed, 292 insertions, 353 deletions
diff --git a/language-web/src/main/js/editor/EditorButtons.tsx b/language-web/src/main/js/editor/EditorButtons.tsx
index 09ce33dd..150aa00d 100644
--- a/language-web/src/main/js/editor/EditorButtons.tsx
+++ b/language-web/src/main/js/editor/EditorButtons.tsx
@@ -7,6 +7,7 @@ import ToggleButtonGroup from '@mui/material/ToggleButtonGroup';
7import CheckIcon from '@mui/icons-material/Check'; 7import CheckIcon from '@mui/icons-material/Check';
8import ErrorIcon from '@mui/icons-material/Error'; 8import ErrorIcon from '@mui/icons-material/Error';
9import FormatListNumberedIcon from '@mui/icons-material/FormatListNumbered'; 9import FormatListNumberedIcon from '@mui/icons-material/FormatListNumbered';
10import FormatPaint from '@mui/icons-material/FormatPaint';
10import InfoOutlinedIcon from '@mui/icons-material/InfoOutlined'; 11import InfoOutlinedIcon from '@mui/icons-material/InfoOutlined';
11import RedoIcon from '@mui/icons-material/Redo'; 12import RedoIcon from '@mui/icons-material/Redo';
12import SearchIcon from '@mui/icons-material/Search'; 13import SearchIcon from '@mui/icons-material/Search';
@@ -47,7 +48,6 @@ export const EditorButtons = observer(() => {
47 disabled={!editorStore.canUndo} 48 disabled={!editorStore.canUndo}
48 onClick={() => editorStore.undo()} 49 onClick={() => editorStore.undo()}
49 aria-label="Undo" 50 aria-label="Undo"
50 value="undo"
51 > 51 >
52 <UndoIcon fontSize="small" /> 52 <UndoIcon fontSize="small" />
53 </IconButton> 53 </IconButton>
@@ -55,7 +55,6 @@ export const EditorButtons = observer(() => {
55 disabled={!editorStore.canRedo} 55 disabled={!editorStore.canRedo}
56 onClick={() => editorStore.redo()} 56 onClick={() => editorStore.redo()}
57 aria-label="Redo" 57 aria-label="Redo"
58 value="redo"
59 > 58 >
60 <RedoIcon fontSize="small" /> 59 <RedoIcon fontSize="small" />
61 </IconButton> 60 </IconButton>
@@ -88,6 +87,12 @@ export const EditorButtons = observer(() => {
88 {getLintIcon(editorStore.highestDiagnosticLevel)} 87 {getLintIcon(editorStore.highestDiagnosticLevel)}
89 </ToggleButton> 88 </ToggleButton>
90 </ToggleButtonGroup> 89 </ToggleButtonGroup>
90 <IconButton
91 onClick={() => editorStore.formatText()}
92 aria-label="Automatic format"
93 >
94 <FormatPaint fontSize="small" />
95 </IconButton>
91 </Stack> 96 </Stack>
92 ); 97 );
93}); 98});
diff --git a/language-web/src/main/js/editor/EditorStore.ts b/language-web/src/main/js/editor/EditorStore.ts
index 059233f4..5760de28 100644
--- a/language-web/src/main/js/editor/EditorStore.ts
+++ b/language-web/src/main/js/editor/EditorStore.ts
@@ -115,6 +115,7 @@ export class EditorStore {
115 lineNumbers(), 115 lineNumbers(),
116 foldGutter(), 116 foldGutter(),
117 keymap.of([ 117 keymap.of([
118 { key: 'Mod-Shift-f', run: () => this.formatText() },
118 ...closeBracketsKeymap, 119 ...closeBracketsKeymap,
119 ...commentKeymap, 120 ...commentKeymap,
120 ...completionKeymap, 121 ...completionKeymap,
@@ -280,4 +281,9 @@ export class EditorStore {
280 toggleLintPanel(): void { 281 toggleLintPanel(): void {
281 this.setLintPanelOpen(!this.showLintPanel); 282 this.setLintPanelOpen(!this.showLintPanel);
282 } 283 }
284
285 formatText(): boolean {
286 this.client.formatText();
287 return true;
288 }
283} 289}
diff --git a/language-web/src/main/js/xtext/ContentAssistService.ts b/language-web/src/main/js/xtext/ContentAssistService.ts
index aa9a80b0..8b872e06 100644
--- a/language-web/src/main/js/xtext/ContentAssistService.ts
+++ b/language-web/src/main/js/xtext/ContentAssistService.ts
@@ -10,7 +10,7 @@ import escapeStringRegexp from 'escape-string-regexp';
10import { implicitCompletion } from '../language/props'; 10import { implicitCompletion } from '../language/props';
11import type { UpdateService } from './UpdateService'; 11import type { UpdateService } from './UpdateService';
12import { getLogger } from '../utils/logger'; 12import { getLogger } from '../utils/logger';
13import type { IContentAssistEntry } from './xtextServiceResults'; 13import type { ContentAssistEntry } from './xtextServiceResults';
14 14
15const PROPOSALS_LIMIT = 1000; 15const PROPOSALS_LIMIT = 1000;
16 16
@@ -67,8 +67,8 @@ function computeSpan(prefix: string, entryCount: number): RegExp {
67 return new RegExp(`^${escapedPrefix}$`); 67 return new RegExp(`^${escapedPrefix}$`);
68} 68}
69 69
70function createCompletion(entry: IContentAssistEntry): Completion { 70function createCompletion(entry: ContentAssistEntry): Completion {
71 let boost; 71 let boost: number;
72 switch (entry.kind) { 72 switch (entry.kind) {
73 case 'KEYWORD': 73 case 'KEYWORD':
74 // Some hard-to-type operators should be on top. 74 // Some hard-to-type operators should be on top.
diff --git a/language-web/src/main/js/xtext/HighlightingService.ts b/language-web/src/main/js/xtext/HighlightingService.ts
index fc3e9e53..dfbb4a19 100644
--- a/language-web/src/main/js/xtext/HighlightingService.ts
+++ b/language-web/src/main/js/xtext/HighlightingService.ts
@@ -1,10 +1,7 @@
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'; 3import type { UpdateService } from './UpdateService';
4import { getLogger } from '../utils/logger'; 4import { highlightingResult } from './xtextServiceResults';
5import { isHighlightingResult } from './xtextServiceResults';
6
7const log = getLogger('xtext.ValidationService');
8 5
9export class HighlightingService { 6export class HighlightingService {
10 private readonly store: EditorStore; 7 private readonly store: EditorStore;
@@ -17,13 +14,10 @@ export class HighlightingService {
17 } 14 }
18 15
19 onPush(push: unknown): void { 16 onPush(push: unknown): void {
20 if (!isHighlightingResult(push)) { 17 const { regions } = highlightingResult.parse(push);
21 log.error('Invalid highlighting result', push);
22 return;
23 }
24 const allChanges = this.updateService.computeChangesSinceLastUpdate(); 18 const allChanges = this.updateService.computeChangesSinceLastUpdate();
25 const ranges: IHighlightRange[] = []; 19 const ranges: IHighlightRange[] = [];
26 push.regions.forEach(({ offset, length, styleClasses }) => { 20 regions.forEach(({ offset, length, styleClasses }) => {
27 if (styleClasses.length === 0) { 21 if (styleClasses.length === 0) {
28 return; 22 return;
29 } 23 }
diff --git a/language-web/src/main/js/xtext/OccurrencesService.ts b/language-web/src/main/js/xtext/OccurrencesService.ts
index d1dec9e9..bc865537 100644
--- a/language-web/src/main/js/xtext/OccurrencesService.ts
+++ b/language-web/src/main/js/xtext/OccurrencesService.ts
@@ -7,9 +7,9 @@ import { getLogger } from '../utils/logger';
7import { Timer } from '../utils/Timer'; 7import { Timer } from '../utils/Timer';
8import { XtextWebSocketClient } from './XtextWebSocketClient'; 8import { XtextWebSocketClient } from './XtextWebSocketClient';
9import { 9import {
10 isOccurrencesResult, 10 isConflictResult,
11 isServiceConflictResult, 11 occurrencesResult,
12 ITextRegion, 12 TextRegion,
13} from './xtextServiceResults'; 13} from './xtextServiceResults';
14 14
15const FIND_OCCURRENCES_TIMEOUT_MS = 1000; 15const FIND_OCCURRENCES_TIMEOUT_MS = 1000;
@@ -20,7 +20,7 @@ const CLEAR_OCCURRENCES_TIMEOUT_MS = 10;
20 20
21const log = getLogger('xtext.OccurrencesService'); 21const log = getLogger('xtext.OccurrencesService');
22 22
23function transformOccurrences(regions: ITextRegion[]): IOccurrence[] { 23function transformOccurrences(regions: TextRegion[]): IOccurrence[] {
24 const occurrences: IOccurrence[] = []; 24 const occurrences: IOccurrence[] = [];
25 regions.forEach(({ offset, length }) => { 25 regions.forEach(({ offset, length }) => {
26 if (length > 0) { 26 if (length > 0) {
@@ -87,21 +87,32 @@ export class OccurrencesService {
87 caretOffset: this.store.state.selection.main.head, 87 caretOffset: this.store.state.selection.main.head,
88 }); 88 });
89 const allChanges = this.updateService.computeChangesSinceLastUpdate(); 89 const allChanges = this.updateService.computeChangesSinceLastUpdate();
90 if (!allChanges.empty 90 if (!allChanges.empty || isConflictResult(result, 'canceled')) {
91 || (isServiceConflictResult(result) && result.conflict === 'canceled')) {
92 // Stale occurrences result, the user already made some changes. 91 // Stale occurrences result, the user already made some changes.
93 // We can safely ignore the occurrences and schedule a new find occurrences call. 92 // We can safely ignore the occurrences and schedule a new find occurrences call.
94 this.clearOccurrences(); 93 this.clearOccurrences();
95 this.findOccurrencesTimer.schedule(); 94 this.findOccurrencesTimer.schedule();
96 return; 95 return;
97 } 96 }
98 if (!isOccurrencesResult(result) || result.stateId !== this.updateService.xtextStateId) { 97 const parsedOccurrencesResult = occurrencesResult.safeParse(result);
99 log.error('Unexpected occurrences result', result); 98 if (!parsedOccurrencesResult.success) {
99 log.error(
100 'Unexpected occurences result',
101 result,
102 'not an OccurrencesResult: ',
103 parsedOccurrencesResult.error,
104 );
100 this.clearOccurrences(); 105 this.clearOccurrences();
101 return; 106 return;
102 } 107 }
103 const write = transformOccurrences(result.writeRegions); 108 const { stateId, writeRegions, readRegions } = parsedOccurrencesResult.data;
104 const read = transformOccurrences(result.readRegions); 109 if (stateId !== this.updateService.xtextStateId) {
110 log.error('Unexpected state id, expected:', this.updateService.xtextStateId, 'got:', stateId);
111 this.clearOccurrences();
112 return;
113 }
114 const write = transformOccurrences(writeRegions);
115 const read = transformOccurrences(readRegions);
105 this.hasOccurrences = write.length > 0 || read.length > 0; 116 this.hasOccurrences = write.length > 0 || read.length > 0;
106 log.debug('Found', write.length, 'write and', read.length, 'read occurrences'); 117 log.debug('Found', write.length, 'write and', read.length, 'read occurrences');
107 this.store.updateOccurrences(write, read); 118 this.store.updateOccurrences(write, read);
diff --git a/language-web/src/main/js/xtext/UpdateService.ts b/language-web/src/main/js/xtext/UpdateService.ts
index 9b672e79..e78944a9 100644
--- a/language-web/src/main/js/xtext/UpdateService.ts
+++ b/language-web/src/main/js/xtext/UpdateService.ts
@@ -1,6 +1,8 @@
1import { 1import {
2 ChangeDesc, 2 ChangeDesc,
3 ChangeSet, 3 ChangeSet,
4 ChangeSpec,
5 StateEffect,
4 Transaction, 6 Transaction,
5} from '@codemirror/state'; 7} from '@codemirror/state';
6import { nanoid } from 'nanoid'; 8import { nanoid } from 'nanoid';
@@ -11,10 +13,11 @@ import { ConditionVariable } from '../utils/ConditionVariable';
11import { getLogger } from '../utils/logger'; 13import { getLogger } from '../utils/logger';
12import { Timer } from '../utils/Timer'; 14import { Timer } from '../utils/Timer';
13import { 15import {
14 IContentAssistEntry, 16 ContentAssistEntry,
15 isContentAssistResult, 17 contentAssistResult,
16 isDocumentStateResult, 18 documentStateResult,
17 isInvalidStateIdConflictResult, 19 formattingResult,
20 isConflictResult,
18} from './xtextServiceResults'; 21} from './xtextServiceResults';
19 22
20const UPDATE_TIMEOUT_MS = 500; 23const UPDATE_TIMEOUT_MS = 500;
@@ -23,6 +26,8 @@ const WAIT_FOR_UPDATE_TIMEOUT_MS = 1000;
23 26
24const log = getLogger('xtext.UpdateService'); 27const log = getLogger('xtext.UpdateService');
25 28
29const setDirtyChanges = StateEffect.define<ChangeSet>();
30
26export interface IAbortSignal { 31export interface IAbortSignal {
27 aborted: boolean; 32 aborted: boolean;
28} 33}
@@ -38,12 +43,12 @@ export class UpdateService {
38 * The changes being synchronized to the server if a full or delta text update is running, 43 * The changes being synchronized to the server if a full or delta text update is running,
39 * `null` otherwise. 44 * `null` otherwise.
40 */ 45 */
41 private pendingUpdate: ChangeDesc | null = null; 46 private pendingUpdate: ChangeSet | null = null;
42 47
43 /** 48 /**
44 * Local changes not yet sychronized to the server and not part of the running update, if any. 49 * Local changes not yet sychronized to the server and not part of the running update, if any.
45 */ 50 */
46 private dirtyChanges: ChangeDesc; 51 private dirtyChanges: ChangeSet;
47 52
48 private readonly webSocketClient: XtextWebSocketClient; 53 private readonly webSocketClient: XtextWebSocketClient;
49 54
@@ -59,7 +64,7 @@ export class UpdateService {
59 constructor(store: EditorStore, webSocketClient: XtextWebSocketClient) { 64 constructor(store: EditorStore, webSocketClient: XtextWebSocketClient) {
60 this.resourceName = `${nanoid(7)}.problem`; 65 this.resourceName = `${nanoid(7)}.problem`;
61 this.store = store; 66 this.store = store;
62 this.dirtyChanges = this.newEmptyChangeDesc(); 67 this.dirtyChanges = this.newEmptyChangeSet();
63 this.webSocketClient = webSocketClient; 68 this.webSocketClient = webSocketClient;
64 } 69 }
65 70
@@ -71,8 +76,19 @@ export class UpdateService {
71 } 76 }
72 77
73 onTransaction(transaction: Transaction): void { 78 onTransaction(transaction: Transaction): void {
79 const setDirtyChangesEffect = transaction.effects.find(
80 (effect) => effect.is(setDirtyChanges),
81 ) as StateEffect<ChangeSet> | undefined;
82 if (setDirtyChangesEffect) {
83 const { value } = setDirtyChangesEffect;
84 if (this.pendingUpdate !== null) {
85 this.pendingUpdate = ChangeSet.empty(value.length);
86 }
87 this.dirtyChanges = value;
88 return;
89 }
74 if (transaction.docChanged) { 90 if (transaction.docChanged) {
75 this.dirtyChanges = this.dirtyChanges.composeDesc(transaction.changes.desc); 91 this.dirtyChanges = this.dirtyChanges.compose(transaction.changes);
76 this.idleUpdateTimer.reschedule(); 92 this.idleUpdateTimer.reschedule();
77 } 93 }
78 } 94 }
@@ -86,7 +102,7 @@ export class UpdateService {
86 * @return the summary of changes since the last update 102 * @return the summary of changes since the last update
87 */ 103 */
88 computeChangesSinceLastUpdate(): ChangeDesc { 104 computeChangesSinceLastUpdate(): ChangeDesc {
89 return this.pendingUpdate?.composeDesc(this.dirtyChanges) || this.dirtyChanges; 105 return this.pendingUpdate?.composeDesc(this.dirtyChanges.desc) || this.dirtyChanges.desc;
90 } 106 }
91 107
92 private handleIdleUpdate() { 108 private handleIdleUpdate() {
@@ -101,9 +117,8 @@ export class UpdateService {
101 this.idleUpdateTimer.reschedule(); 117 this.idleUpdateTimer.reschedule();
102 } 118 }
103 119
104 private newEmptyChangeDesc() { 120 private newEmptyChangeSet() {
105 const changeSet = ChangeSet.of([], this.store.state.doc.length); 121 return ChangeSet.of([], this.store.state.doc.length);
106 return changeSet.desc;
107 } 122 }
108 123
109 async updateFullText(): Promise<void> { 124 async updateFullText(): Promise<void> {
@@ -116,11 +131,8 @@ export class UpdateService {
116 serviceType: 'update', 131 serviceType: 'update',
117 fullText: this.store.state.doc.sliceString(0), 132 fullText: this.store.state.doc.sliceString(0),
118 }); 133 });
119 if (isDocumentStateResult(result)) { 134 const { stateId } = documentStateResult.parse(result);
120 return [result.stateId, undefined]; 135 return [stateId, undefined];
121 }
122 log.error('Unexpected full text update result:', result);
123 throw new Error('Full text update failed');
124 } 136 }
125 137
126 /** 138 /**
@@ -146,14 +158,14 @@ export class UpdateService {
146 requiredStateId: this.xtextStateId, 158 requiredStateId: this.xtextStateId,
147 ...delta, 159 ...delta,
148 }); 160 });
149 if (isDocumentStateResult(result)) { 161 const parsedDocumentStateResult = documentStateResult.safeParse(result);
150 return [result.stateId, undefined]; 162 if (parsedDocumentStateResult.success) {
163 return [parsedDocumentStateResult.data.stateId, undefined];
151 } 164 }
152 if (isInvalidStateIdConflictResult(result)) { 165 if (isConflictResult(result, 'invalidStateId')) {
153 return this.doFallbackToUpdateFullText(); 166 return this.doFallbackToUpdateFullText();
154 } 167 }
155 log.error('Unexpected delta text update result:', result); 168 throw parsedDocumentStateResult.error;
156 throw new Error('Delta text update failed');
157 }); 169 });
158 } 170 }
159 171
@@ -163,15 +175,15 @@ export class UpdateService {
163 } 175 }
164 log.warn('Delta update failed, performing full text update'); 176 log.warn('Delta update failed, performing full text update');
165 this.xtextStateId = null; 177 this.xtextStateId = null;
166 this.pendingUpdate = this.pendingUpdate.composeDesc(this.dirtyChanges); 178 this.pendingUpdate = this.pendingUpdate.compose(this.dirtyChanges);
167 this.dirtyChanges = this.newEmptyChangeDesc(); 179 this.dirtyChanges = this.newEmptyChangeSet();
168 return this.doUpdateFullText(); 180 return this.doUpdateFullText();
169 } 181 }
170 182
171 async fetchContentAssist( 183 async fetchContentAssist(
172 params: Record<string, unknown>, 184 params: Record<string, unknown>,
173 signal: IAbortSignal, 185 signal: IAbortSignal,
174 ): Promise<IContentAssistEntry[]> { 186 ): Promise<ContentAssistEntry[]> {
175 await this.prepareForDeltaUpdate(); 187 await this.prepareForDeltaUpdate();
176 if (signal.aborted) { 188 if (signal.aborted) {
177 return []; 189 return [];
@@ -185,18 +197,20 @@ export class UpdateService {
185 requiredStateId: this.xtextStateId, 197 requiredStateId: this.xtextStateId,
186 ...delta, 198 ...delta,
187 }); 199 });
188 if (isContentAssistResult(result)) { 200 const parsedContentAssistResult = contentAssistResult.safeParse(result);
189 return [result.stateId, result.entries]; 201 if (parsedContentAssistResult.success) {
202 const { stateId, entries: resultEntries } = parsedContentAssistResult.data;
203 return [stateId, resultEntries];
190 } 204 }
191 if (isInvalidStateIdConflictResult(result)) { 205 if (isConflictResult(result, 'invalidStateId')) {
206 log.warn('Server state invalid during content assist');
192 const [newStateId] = await this.doFallbackToUpdateFullText(); 207 const [newStateId] = await this.doFallbackToUpdateFullText();
193 // We must finish this state update transaction to prepare for any push events 208 // We must finish this state update transaction to prepare for any push events
194 // before querying for content assist, so we just return `null` and will query 209 // before querying for content assist, so we just return `null` and will query
195 // the content assist service later. 210 // the content assist service later.
196 return [newStateId, null]; 211 return [newStateId, null];
197 } 212 }
198 log.error('Unextpected content assist result with delta update', result); 213 throw parsedContentAssistResult.error;
199 throw new Error('Unexpexted content assist result with delta update');
200 }); 214 });
201 if (entries !== null) { 215 if (entries !== null) {
202 return entries; 216 return entries;
@@ -214,11 +228,36 @@ export class UpdateService {
214 ...params, 228 ...params,
215 requiredStateId: expectedStateId, 229 requiredStateId: expectedStateId,
216 }); 230 });
217 if (isContentAssistResult(result) && result.stateId === expectedStateId) { 231 const { stateId, entries } = contentAssistResult.parse(result);
218 return result.entries; 232 if (stateId !== expectedStateId) {
233 throw new Error(`Unexpected state id, expected: ${expectedStateId} got: ${stateId}`);
219 } 234 }
220 log.error('Unexpected content assist result', result); 235 return entries;
221 throw new Error('Unexpected content assist result'); 236 }
237
238 async formatText(): Promise<void> {
239 await this.update();
240 let { from, to } = this.store.state.selection.main;
241 if (to <= from) {
242 from = 0;
243 to = this.store.state.doc.length;
244 }
245 log.debug('Formatting from', from, 'to', to);
246 await this.withUpdate(async () => {
247 const result = await this.webSocketClient.send({
248 resource: this.resourceName,
249 serviceType: 'format',
250 selectionStart: from,
251 selectionEnd: to,
252 });
253 const { stateId, formattedText } = formattingResult.parse(result);
254 this.applyBeforeDirtyChanges({
255 from,
256 to,
257 insert: formattedText,
258 });
259 return [stateId, null];
260 });
222 } 261 }
223 262
224 private computeDelta() { 263 private computeDelta() {
@@ -242,6 +281,20 @@ export class UpdateService {
242 }; 281 };
243 } 282 }
244 283
284 private applyBeforeDirtyChanges(changeSpec: ChangeSpec) {
285 const pendingChanges = this.pendingUpdate?.compose(this.dirtyChanges) || this.dirtyChanges;
286 const revertChanges = pendingChanges.invert(this.store.state.doc);
287 const applyBefore = ChangeSet.of(changeSpec, revertChanges.newLength);
288 const redoChanges = pendingChanges.map(applyBefore.desc);
289 const changeSet = revertChanges.compose(applyBefore).compose(redoChanges);
290 this.store.dispatch({
291 changes: changeSet,
292 effects: [
293 setDirtyChanges.of(redoChanges),
294 ],
295 });
296 }
297
245 /** 298 /**
246 * Executes an asynchronous callback that updates the state on the server. 299 * Executes an asynchronous callback that updates the state on the server.
247 * 300 *
@@ -268,7 +321,7 @@ export class UpdateService {
268 throw new Error('Another update is pending, will not perform update'); 321 throw new Error('Another update is pending, will not perform update');
269 } 322 }
270 this.pendingUpdate = this.dirtyChanges; 323 this.pendingUpdate = this.dirtyChanges;
271 this.dirtyChanges = this.newEmptyChangeDesc(); 324 this.dirtyChanges = this.newEmptyChangeSet();
272 let newStateId: string | null = null; 325 let newStateId: string | null = null;
273 try { 326 try {
274 let result: T; 327 let result: T;
@@ -282,7 +335,7 @@ export class UpdateService {
282 if (this.pendingUpdate === null) { 335 if (this.pendingUpdate === null) {
283 log.error('pendingUpdate was cleared during update'); 336 log.error('pendingUpdate was cleared during update');
284 } else { 337 } else {
285 this.dirtyChanges = this.pendingUpdate.composeDesc(this.dirtyChanges); 338 this.dirtyChanges = this.pendingUpdate.compose(this.dirtyChanges);
286 } 339 }
287 this.pendingUpdate = null; 340 this.pendingUpdate = null;
288 this.webSocketClient.forceReconnectOnError(); 341 this.webSocketClient.forceReconnectOnError();
diff --git a/language-web/src/main/js/xtext/ValidationService.ts b/language-web/src/main/js/xtext/ValidationService.ts
index 8e4934ac..c7f6ac7f 100644
--- a/language-web/src/main/js/xtext/ValidationService.ts
+++ b/language-web/src/main/js/xtext/ValidationService.ts
@@ -3,9 +3,7 @@ import type { Diagnostic } from '@codemirror/lint';
3import type { EditorStore } from '../editor/EditorStore'; 3import type { EditorStore } from '../editor/EditorStore';
4import type { UpdateService } from './UpdateService'; 4import type { UpdateService } from './UpdateService';
5import { getLogger } from '../utils/logger'; 5import { getLogger } from '../utils/logger';
6import { isValidationResult } from './xtextServiceResults'; 6import { validationResult } from './xtextServiceResults';
7
8const log = getLogger('xtext.ValidationService');
9 7
10export class ValidationService { 8export class ValidationService {
11 private readonly store: EditorStore; 9 private readonly store: EditorStore;
@@ -18,13 +16,10 @@ export class ValidationService {
18 } 16 }
19 17
20 onPush(push: unknown): void { 18 onPush(push: unknown): void {
21 if (!isValidationResult(push)) { 19 const { issues } = validationResult.parse(push);
22 log.error('Invalid validation result', push);
23 return;
24 }
25 const allChanges = this.updateService.computeChangesSinceLastUpdate(); 20 const allChanges = this.updateService.computeChangesSinceLastUpdate();
26 const diagnostics: Diagnostic[] = []; 21 const diagnostics: Diagnostic[] = [];
27 push.issues.forEach(({ 22 issues.forEach(({
28 offset, 23 offset,
29 length, 24 length,
30 severity, 25 severity,
diff --git a/language-web/src/main/js/xtext/XtextClient.ts b/language-web/src/main/js/xtext/XtextClient.ts
index 28f3d0cc..0898e725 100644
--- a/language-web/src/main/js/xtext/XtextClient.ts
+++ b/language-web/src/main/js/xtext/XtextClient.ts
@@ -12,6 +12,7 @@ import { UpdateService } from './UpdateService';
12import { getLogger } from '../utils/logger'; 12import { getLogger } from '../utils/logger';
13import { ValidationService } from './ValidationService'; 13import { ValidationService } from './ValidationService';
14import { XtextWebSocketClient } from './XtextWebSocketClient'; 14import { XtextWebSocketClient } from './XtextWebSocketClient';
15import { XtextWebPushService } from './xtextMessages';
15 16
16const log = getLogger('xtext.XtextClient'); 17const log = getLogger('xtext.XtextClient');
17 18
@@ -52,7 +53,7 @@ export class XtextClient {
52 this.occurrencesService.onTransaction(transaction); 53 this.occurrencesService.onTransaction(transaction);
53 } 54 }
54 55
55 private onPush(resource: string, stateId: string, service: string, push: unknown) { 56 private onPush(resource: string, stateId: string, service: XtextWebPushService, push: unknown) {
56 const { resourceName, xtextStateId } = this.updateService; 57 const { resourceName, xtextStateId } = this.updateService;
57 if (resource !== resourceName) { 58 if (resource !== resourceName) {
58 log.error('Unknown resource name: expected:', resourceName, 'got:', resource); 59 log.error('Unknown resource name: expected:', resourceName, 'got:', resource);
@@ -70,14 +71,16 @@ export class XtextClient {
70 return; 71 return;
71 case 'validate': 72 case 'validate':
72 this.validationService.onPush(push); 73 this.validationService.onPush(push);
73 return;
74 default:
75 log.error('Unknown push service:', service);
76 break;
77 } 74 }
78 } 75 }
79 76
80 contentAssist(context: CompletionContext): Promise<CompletionResult> { 77 contentAssist(context: CompletionContext): Promise<CompletionResult> {
81 return this.contentAssistService.contentAssist(context); 78 return this.contentAssistService.contentAssist(context);
82 } 79 }
80
81 formatText(): void {
82 this.updateService.formatText().catch((e) => {
83 log.error('Error while formatting text', e);
84 });
85 }
83} 86}
diff --git a/language-web/src/main/js/xtext/XtextWebSocketClient.ts b/language-web/src/main/js/xtext/XtextWebSocketClient.ts
index 488e4b3b..2ce20a54 100644
--- a/language-web/src/main/js/xtext/XtextWebSocketClient.ts
+++ b/language-web/src/main/js/xtext/XtextWebSocketClient.ts
@@ -4,12 +4,13 @@ import { getLogger } from '../utils/logger';
4import { PendingTask } from '../utils/PendingTask'; 4import { PendingTask } from '../utils/PendingTask';
5import { Timer } from '../utils/Timer'; 5import { Timer } from '../utils/Timer';
6import { 6import {
7 isErrorResponse, 7 xtextWebErrorResponse,
8 isOkResponse, 8 XtextWebRequest,
9 isPushMessage, 9 xtextWebOkResponse,
10 IXtextWebRequest, 10 xtextWebPushMessage,
11 XtextWebPushService,
11} from './xtextMessages'; 12} from './xtextMessages';
12import { isPongResult } from './xtextServiceResults'; 13import { pongResult } from './xtextServiceResults';
13 14
14const XTEXT_SUBPROTOCOL_V1 = 'tools.refinery.language.web.xtext.v1'; 15const XTEXT_SUBPROTOCOL_V1 = 'tools.refinery.language.web.xtext.v1';
15 16
@@ -32,7 +33,7 @@ export type ReconnectHandler = () => void;
32export type PushHandler = ( 33export type PushHandler = (
33 resourceId: string, 34 resourceId: string,
34 stateId: string, 35 stateId: string,
35 service: string, 36 service: XtextWebPushService,
36 data: unknown, 37 data: unknown,
37) => void; 38) => void;
38 39
@@ -192,11 +193,12 @@ export class XtextWebSocketClient {
192 const ping = nanoid(); 193 const ping = nanoid();
193 log.trace('Ping', ping); 194 log.trace('Ping', ping);
194 this.send({ ping }).then((result) => { 195 this.send({ ping }).then((result) => {
195 if (isPongResult(result) && result.pong === ping) { 196 const parsedPongResult = pongResult.safeParse(result);
197 if (parsedPongResult.success && parsedPongResult.data.pong === ping) {
196 log.trace('Pong', ping); 198 log.trace('Pong', ping);
197 this.pingTimer.schedule(); 199 this.pingTimer.schedule();
198 } else { 200 } else {
199 log.error('Invalid pong'); 201 log.error('Invalid pong:', parsedPongResult, 'expected:', ping);
200 this.forceReconnectOnError(); 202 this.forceReconnectOnError();
201 } 203 }
202 }).catch((error) => { 204 }).catch((error) => {
@@ -222,7 +224,7 @@ export class XtextWebSocketClient {
222 const message = JSON.stringify({ 224 const message = JSON.stringify({
223 id: messageId, 225 id: messageId,
224 request, 226 request,
225 } as IXtextWebRequest); 227 } as XtextWebRequest);
226 log.trace('Sending message', message); 228 log.trace('Sending message', message);
227 return new Promise((resolve, reject) => { 229 return new Promise((resolve, reject) => {
228 const task = new PendingTask(resolve, reject, REQUEST_TIMEOUT_MS, () => { 230 const task = new PendingTask(resolve, reject, REQUEST_TIMEOUT_MS, () => {
@@ -248,23 +250,42 @@ export class XtextWebSocketClient {
248 this.forceReconnectOnError(); 250 this.forceReconnectOnError();
249 return; 251 return;
250 } 252 }
251 if (isOkResponse(message)) { 253 const okResponse = xtextWebOkResponse.safeParse(message);
252 this.resolveRequest(message.id, message.response); 254 if (okResponse.success) {
253 } else if (isErrorResponse(message)) { 255 const { id, response } = okResponse.data;
254 this.rejectRequest(message.id, new Error(`${message.error} error: ${message.message}`)); 256 this.resolveRequest(id, response);
255 if (message.error === 'server') { 257 return;
256 log.error('Reconnecting due to server error: ', message.message); 258 }
259 const errorResponse = xtextWebErrorResponse.safeParse(message);
260 if (errorResponse.success) {
261 const { id, error, message: errorMessage } = errorResponse.data;
262 this.rejectRequest(id, new Error(`${error} error: ${errorMessage}`));
263 if (error === 'server') {
264 log.error('Reconnecting due to server error: ', errorMessage);
257 this.forceReconnectOnError(); 265 this.forceReconnectOnError();
258 } 266 }
259 } else if (isPushMessage(message)) { 267 return;
260 this.onPush( 268 }
261 message.resource, 269 const pushMessage = xtextWebPushMessage.safeParse(message);
262 message.stateId, 270 if (pushMessage.success) {
263 message.service, 271 const {
264 message.push, 272 resource,
265 ); 273 stateId,
274 service,
275 push,
276 } = pushMessage.data;
277 this.onPush(resource, stateId, service, push);
266 } else { 278 } else {
267 log.error('Unexpected websocket message', message); 279 log.error(
280 'Unexpected websocket message:',
281 message,
282 'not ok response because:',
283 okResponse.error,
284 'not error response because:',
285 errorResponse.error,
286 'not push message because:',
287 pushMessage.error,
288 );
268 this.forceReconnectOnError(); 289 this.forceReconnectOnError();
269 } 290 }
270 } 291 }
diff --git a/language-web/src/main/js/xtext/xtextMessages.ts b/language-web/src/main/js/xtext/xtextMessages.ts
index 68737958..c4305fcf 100644
--- a/language-web/src/main/js/xtext/xtextMessages.ts
+++ b/language-web/src/main/js/xtext/xtextMessages.ts
@@ -1,62 +1,40 @@
1export interface IXtextWebRequest { 1import { z } from 'zod';
2 id: string;
3 2
4 request: unknown; 3export const xtextWebRequest = z.object({
5} 4 id: z.string().nonempty(),
5 request: z.unknown(),
6});
6 7
7export interface IXtextWebOkResponse { 8export type XtextWebRequest = z.infer<typeof xtextWebRequest>;
8 id: string;
9 9
10 response: unknown; 10export const xtextWebOkResponse = z.object({
11} 11 id: z.string().nonempty(),
12 response: z.unknown(),
13});
12 14
13export function isOkResponse(response: unknown): response is IXtextWebOkResponse { 15export type XtextWebOkResponse = z.infer<typeof xtextWebOkResponse>;
14 const okResponse = response as IXtextWebOkResponse;
15 return typeof okResponse === 'object'
16 && typeof okResponse.id === 'string'
17 && typeof okResponse.response !== 'undefined';
18}
19 16
20export const VALID_XTEXT_WEB_ERROR_KINDS = ['request', 'server'] as const; 17export const xtextWebErrorKind = z.enum(['request', 'server']);
21 18
22export type XtextWebErrorKind = typeof VALID_XTEXT_WEB_ERROR_KINDS[number]; 19export type XtextWebErrorKind = z.infer<typeof xtextWebErrorKind>;
23 20
24export function isXtextWebErrorKind(value: unknown): value is XtextWebErrorKind { 21export const xtextWebErrorResponse = z.object({
25 return typeof value === 'string' 22 id: z.string().nonempty(),
26 && VALID_XTEXT_WEB_ERROR_KINDS.includes(value as XtextWebErrorKind); 23 error: xtextWebErrorKind,
27} 24 message: z.string(),
25});
28 26
29export interface IXtextWebErrorResponse { 27export type XtextWebErrorResponse = z.infer<typeof xtextWebErrorResponse>;
30 id: string;
31 28
32 error: XtextWebErrorKind; 29export const xtextWebPushService = z.enum(['highlight', 'validate']);
33 30
34 message: string; 31export type XtextWebPushService = z.infer<typeof xtextWebPushService>;
35}
36 32
37export function isErrorResponse(response: unknown): response is IXtextWebErrorResponse { 33export const xtextWebPushMessage = z.object({
38 const errorResponse = response as IXtextWebErrorResponse; 34 resource: z.string().nonempty(),
39 return typeof errorResponse === 'object' 35 stateId: z.string().nonempty(),
40 && typeof errorResponse.id === 'string' 36 service: xtextWebPushService,
41 && isXtextWebErrorKind(errorResponse.error) 37 push: z.unknown(),
42 && typeof errorResponse.message === 'string'; 38});
43}
44 39
45export interface IXtextWebPushMessage { 40export type XtextWebPushMessage = z.infer<typeof xtextWebPushMessage>;
46 resource: string;
47
48 stateId: string;
49
50 service: string;
51
52 push: unknown;
53}
54
55export function isPushMessage(response: unknown): response is IXtextWebPushMessage {
56 const pushMessage = response as IXtextWebPushMessage;
57 return typeof pushMessage === 'object'
58 && typeof pushMessage.resource === 'string'
59 && typeof pushMessage.stateId === 'string'
60 && typeof pushMessage.service === 'string'
61 && typeof pushMessage.push !== 'undefined';
62}
diff --git a/language-web/src/main/js/xtext/xtextServiceResults.ts b/language-web/src/main/js/xtext/xtextServiceResults.ts
index b2de1e4a..f79b059c 100644
--- a/language-web/src/main/js/xtext/xtextServiceResults.ts
+++ b/language-web/src/main/js/xtext/xtextServiceResults.ts
@@ -1,239 +1,112 @@
1export interface IPongResult { 1import { z } from 'zod';
2 pong: string;
3}
4
5export function isPongResult(result: unknown): result is IPongResult {
6 const pongResult = result as IPongResult;
7 return typeof pongResult === 'object'
8 && typeof pongResult.pong === 'string';
9}
10
11export interface IDocumentStateResult {
12 stateId: string;
13}
14
15export function isDocumentStateResult(result: unknown): result is IDocumentStateResult {
16 const documentStateResult = result as IDocumentStateResult;
17 return typeof documentStateResult === 'object'
18 && typeof documentStateResult.stateId === 'string';
19}
20
21export const VALID_CONFLICTS = ['invalidStateId', 'canceled'] as const;
22
23export type Conflict = typeof VALID_CONFLICTS[number];
24
25export function isConflict(value: unknown): value is Conflict {
26 return typeof value === 'string' && VALID_CONFLICTS.includes(value as Conflict);
27}
28
29export interface IServiceConflictResult {
30 conflict: Conflict;
31}
32
33export function isServiceConflictResult(result: unknown): result is IServiceConflictResult {
34 const serviceConflictResult = result as IServiceConflictResult;
35 return typeof serviceConflictResult === 'object'
36 && isConflict(serviceConflictResult.conflict);
37}
38
39export function isInvalidStateIdConflictResult(result: unknown): boolean {
40 return isServiceConflictResult(result) && result.conflict === 'invalidStateId';
41}
42
43export const VALID_SEVERITIES = ['error', 'warning', 'info', 'ignore'] as const;
44
45export type Severity = typeof VALID_SEVERITIES[number];
46
47export function isSeverity(value: unknown): value is Severity {
48 return typeof value === 'string' && VALID_SEVERITIES.includes(value as Severity);
49}
50
51export interface IIssue {
52 description: string;
53
54 severity: Severity;
55
56 line: number;
57
58 column: number;
59
60 offset: number;
61 2
62 length: number; 3export const pongResult = z.object({
63} 4 pong: z.string().nonempty(),
5});
64 6
65export function isIssue(value: unknown): value is IIssue { 7export type PongResult = z.infer<typeof pongResult>;
66 const issue = value as IIssue;
67 return typeof issue === 'object'
68 && typeof issue.description === 'string'
69 && isSeverity(issue.severity)
70 && typeof issue.line === 'number'
71 && typeof issue.column === 'number'
72 && typeof issue.offset === 'number'
73 && typeof issue.length === 'number';
74}
75 8
76export interface IValidationResult { 9export const documentStateResult = z.object({
77 issues: IIssue[]; 10 stateId: z.string().nonempty(),
78} 11});
79 12
80function isArrayOfType<T>(value: unknown, check: (entry: unknown) => entry is T): value is T[] { 13export type DocumentStateResult = z.infer<typeof documentStateResult>;
81 return Array.isArray(value) && (value as T[]).every(check);
82}
83 14
84export function isValidationResult(result: unknown): result is IValidationResult { 15export const conflict = z.enum(['invalidStateId', 'canceled']);
85 const validationResult = result as IValidationResult;
86 return typeof validationResult === 'object'
87 && isArrayOfType(validationResult.issues, isIssue);
88}
89
90export interface IReplaceRegion {
91 offset: number;
92 16
93 length: number; 17export type Conflict = z.infer<typeof conflict>;
94 18
95 text: string; 19export const serviceConflictResult = z.object({
96} 20 conflict,
21});
97 22
98export function isReplaceRegion(value: unknown): value is IReplaceRegion { 23export type ServiceConflictResult = z.infer<typeof serviceConflictResult>;
99 const replaceRegion = value as IReplaceRegion;
100 return typeof replaceRegion === 'object'
101 && typeof replaceRegion.offset === 'number'
102 && typeof replaceRegion.length === 'number'
103 && typeof replaceRegion.text === 'string';
104}
105
106export interface ITextRegion {
107 offset: number;
108
109 length: number;
110}
111
112export function isTextRegion(value: unknown): value is ITextRegion {
113 const textRegion = value as ITextRegion;
114 return typeof textRegion === 'object'
115 && typeof textRegion.offset === 'number'
116 && typeof textRegion.length === 'number';
117}
118 24
119export const VALID_XTEXT_CONTENT_ASSIST_ENTRY_KINDS = [ 25export function isConflictResult(result: unknown, conflictType: Conflict): boolean {
120 'TEXT', 26 const parsedConflictResult = serviceConflictResult.safeParse(result);
121 'METHOD', 27 return parsedConflictResult.success && parsedConflictResult.data.conflict === conflictType;
122 'FUNCTION',
123 'CONSTRUCTOR',
124 'FIELD',
125 'VARIABLE',
126 'CLASS',
127 'INTERFACE',
128 'MODULE',
129 'PROPERTY',
130 'UNIT',
131 'VALUE',
132 'ENUM',
133 'KEYWORD',
134 'SNIPPET',
135 'COLOR',
136 'FILE',
137 'REFERENCE',
138 'UNKNOWN',
139] as const;
140
141export type XtextContentAssistEntryKind = typeof VALID_XTEXT_CONTENT_ASSIST_ENTRY_KINDS[number];
142
143export function isXtextContentAssistEntryKind(
144 value: unknown,
145): value is XtextContentAssistEntryKind {
146 return typeof value === 'string'
147 && VALID_XTEXT_CONTENT_ASSIST_ENTRY_KINDS.includes(value as XtextContentAssistEntryKind);
148} 28}
149 29
150export interface IContentAssistEntry { 30export const severity = z.enum(['error', 'warning', 'info', 'ignore']);
151 prefix: string;
152 31
153 proposal: string; 32export type Severity = z.infer<typeof severity>;
154 33
155 label?: string; 34export const issue = z.object({
35 description: z.string().nonempty(),
36 severity,
37 line: z.number().int(),
38 column: z.number().int().nonnegative(),
39 offset: z.number().int().nonnegative(),
40 length: z.number().int().nonnegative(),
41});
156 42
157 description?: string; 43export type Issue = z.infer<typeof issue>;
158 44
159 documentation?: string; 45export const validationResult = z.object({
46 issues: issue.array(),
47});
160 48
161 escapePosition?: number; 49export type ValidationResult = z.infer<typeof validationResult>;
162 50
163 textReplacements: IReplaceRegion[]; 51export const replaceRegion = z.object({
52 offset: z.number().int().nonnegative(),
53 length: z.number().int().nonnegative(),
54 text: z.string(),
55});
164 56
165 editPositions: ITextRegion[]; 57export type ReplaceRegion = z.infer<typeof replaceRegion>;
166 58
167 kind: XtextContentAssistEntryKind | string; 59export const textRegion = z.object({
168} 60 offset: z.number().int().nonnegative(),
61 length: z.number().int().nonnegative(),
62});
169 63
170function isStringOrUndefined(value: unknown): value is string | undefined { 64export type TextRegion = z.infer<typeof textRegion>;
171 return typeof value === 'string' || typeof value === 'undefined';
172}
173 65
174function isNumberOrUndefined(value: unknown): value is number | undefined { 66export const contentAssistEntry = z.object({
175 return typeof value === 'number' || typeof value === 'undefined'; 67 prefix: z.string(),
176} 68 proposal: z.string().nonempty(),
69 label: z.string().optional(),
70 description: z.string().nonempty().optional(),
71 documentation: z.string().nonempty().optional(),
72 escapePosition: z.number().int().nonnegative().optional(),
73 textReplacements: replaceRegion.array(),
74 editPositions: textRegion.array(),
75 kind: z.string().nonempty(),
76});
177 77
178export function isContentAssistEntry(value: unknown): value is IContentAssistEntry { 78export type ContentAssistEntry = z.infer<typeof contentAssistEntry>;
179 const entry = value as IContentAssistEntry;
180 return typeof entry === 'object'
181 && typeof entry.prefix === 'string'
182 && typeof entry.proposal === 'string'
183 && isStringOrUndefined(entry.label)
184 && isStringOrUndefined(entry.description)
185 && isStringOrUndefined(entry.documentation)
186 && isNumberOrUndefined(entry.escapePosition)
187 && isArrayOfType(entry.textReplacements, isReplaceRegion)
188 && isArrayOfType(entry.editPositions, isTextRegion)
189 && typeof entry.kind === 'string';
190}
191 79
192export interface IContentAssistResult extends IDocumentStateResult { 80export const contentAssistResult = documentStateResult.extend({
193 entries: IContentAssistEntry[]; 81 entries: contentAssistEntry.array(),
194} 82});
195 83
196export function isContentAssistResult(result: unknown): result is IContentAssistResult { 84export type ContentAssistResult = z.infer<typeof contentAssistResult>;
197 const contentAssistResult = result as IContentAssistResult;
198 return isDocumentStateResult(result)
199 && isArrayOfType(contentAssistResult.entries, isContentAssistEntry);
200}
201 85
202export interface IHighlightingRegion { 86export const highlightingRegion = z.object({
203 offset: number; 87 offset: z.number().int().nonnegative(),
88 length: z.number().int().nonnegative(),
89 styleClasses: z.string().nonempty().array(),
90});
204 91
205 length: number; 92export type HighlightingRegion = z.infer<typeof highlightingRegion>;
206 93
207 styleClasses: string[]; 94export const highlightingResult = z.object({
208} 95 regions: highlightingRegion.array(),
96});
209 97
210export function isHighlightingRegion(value: unknown): value is IHighlightingRegion { 98export type HighlightingResult = z.infer<typeof highlightingResult>;
211 const region = value as IHighlightingRegion;
212 return typeof region === 'object'
213 && typeof region.offset === 'number'
214 && typeof region.length === 'number'
215 && isArrayOfType(region.styleClasses, (s): s is string => typeof s === 'string');
216}
217
218export interface IHighlightingResult {
219 regions: IHighlightingRegion[];
220}
221 99
222export function isHighlightingResult(result: unknown): result is IHighlightingResult { 100export const occurrencesResult = documentStateResult.extend({
223 const highlightingResult = result as IHighlightingResult; 101 writeRegions: textRegion.array(),
224 return typeof highlightingResult === 'object' 102 readRegions: textRegion.array(),
225 && isArrayOfType(highlightingResult.regions, isHighlightingRegion); 103});
226}
227 104
228export interface IOccurrencesResult extends IDocumentStateResult { 105export type OccurrencesResult = z.infer<typeof occurrencesResult>;
229 writeRegions: ITextRegion[];
230 106
231 readRegions: ITextRegion[]; 107export const formattingResult = documentStateResult.extend({
232} 108 formattedText: z.string(),
109 replaceRegion: textRegion,
110});
233 111
234export function isOccurrencesResult(result: unknown): result is IOccurrencesResult { 112export type FormattingResult = z.infer<typeof formattingResult>;
235 const occurrencesResult = result as IOccurrencesResult;
236 return isDocumentStateResult(occurrencesResult)
237 && isArrayOfType(occurrencesResult.writeRegions, isTextRegion)
238 && isArrayOfType(occurrencesResult.readRegions, isTextRegion);
239}