aboutsummaryrefslogtreecommitdiffstats
path: root/subprojects/frontend/src/xtext
diff options
context:
space:
mode:
authorLibravatar Kristóf Marussy <kristof@marussy.com>2022-08-12 19:54:46 +0200
committerLibravatar Kristóf Marussy <kristof@marussy.com>2022-08-12 19:54:46 +0200
commitd22c3b0c257f5daf5b401988a35ab9ce981a2341 (patch)
tree0a661c927c37b52197326d1c05e211daf9bd19e5 /subprojects/frontend/src/xtext
parentfix(language): rule parsing test (diff)
downloadrefinery-d22c3b0c257f5daf5b401988a35ab9ce981a2341.tar.gz
refinery-d22c3b0c257f5daf5b401988a35ab9ce981a2341.tar.zst
refinery-d22c3b0c257f5daf5b401988a35ab9ce981a2341.zip
refactor(frontend): move from Webpack to Vite
Also overhaulds the building and linting for frontend assets.
Diffstat (limited to 'subprojects/frontend/src/xtext')
-rw-r--r--subprojects/frontend/src/xtext/ContentAssistService.ts86
-rw-r--r--subprojects/frontend/src/xtext/HighlightingService.ts7
-rw-r--r--subprojects/frontend/src/xtext/OccurrencesService.ts34
-rw-r--r--subprojects/frontend/src/xtext/UpdateService.ts69
-rw-r--r--subprojects/frontend/src/xtext/ValidationService.ts18
-rw-r--r--subprojects/frontend/src/xtext/XtextClient.ts53
-rw-r--r--subprojects/frontend/src/xtext/XtextWebSocketClient.ts94
-rw-r--r--subprojects/frontend/src/xtext/xtextMessages.ts30
-rw-r--r--subprojects/frontend/src/xtext/xtextServiceResults.ts92
9 files changed, 288 insertions, 195 deletions
diff --git a/subprojects/frontend/src/xtext/ContentAssistService.ts b/subprojects/frontend/src/xtext/ContentAssistService.ts
index bedd3b5c..dce2a902 100644
--- a/subprojects/frontend/src/xtext/ContentAssistService.ts
+++ b/subprojects/frontend/src/xtext/ContentAssistService.ts
@@ -8,8 +8,9 @@ import type { Transaction } from '@codemirror/state';
8import escapeStringRegexp from 'escape-string-regexp'; 8import escapeStringRegexp from 'escape-string-regexp';
9 9
10import { implicitCompletion } from '../language/props'; 10import { implicitCompletion } from '../language/props';
11import type { UpdateService } from './UpdateService'; 11import getLogger from '../utils/getLogger';
12import { getLogger } from '../utils/logger'; 12
13import type UpdateService from './UpdateService';
13import type { ContentAssistEntry } from './xtextServiceResults'; 14import type { ContentAssistEntry } from './xtextServiceResults';
14 15
15const PROPOSALS_LIMIT = 1000; 16const PROPOSALS_LIMIT = 1000;
@@ -48,10 +49,13 @@ function findToken({ pos, state }: CompletionContext): IFoundToken | null {
48 }; 49 };
49} 50}
50 51
51function shouldCompleteImplicitly(token: IFoundToken | null, context: CompletionContext): boolean { 52function shouldCompleteImplicitly(
52 return token !== null 53 token: IFoundToken | null,
53 && token.implicitCompletion 54 context: CompletionContext,
54 && context.pos - token.from >= 2; 55): boolean {
56 return (
57 token !== null && token.implicitCompletion && context.pos - token.from >= 2
58 );
55} 59}
56 60
57function computeSpan(prefix: string, entryCount: number): RegExp { 61function computeSpan(prefix: string, entryCount: number): RegExp {
@@ -78,23 +82,29 @@ function createCompletion(entry: ContentAssistEntry): Completion {
78 case 'SNIPPET': 82 case 'SNIPPET':
79 boost = -90; 83 boost = -90;
80 break; 84 break;
81 default: { 85 default:
82 // Penalize qualified names (vs available unqualified names). 86 {
83 const extraSegments = entry.proposal.match(/::/g)?.length || 0; 87 // Penalize qualified names (vs available unqualified names).
84 boost = Math.max(-5 * extraSegments, -50); 88 const extraSegments = entry.proposal.match(/::/g)?.length || 0;
85 } 89 boost = Math.max(-5 * extraSegments, -50);
90 }
86 break; 91 break;
87 } 92 }
88 return { 93 const completion: Completion = {
89 label: entry.proposal, 94 label: entry.proposal,
90 detail: entry.description,
91 info: entry.documentation,
92 type: entry.kind?.toLowerCase(), 95 type: entry.kind?.toLowerCase(),
93 boost, 96 boost,
94 }; 97 };
98 if (entry.documentation !== undefined) {
99 completion.info = entry.documentation;
100 }
101 if (entry.description !== undefined) {
102 completion.detail = entry.description;
103 }
104 return completion;
95} 105}
96 106
97export class ContentAssistService { 107export default class ContentAssistService {
98 private readonly updateService: UpdateService; 108 private readonly updateService: UpdateService;
99 109
100 private lastCompletion: CompletionResult | null = null; 110 private lastCompletion: CompletionResult | null = null;
@@ -117,7 +127,7 @@ export class ContentAssistService {
117 options: [], 127 options: [],
118 }; 128 };
119 } 129 }
120 let range: { from: number, to: number }; 130 let range: { from: number; to: number };
121 let prefix = ''; 131 let prefix = '';
122 if (tokenBefore === null) { 132 if (tokenBefore === null) {
123 range = { 133 range = {
@@ -139,17 +149,20 @@ export class ContentAssistService {
139 log.trace('Returning cached completion result'); 149 log.trace('Returning cached completion result');
140 // Postcondition of `shouldReturnCachedCompletion`: `lastCompletion !== null` 150 // Postcondition of `shouldReturnCachedCompletion`: `lastCompletion !== null`
141 return { 151 return {
142 ...this.lastCompletion as CompletionResult, 152 ...(this.lastCompletion as CompletionResult),
143 ...range, 153 ...range,
144 }; 154 };
145 } 155 }
146 this.lastCompletion = null; 156 this.lastCompletion = null;
147 const entries = await this.updateService.fetchContentAssist({ 157 const entries = await this.updateService.fetchContentAssist(
148 resource: this.updateService.resourceName, 158 {
149 serviceType: 'assist', 159 resource: this.updateService.resourceName,
150 caretOffset: context.pos, 160 serviceType: 'assist',
151 proposalsLimit: PROPOSALS_LIMIT, 161 caretOffset: context.pos,
152 }, context); 162 proposalsLimit: PROPOSALS_LIMIT,
163 },
164 context,
165 );
153 if (context.aborted) { 166 if (context.aborted) {
154 return { 167 return {
155 ...range, 168 ...range,
@@ -175,7 +188,7 @@ export class ContentAssistService {
175 } 188 }
176 189
177 private shouldReturnCachedCompletion( 190 private shouldReturnCachedCompletion(
178 token: { from: number, to: number, text: string } | null, 191 token: { from: number; to: number; text: string } | null,
179 ): boolean { 192 ): boolean {
180 if (token === null || this.lastCompletion === null) { 193 if (token === null || this.lastCompletion === null) {
181 return false; 194 return false;
@@ -185,11 +198,16 @@ export class ContentAssistService {
185 if (!lastTo) { 198 if (!lastTo) {
186 return true; 199 return true;
187 } 200 }
188 const [transformedFrom, transformedTo] = this.mapRangeInclusive(lastFrom, lastTo); 201 const [transformedFrom, transformedTo] = this.mapRangeInclusive(
189 return from >= transformedFrom 202 lastFrom,
190 && to <= transformedTo 203 lastTo,
191 && validFor instanceof RegExp 204 );
192 && validFor.exec(text) !== null; 205 return (
206 from >= transformedFrom &&
207 to <= transformedTo &&
208 validFor instanceof RegExp &&
209 validFor.exec(text) !== null
210 );
193 } 211 }
194 212
195 private shouldInvalidateCachedCompletion(transaction: Transaction): boolean { 213 private shouldInvalidateCachedCompletion(transaction: Transaction): boolean {
@@ -200,7 +218,10 @@ export class ContentAssistService {
200 if (!lastTo) { 218 if (!lastTo) {
201 return true; 219 return true;
202 } 220 }
203 const [transformedFrom, transformedTo] = this.mapRangeInclusive(lastFrom, lastTo); 221 const [transformedFrom, transformedTo] = this.mapRangeInclusive(
222 lastFrom,
223 lastTo,
224 );
204 let invalidate = false; 225 let invalidate = false;
205 transaction.changes.iterChangedRanges((fromA, toA) => { 226 transaction.changes.iterChangedRanges((fromA, toA) => {
206 if (fromA < transformedFrom || toA > transformedTo) { 227 if (fromA < transformedFrom || toA > transformedTo) {
@@ -210,7 +231,10 @@ export class ContentAssistService {
210 return invalidate; 231 return invalidate;
211 } 232 }
212 233
213 private mapRangeInclusive(lastFrom: number, lastTo: number): [number, number] { 234 private mapRangeInclusive(
235 lastFrom: number,
236 lastTo: number,
237 ): [number, number] {
214 const changes = this.updateService.computeChangesSinceLastUpdate(); 238 const changes = this.updateService.computeChangesSinceLastUpdate();
215 const transformedFrom = changes.mapPos(lastFrom); 239 const transformedFrom = changes.mapPos(lastFrom);
216 const transformedTo = changes.mapPos(lastTo, 1); 240 const transformedTo = changes.mapPos(lastTo, 1);
diff --git a/subprojects/frontend/src/xtext/HighlightingService.ts b/subprojects/frontend/src/xtext/HighlightingService.ts
index dfbb4a19..cf618b96 100644
--- a/subprojects/frontend/src/xtext/HighlightingService.ts
+++ b/subprojects/frontend/src/xtext/HighlightingService.ts
@@ -1,9 +1,10 @@
1import type { EditorStore } from '../editor/EditorStore'; 1import type EditorStore from '../editor/EditorStore';
2import type { IHighlightRange } from '../editor/semanticHighlighting'; 2import type { IHighlightRange } from '../editor/semanticHighlighting';
3import type { UpdateService } from './UpdateService'; 3
4import type UpdateService from './UpdateService';
4import { highlightingResult } from './xtextServiceResults'; 5import { highlightingResult } from './xtextServiceResults';
5 6
6export class HighlightingService { 7export default class HighlightingService {
7 private readonly store: EditorStore; 8 private readonly store: EditorStore;
8 9
9 private readonly updateService: UpdateService; 10 private readonly updateService: UpdateService;
diff --git a/subprojects/frontend/src/xtext/OccurrencesService.ts b/subprojects/frontend/src/xtext/OccurrencesService.ts
index bc865537..21fe8644 100644
--- a/subprojects/frontend/src/xtext/OccurrencesService.ts
+++ b/subprojects/frontend/src/xtext/OccurrencesService.ts
@@ -1,15 +1,16 @@
1import { Transaction } from '@codemirror/state'; 1import { Transaction } from '@codemirror/state';
2 2
3import type { EditorStore } from '../editor/EditorStore'; 3import type EditorStore from '../editor/EditorStore';
4import type { IOccurrence } from '../editor/findOccurrences'; 4import type { IOccurrence } from '../editor/findOccurrences';
5import type { UpdateService } from './UpdateService'; 5import Timer from '../utils/Timer';
6import { getLogger } from '../utils/logger'; 6import getLogger from '../utils/getLogger';
7import { Timer } from '../utils/Timer'; 7
8import { XtextWebSocketClient } from './XtextWebSocketClient'; 8import type UpdateService from './UpdateService';
9import type XtextWebSocketClient from './XtextWebSocketClient';
9import { 10import {
10 isConflictResult, 11 isConflictResult,
11 occurrencesResult, 12 OccurrencesResult,
12 TextRegion, 13 type TextRegion,
13} from './xtextServiceResults'; 14} from './xtextServiceResults';
14 15
15const FIND_OCCURRENCES_TIMEOUT_MS = 1000; 16const FIND_OCCURRENCES_TIMEOUT_MS = 1000;
@@ -33,7 +34,7 @@ function transformOccurrences(regions: TextRegion[]): IOccurrence[] {
33 return occurrences; 34 return occurrences;
34} 35}
35 36
36export class OccurrencesService { 37export default class OccurrencesService {
37 private readonly store: EditorStore; 38 private readonly store: EditorStore;
38 39
39 private readonly webSocketClient: XtextWebSocketClient; 40 private readonly webSocketClient: XtextWebSocketClient;
@@ -94,7 +95,7 @@ export class OccurrencesService {
94 this.findOccurrencesTimer.schedule(); 95 this.findOccurrencesTimer.schedule();
95 return; 96 return;
96 } 97 }
97 const parsedOccurrencesResult = occurrencesResult.safeParse(result); 98 const parsedOccurrencesResult = OccurrencesResult.safeParse(result);
98 if (!parsedOccurrencesResult.success) { 99 if (!parsedOccurrencesResult.success) {
99 log.error( 100 log.error(
100 'Unexpected occurences result', 101 'Unexpected occurences result',
@@ -107,14 +108,25 @@ export class OccurrencesService {
107 } 108 }
108 const { stateId, writeRegions, readRegions } = parsedOccurrencesResult.data; 109 const { stateId, writeRegions, readRegions } = parsedOccurrencesResult.data;
109 if (stateId !== this.updateService.xtextStateId) { 110 if (stateId !== this.updateService.xtextStateId) {
110 log.error('Unexpected state id, expected:', this.updateService.xtextStateId, 'got:', stateId); 111 log.error(
112 'Unexpected state id, expected:',
113 this.updateService.xtextStateId,
114 'got:',
115 stateId,
116 );
111 this.clearOccurrences(); 117 this.clearOccurrences();
112 return; 118 return;
113 } 119 }
114 const write = transformOccurrences(writeRegions); 120 const write = transformOccurrences(writeRegions);
115 const read = transformOccurrences(readRegions); 121 const read = transformOccurrences(readRegions);
116 this.hasOccurrences = write.length > 0 || read.length > 0; 122 this.hasOccurrences = write.length > 0 || read.length > 0;
117 log.debug('Found', write.length, 'write and', read.length, 'read occurrences'); 123 log.debug(
124 'Found',
125 write.length,
126 'write and',
127 read.length,
128 'read occurrences',
129 );
118 this.store.updateOccurrences(write, read); 130 this.store.updateOccurrences(write, read);
119 } 131 }
120 132
diff --git a/subprojects/frontend/src/xtext/UpdateService.ts b/subprojects/frontend/src/xtext/UpdateService.ts
index e78944a9..2994b11b 100644
--- a/subprojects/frontend/src/xtext/UpdateService.ts
+++ b/subprojects/frontend/src/xtext/UpdateService.ts
@@ -1,22 +1,23 @@
1import { 1import {
2 ChangeDesc, 2 type ChangeDesc,
3 ChangeSet, 3 ChangeSet,
4 ChangeSpec, 4 type ChangeSpec,
5 StateEffect, 5 StateEffect,
6 Transaction, 6 type Transaction,
7} from '@codemirror/state'; 7} from '@codemirror/state';
8import { nanoid } from 'nanoid'; 8import { nanoid } from 'nanoid';
9 9
10import type { EditorStore } from '../editor/EditorStore'; 10import type EditorStore from '../editor/EditorStore';
11import type { XtextWebSocketClient } from './XtextWebSocketClient'; 11import ConditionVariable from '../utils/ConditionVariable';
12import { ConditionVariable } from '../utils/ConditionVariable'; 12import Timer from '../utils/Timer';
13import { getLogger } from '../utils/logger'; 13import getLogger from '../utils/getLogger';
14import { Timer } from '../utils/Timer'; 14
15import type XtextWebSocketClient from './XtextWebSocketClient';
15import { 16import {
16 ContentAssistEntry, 17 type ContentAssistEntry,
17 contentAssistResult, 18 ContentAssistResult,
18 documentStateResult, 19 DocumentStateResult,
19 formattingResult, 20 FormattingResult,
20 isConflictResult, 21 isConflictResult,
21} from './xtextServiceResults'; 22} from './xtextServiceResults';
22 23
@@ -32,7 +33,7 @@ export interface IAbortSignal {
32 aborted: boolean; 33 aborted: boolean;
33} 34}
34 35
35export class UpdateService { 36export default class UpdateService {
36 resourceName: string; 37 resourceName: string;
37 38
38 xtextStateId: string | null = null; 39 xtextStateId: string | null = null;
@@ -76,8 +77,8 @@ export class UpdateService {
76 } 77 }
77 78
78 onTransaction(transaction: Transaction): void { 79 onTransaction(transaction: Transaction): void {
79 const setDirtyChangesEffect = transaction.effects.find( 80 const setDirtyChangesEffect = transaction.effects.find((effect) =>
80 (effect) => effect.is(setDirtyChanges), 81 effect.is(setDirtyChanges),
81 ) as StateEffect<ChangeSet> | undefined; 82 ) as StateEffect<ChangeSet> | undefined;
82 if (setDirtyChangesEffect) { 83 if (setDirtyChangesEffect) {
83 const { value } = setDirtyChangesEffect; 84 const { value } = setDirtyChangesEffect;
@@ -102,7 +103,10 @@ export class UpdateService {
102 * @return the summary of changes since the last update 103 * @return the summary of changes since the last update
103 */ 104 */
104 computeChangesSinceLastUpdate(): ChangeDesc { 105 computeChangesSinceLastUpdate(): ChangeDesc {
105 return this.pendingUpdate?.composeDesc(this.dirtyChanges.desc) || this.dirtyChanges.desc; 106 return (
107 this.pendingUpdate?.composeDesc(this.dirtyChanges.desc) ||
108 this.dirtyChanges.desc
109 );
106 } 110 }
107 111
108 private handleIdleUpdate() { 112 private handleIdleUpdate() {
@@ -131,7 +135,7 @@ export class UpdateService {
131 serviceType: 'update', 135 serviceType: 'update',
132 fullText: this.store.state.doc.sliceString(0), 136 fullText: this.store.state.doc.sliceString(0),
133 }); 137 });
134 const { stateId } = documentStateResult.parse(result); 138 const { stateId } = DocumentStateResult.parse(result);
135 return [stateId, undefined]; 139 return [stateId, undefined];
136 } 140 }
137 141
@@ -158,7 +162,7 @@ export class UpdateService {
158 requiredStateId: this.xtextStateId, 162 requiredStateId: this.xtextStateId,
159 ...delta, 163 ...delta,
160 }); 164 });
161 const parsedDocumentStateResult = documentStateResult.safeParse(result); 165 const parsedDocumentStateResult = DocumentStateResult.safeParse(result);
162 if (parsedDocumentStateResult.success) { 166 if (parsedDocumentStateResult.success) {
163 return [parsedDocumentStateResult.data.stateId, undefined]; 167 return [parsedDocumentStateResult.data.stateId, undefined];
164 } 168 }
@@ -197,9 +201,10 @@ export class UpdateService {
197 requiredStateId: this.xtextStateId, 201 requiredStateId: this.xtextStateId,
198 ...delta, 202 ...delta,
199 }); 203 });
200 const parsedContentAssistResult = contentAssistResult.safeParse(result); 204 const parsedContentAssistResult = ContentAssistResult.safeParse(result);
201 if (parsedContentAssistResult.success) { 205 if (parsedContentAssistResult.success) {
202 const { stateId, entries: resultEntries } = parsedContentAssistResult.data; 206 const { stateId, entries: resultEntries } =
207 parsedContentAssistResult.data;
203 return [stateId, resultEntries]; 208 return [stateId, resultEntries];
204 } 209 }
205 if (isConflictResult(result, 'invalidStateId')) { 210 if (isConflictResult(result, 'invalidStateId')) {
@@ -223,14 +228,19 @@ export class UpdateService {
223 return this.doFetchContentAssist(params, this.xtextStateId as string); 228 return this.doFetchContentAssist(params, this.xtextStateId as string);
224 } 229 }
225 230
226 private async doFetchContentAssist(params: Record<string, unknown>, expectedStateId: string) { 231 private async doFetchContentAssist(
232 params: Record<string, unknown>,
233 expectedStateId: string,
234 ) {
227 const result = await this.webSocketClient.send({ 235 const result = await this.webSocketClient.send({
228 ...params, 236 ...params,
229 requiredStateId: expectedStateId, 237 requiredStateId: expectedStateId,
230 }); 238 });
231 const { stateId, entries } = contentAssistResult.parse(result); 239 const { stateId, entries } = ContentAssistResult.parse(result);
232 if (stateId !== expectedStateId) { 240 if (stateId !== expectedStateId) {
233 throw new Error(`Unexpected state id, expected: ${expectedStateId} got: ${stateId}`); 241 throw new Error(
242 `Unexpected state id, expected: ${expectedStateId} got: ${stateId}`,
243 );
234 } 244 }
235 return entries; 245 return entries;
236 } 246 }
@@ -250,7 +260,7 @@ export class UpdateService {
250 selectionStart: from, 260 selectionStart: from,
251 selectionEnd: to, 261 selectionEnd: to,
252 }); 262 });
253 const { stateId, formattedText } = formattingResult.parse(result); 263 const { stateId, formattedText } = FormattingResult.parse(result);
254 this.applyBeforeDirtyChanges({ 264 this.applyBeforeDirtyChanges({
255 from, 265 from,
256 to, 266 to,
@@ -282,16 +292,15 @@ export class UpdateService {
282 } 292 }
283 293
284 private applyBeforeDirtyChanges(changeSpec: ChangeSpec) { 294 private applyBeforeDirtyChanges(changeSpec: ChangeSpec) {
285 const pendingChanges = this.pendingUpdate?.compose(this.dirtyChanges) || this.dirtyChanges; 295 const pendingChanges =
296 this.pendingUpdate?.compose(this.dirtyChanges) || this.dirtyChanges;
286 const revertChanges = pendingChanges.invert(this.store.state.doc); 297 const revertChanges = pendingChanges.invert(this.store.state.doc);
287 const applyBefore = ChangeSet.of(changeSpec, revertChanges.newLength); 298 const applyBefore = ChangeSet.of(changeSpec, revertChanges.newLength);
288 const redoChanges = pendingChanges.map(applyBefore.desc); 299 const redoChanges = pendingChanges.map(applyBefore.desc);
289 const changeSet = revertChanges.compose(applyBefore).compose(redoChanges); 300 const changeSet = revertChanges.compose(applyBefore).compose(redoChanges);
290 this.store.dispatch({ 301 this.store.dispatch({
291 changes: changeSet, 302 changes: changeSet,
292 effects: [ 303 effects: [setDirtyChanges.of(redoChanges)],
293 setDirtyChanges.of(redoChanges),
294 ],
295 }); 304 });
296 } 305 }
297 306
@@ -316,7 +325,9 @@ export class UpdateService {
316 * @param callback the asynchronous callback that updates the server state 325 * @param callback the asynchronous callback that updates the server state
317 * @return a promise resolving to the second value returned by `callback` 326 * @return a promise resolving to the second value returned by `callback`
318 */ 327 */
319 private async withUpdate<T>(callback: () => Promise<[string, T]>): Promise<T> { 328 private async withUpdate<T>(
329 callback: () => Promise<[string, T]>,
330 ): Promise<T> {
320 if (this.pendingUpdate !== null) { 331 if (this.pendingUpdate !== null) {
321 throw new Error('Another update is pending, will not perform update'); 332 throw new Error('Another update is pending, will not perform update');
322 } 333 }
diff --git a/subprojects/frontend/src/xtext/ValidationService.ts b/subprojects/frontend/src/xtext/ValidationService.ts
index ff7d3700..a0b27251 100644
--- a/subprojects/frontend/src/xtext/ValidationService.ts
+++ b/subprojects/frontend/src/xtext/ValidationService.ts
@@ -1,10 +1,11 @@
1import type { Diagnostic } from '@codemirror/lint'; 1import type { Diagnostic } from '@codemirror/lint';
2 2
3import type { EditorStore } from '../editor/EditorStore'; 3import type EditorStore from '../editor/EditorStore';
4import type { UpdateService } from './UpdateService';
5import { validationResult } from './xtextServiceResults';
6 4
7export class ValidationService { 5import type UpdateService from './UpdateService';
6import { ValidationResult } from './xtextServiceResults';
7
8export default class ValidationService {
8 private readonly store: EditorStore; 9 private readonly store: EditorStore;
9 10
10 private readonly updateService: UpdateService; 11 private readonly updateService: UpdateService;
@@ -15,15 +16,10 @@ export class ValidationService {
15 } 16 }
16 17
17 onPush(push: unknown): void { 18 onPush(push: unknown): void {
18 const { issues } = validationResult.parse(push); 19 const { issues } = ValidationResult.parse(push);
19 const allChanges = this.updateService.computeChangesSinceLastUpdate(); 20 const allChanges = this.updateService.computeChangesSinceLastUpdate();
20 const diagnostics: Diagnostic[] = []; 21 const diagnostics: Diagnostic[] = [];
21 issues.forEach(({ 22 issues.forEach(({ offset, length, severity, description }) => {
22 offset,
23 length,
24 severity,
25 description,
26 }) => {
27 if (severity === 'ignore') { 23 if (severity === 'ignore') {
28 return; 24 return;
29 } 25 }
diff --git a/subprojects/frontend/src/xtext/XtextClient.ts b/subprojects/frontend/src/xtext/XtextClient.ts
index 0898e725..7297c674 100644
--- a/subprojects/frontend/src/xtext/XtextClient.ts
+++ b/subprojects/frontend/src/xtext/XtextClient.ts
@@ -4,19 +4,20 @@ import type {
4} from '@codemirror/autocomplete'; 4} from '@codemirror/autocomplete';
5import type { Transaction } from '@codemirror/state'; 5import type { Transaction } from '@codemirror/state';
6 6
7import type { EditorStore } from '../editor/EditorStore'; 7import type EditorStore from '../editor/EditorStore';
8import { ContentAssistService } from './ContentAssistService'; 8import getLogger from '../utils/getLogger';
9import { HighlightingService } from './HighlightingService'; 9
10import { OccurrencesService } from './OccurrencesService'; 10import ContentAssistService from './ContentAssistService';
11import { UpdateService } from './UpdateService'; 11import HighlightingService from './HighlightingService';
12import { getLogger } from '../utils/logger'; 12import OccurrencesService from './OccurrencesService';
13import { ValidationService } from './ValidationService'; 13import UpdateService from './UpdateService';
14import { XtextWebSocketClient } from './XtextWebSocketClient'; 14import ValidationService from './ValidationService';
15import { XtextWebPushService } from './xtextMessages'; 15import XtextWebSocketClient from './XtextWebSocketClient';
16import type { XtextWebPushService } from './xtextMessages';
16 17
17const log = getLogger('xtext.XtextClient'); 18const log = getLogger('xtext.XtextClient');
18 19
19export class XtextClient { 20export default class XtextClient {
20 private readonly webSocketClient: XtextWebSocketClient; 21 private readonly webSocketClient: XtextWebSocketClient;
21 22
22 private readonly updateService: UpdateService; 23 private readonly updateService: UpdateService;
@@ -32,11 +33,15 @@ export class XtextClient {
32 constructor(store: EditorStore) { 33 constructor(store: EditorStore) {
33 this.webSocketClient = new XtextWebSocketClient( 34 this.webSocketClient = new XtextWebSocketClient(
34 () => this.updateService.onReconnect(), 35 () => this.updateService.onReconnect(),
35 (resource, stateId, service, push) => this.onPush(resource, stateId, service, push), 36 (resource, stateId, service, push) =>
37 this.onPush(resource, stateId, service, push),
36 ); 38 );
37 this.updateService = new UpdateService(store, this.webSocketClient); 39 this.updateService = new UpdateService(store, this.webSocketClient);
38 this.contentAssistService = new ContentAssistService(this.updateService); 40 this.contentAssistService = new ContentAssistService(this.updateService);
39 this.highlightingService = new HighlightingService(store, this.updateService); 41 this.highlightingService = new HighlightingService(
42 store,
43 this.updateService,
44 );
40 this.validationService = new ValidationService(store, this.updateService); 45 this.validationService = new ValidationService(store, this.updateService);
41 this.occurrencesService = new OccurrencesService( 46 this.occurrencesService = new OccurrencesService(
42 store, 47 store,
@@ -53,14 +58,29 @@ export class XtextClient {
53 this.occurrencesService.onTransaction(transaction); 58 this.occurrencesService.onTransaction(transaction);
54 } 59 }
55 60
56 private onPush(resource: string, stateId: string, service: XtextWebPushService, push: unknown) { 61 private onPush(
62 resource: string,
63 stateId: string,
64 service: XtextWebPushService,
65 push: unknown,
66 ) {
57 const { resourceName, xtextStateId } = this.updateService; 67 const { resourceName, xtextStateId } = this.updateService;
58 if (resource !== resourceName) { 68 if (resource !== resourceName) {
59 log.error('Unknown resource name: expected:', resourceName, 'got:', resource); 69 log.error(
70 'Unknown resource name: expected:',
71 resourceName,
72 'got:',
73 resource,
74 );
60 return; 75 return;
61 } 76 }
62 if (stateId !== xtextStateId) { 77 if (stateId !== xtextStateId) {
63 log.error('Unexpected xtext state id: expected:', xtextStateId, 'got:', stateId); 78 log.error(
79 'Unexpected xtext state id: expected:',
80 xtextStateId,
81 'got:',
82 stateId,
83 );
64 // The current push message might be stale (referring to a previous state), 84 // The current push message might be stale (referring to a previous state),
65 // so this is not neccessarily an error and there is no need to force-reconnect. 85 // so this is not neccessarily an error and there is no need to force-reconnect.
66 return; 86 return;
@@ -71,6 +91,9 @@ export class XtextClient {
71 return; 91 return;
72 case 'validate': 92 case 'validate':
73 this.validationService.onPush(push); 93 this.validationService.onPush(push);
94 return;
95 default:
96 throw new Error('Unknown service');
74 } 97 }
75 } 98 }
76 99
diff --git a/subprojects/frontend/src/xtext/XtextWebSocketClient.ts b/subprojects/frontend/src/xtext/XtextWebSocketClient.ts
index 2ce20a54..ceb1f3fd 100644
--- a/subprojects/frontend/src/xtext/XtextWebSocketClient.ts
+++ b/subprojects/frontend/src/xtext/XtextWebSocketClient.ts
@@ -1,16 +1,17 @@
1import { nanoid } from 'nanoid'; 1import { nanoid } from 'nanoid';
2 2
3import { getLogger } from '../utils/logger'; 3import PendingTask from '../utils/PendingTask';
4import { PendingTask } from '../utils/PendingTask'; 4import Timer from '../utils/Timer';
5import { Timer } from '../utils/Timer'; 5import getLogger from '../utils/getLogger';
6
6import { 7import {
7 xtextWebErrorResponse, 8 XtextWebErrorResponse,
8 XtextWebRequest, 9 XtextWebRequest,
9 xtextWebOkResponse, 10 XtextWebOkResponse,
10 xtextWebPushMessage, 11 XtextWebPushMessage,
11 XtextWebPushService, 12 XtextWebPushService,
12} from './xtextMessages'; 13} from './xtextMessages';
13import { pongResult } from './xtextServiceResults'; 14import { PongResult } from './xtextServiceResults';
14 15
15const XTEXT_SUBPROTOCOL_V1 = 'tools.refinery.language.web.xtext.v1'; 16const XTEXT_SUBPROTOCOL_V1 = 'tools.refinery.language.web.xtext.v1';
16 17
@@ -18,7 +19,8 @@ const WEBSOCKET_CLOSE_OK = 1000;
18 19
19const RECONNECT_DELAY_MS = [200, 1000, 5000, 30_000]; 20const RECONNECT_DELAY_MS = [200, 1000, 5000, 30_000];
20 21
21const MAX_RECONNECT_DELAY_MS = RECONNECT_DELAY_MS[RECONNECT_DELAY_MS.length - 1]; 22const MAX_RECONNECT_DELAY_MS =
23 RECONNECT_DELAY_MS[RECONNECT_DELAY_MS.length - 1];
22 24
23const BACKGROUND_IDLE_TIMEOUT_MS = 5 * 60 * 1000; 25const BACKGROUND_IDLE_TIMEOUT_MS = 5 * 60 * 1000;
24 26
@@ -47,7 +49,7 @@ enum State {
47 TimedOut, 49 TimedOut,
48} 50}
49 51
50export class XtextWebSocketClient { 52export default class XtextWebSocketClient {
51 private nextMessageId = 0; 53 private nextMessageId = 0;
52 54
53 private connection!: WebSocket; 55 private connection!: WebSocket;
@@ -88,9 +90,11 @@ export class XtextWebSocketClient {
88 } 90 }
89 91
90 get isOpen(): boolean { 92 get isOpen(): boolean {
91 return this.state === State.TabVisible 93 return (
92 || this.state === State.TabHiddenIdle 94 this.state === State.TabVisible ||
93 || this.state === State.TabHiddenWaiting; 95 this.state === State.TabHiddenIdle ||
96 this.state === State.TabHiddenWaiting
97 );
94 } 98 }
95 99
96 private reconnect() { 100 private reconnect() {
@@ -104,7 +108,11 @@ export class XtextWebSocketClient {
104 this.connection = new WebSocket(webSocketUrl, XTEXT_SUBPROTOCOL_V1); 108 this.connection = new WebSocket(webSocketUrl, XTEXT_SUBPROTOCOL_V1);
105 this.connection.addEventListener('open', () => { 109 this.connection.addEventListener('open', () => {
106 if (this.connection.protocol !== XTEXT_SUBPROTOCOL_V1) { 110 if (this.connection.protocol !== XTEXT_SUBPROTOCOL_V1) {
107 log.error('Unknown subprotocol', this.connection.protocol, 'selected by server'); 111 log.error(
112 'Unknown subprotocol',
113 this.connection.protocol,
114 'selected by server',
115 );
108 this.forceReconnectOnError(); 116 this.forceReconnectOnError();
109 } 117 }
110 if (document.visibilityState === 'hidden') { 118 if (document.visibilityState === 'hidden') {
@@ -126,8 +134,11 @@ export class XtextWebSocketClient {
126 this.handleMessage(event.data); 134 this.handleMessage(event.data);
127 }); 135 });
128 this.connection.addEventListener('close', (event) => { 136 this.connection.addEventListener('close', (event) => {
129 if (this.isLogicallyClosed && event.code === WEBSOCKET_CLOSE_OK 137 if (
130 && this.pendingRequests.size === 0) { 138 this.isLogicallyClosed &&
139 event.code === WEBSOCKET_CLOSE_OK &&
140 this.pendingRequests.size === 0
141 ) {
131 log.info('Websocket closed'); 142 log.info('Websocket closed');
132 return; 143 return;
133 } 144 }
@@ -144,7 +155,10 @@ export class XtextWebSocketClient {
144 return; 155 return;
145 } 156 }
146 this.idleTimer.cancel(); 157 this.idleTimer.cancel();
147 if (this.state === State.TabHiddenIdle || this.state === State.TabHiddenWaiting) { 158 if (
159 this.state === State.TabHiddenIdle ||
160 this.state === State.TabHiddenWaiting
161 ) {
148 this.handleTabVisibleConnected(); 162 this.handleTabVisibleConnected();
149 return; 163 return;
150 } 164 }
@@ -183,7 +197,11 @@ export class XtextWebSocketClient {
183 this.closeConnection(1000, 'idle timeout'); 197 this.closeConnection(1000, 'idle timeout');
184 return; 198 return;
185 } 199 }
186 log.info('Waiting for', pending, 'pending requests before closing websocket'); 200 log.info(
201 'Waiting for',
202 pending,
203 'pending requests before closing websocket',
204 );
187 } 205 }
188 206
189 private sendPing() { 207 private sendPing() {
@@ -192,19 +210,21 @@ export class XtextWebSocketClient {
192 } 210 }
193 const ping = nanoid(); 211 const ping = nanoid();
194 log.trace('Ping', ping); 212 log.trace('Ping', ping);
195 this.send({ ping }).then((result) => { 213 this.send({ ping })
196 const parsedPongResult = pongResult.safeParse(result); 214 .then((result) => {
197 if (parsedPongResult.success && parsedPongResult.data.pong === ping) { 215 const parsedPongResult = PongResult.safeParse(result);
198 log.trace('Pong', ping); 216 if (parsedPongResult.success && parsedPongResult.data.pong === ping) {
199 this.pingTimer.schedule(); 217 log.trace('Pong', ping);
200 } else { 218 this.pingTimer.schedule();
201 log.error('Invalid pong:', parsedPongResult, 'expected:', ping); 219 } else {
220 log.error('Invalid pong:', parsedPongResult, 'expected:', ping);
221 this.forceReconnectOnError();
222 }
223 })
224 .catch((error) => {
225 log.error('Error while waiting for ping', error);
202 this.forceReconnectOnError(); 226 this.forceReconnectOnError();
203 } 227 });
204 }).catch((error) => {
205 log.error('Error while waiting for ping', error);
206 this.forceReconnectOnError();
207 });
208 } 228 }
209 229
210 send(request: unknown): Promise<unknown> { 230 send(request: unknown): Promise<unknown> {
@@ -250,13 +270,13 @@ export class XtextWebSocketClient {
250 this.forceReconnectOnError(); 270 this.forceReconnectOnError();
251 return; 271 return;
252 } 272 }
253 const okResponse = xtextWebOkResponse.safeParse(message); 273 const okResponse = XtextWebOkResponse.safeParse(message);
254 if (okResponse.success) { 274 if (okResponse.success) {
255 const { id, response } = okResponse.data; 275 const { id, response } = okResponse.data;
256 this.resolveRequest(id, response); 276 this.resolveRequest(id, response);
257 return; 277 return;
258 } 278 }
259 const errorResponse = xtextWebErrorResponse.safeParse(message); 279 const errorResponse = XtextWebErrorResponse.safeParse(message);
260 if (errorResponse.success) { 280 if (errorResponse.success) {
261 const { id, error, message: errorMessage } = errorResponse.data; 281 const { id, error, message: errorMessage } = errorResponse.data;
262 this.rejectRequest(id, new Error(`${error} error: ${errorMessage}`)); 282 this.rejectRequest(id, new Error(`${error} error: ${errorMessage}`));
@@ -266,14 +286,9 @@ export class XtextWebSocketClient {
266 } 286 }
267 return; 287 return;
268 } 288 }
269 const pushMessage = xtextWebPushMessage.safeParse(message); 289 const pushMessage = XtextWebPushMessage.safeParse(message);
270 if (pushMessage.success) { 290 if (pushMessage.success) {
271 const { 291 const { resource, stateId, service, push } = pushMessage.data;
272 resource,
273 stateId,
274 service,
275 push,
276 } = pushMessage.data;
277 this.onPush(resource, stateId, service, push); 292 this.onPush(resource, stateId, service, push);
278 } else { 293 } else {
279 log.error( 294 log.error(
@@ -343,7 +358,8 @@ export class XtextWebSocketClient {
343 private handleErrorState() { 358 private handleErrorState() {
344 this.state = State.Error; 359 this.state = State.Error;
345 this.reconnectTryCount += 1; 360 this.reconnectTryCount += 1;
346 const delay = RECONNECT_DELAY_MS[this.reconnectTryCount - 1] || MAX_RECONNECT_DELAY_MS; 361 const delay =
362 RECONNECT_DELAY_MS[this.reconnectTryCount - 1] || MAX_RECONNECT_DELAY_MS;
347 log.info('Reconnecting in', delay, 'ms'); 363 log.info('Reconnecting in', delay, 'ms');
348 this.reconnectTimer.schedule(delay); 364 this.reconnectTimer.schedule(delay);
349 } 365 }
diff --git a/subprojects/frontend/src/xtext/xtextMessages.ts b/subprojects/frontend/src/xtext/xtextMessages.ts
index 4bf49c17..c4d0c676 100644
--- a/subprojects/frontend/src/xtext/xtextMessages.ts
+++ b/subprojects/frontend/src/xtext/xtextMessages.ts
@@ -1,40 +1,42 @@
1/* eslint-disable @typescript-eslint/no-redeclare -- Declare types with their companion objects */
2
1import { z } from 'zod'; 3import { z } from 'zod';
2 4
3export const xtextWebRequest = z.object({ 5export const XtextWebRequest = z.object({
4 id: z.string().min(1), 6 id: z.string().min(1),
5 request: z.unknown(), 7 request: z.unknown(),
6}); 8});
7 9
8export type XtextWebRequest = z.infer<typeof xtextWebRequest>; 10export type XtextWebRequest = z.infer<typeof XtextWebRequest>;
9 11
10export const xtextWebOkResponse = z.object({ 12export const XtextWebOkResponse = z.object({
11 id: z.string().min(1), 13 id: z.string().min(1),
12 response: z.unknown(), 14 response: z.unknown(),
13}); 15});
14 16
15export type XtextWebOkResponse = z.infer<typeof xtextWebOkResponse>; 17export type XtextWebOkResponse = z.infer<typeof XtextWebOkResponse>;
16 18
17export const xtextWebErrorKind = z.enum(['request', 'server']); 19export const XtextWebErrorKind = z.enum(['request', 'server']);
18 20
19export type XtextWebErrorKind = z.infer<typeof xtextWebErrorKind>; 21export type XtextWebErrorKind = z.infer<typeof XtextWebErrorKind>;
20 22
21export const xtextWebErrorResponse = z.object({ 23export const XtextWebErrorResponse = z.object({
22 id: z.string().min(1), 24 id: z.string().min(1),
23 error: xtextWebErrorKind, 25 error: XtextWebErrorKind,
24 message: z.string(), 26 message: z.string(),
25}); 27});
26 28
27export type XtextWebErrorResponse = z.infer<typeof xtextWebErrorResponse>; 29export type XtextWebErrorResponse = z.infer<typeof XtextWebErrorResponse>;
28 30
29export const xtextWebPushService = z.enum(['highlight', 'validate']); 31export const XtextWebPushService = z.enum(['highlight', 'validate']);
30 32
31export type XtextWebPushService = z.infer<typeof xtextWebPushService>; 33export type XtextWebPushService = z.infer<typeof XtextWebPushService>;
32 34
33export const xtextWebPushMessage = z.object({ 35export const XtextWebPushMessage = z.object({
34 resource: z.string().min(1), 36 resource: z.string().min(1),
35 stateId: z.string().min(1), 37 stateId: z.string().min(1),
36 service: xtextWebPushService, 38 service: XtextWebPushService,
37 push: z.unknown(), 39 push: z.unknown(),
38}); 40});
39 41
40export type XtextWebPushMessage = z.infer<typeof xtextWebPushMessage>; 42export type XtextWebPushMessage = z.infer<typeof XtextWebPushMessage>;
diff --git a/subprojects/frontend/src/xtext/xtextServiceResults.ts b/subprojects/frontend/src/xtext/xtextServiceResults.ts
index 8b0dbbfb..4cfb9c33 100644
--- a/subprojects/frontend/src/xtext/xtextServiceResults.ts
+++ b/subprojects/frontend/src/xtext/xtextServiceResults.ts
@@ -1,112 +1,120 @@
1/* eslint-disable @typescript-eslint/no-redeclare -- Declare types with their companion objects */
2
1import { z } from 'zod'; 3import { z } from 'zod';
2 4
3export const pongResult = z.object({ 5export const PongResult = z.object({
4 pong: z.string().min(1), 6 pong: z.string().min(1),
5}); 7});
6 8
7export type PongResult = z.infer<typeof pongResult>; 9export type PongResult = z.infer<typeof PongResult>;
8 10
9export const documentStateResult = z.object({ 11export const DocumentStateResult = z.object({
10 stateId: z.string().min(1), 12 stateId: z.string().min(1),
11}); 13});
12 14
13export type DocumentStateResult = z.infer<typeof documentStateResult>; 15export type DocumentStateResult = z.infer<typeof DocumentStateResult>;
14 16
15export const conflict = z.enum(['invalidStateId', 'canceled']); 17export const Conflict = z.enum(['invalidStateId', 'canceled']);
16 18
17export type Conflict = z.infer<typeof conflict>; 19export type Conflict = z.infer<typeof Conflict>;
18 20
19export const serviceConflictResult = z.object({ 21export const ServiceConflictResult = z.object({
20 conflict, 22 conflict: Conflict,
21}); 23});
22 24
23export type ServiceConflictResult = z.infer<typeof serviceConflictResult>; 25export type ServiceConflictResult = z.infer<typeof ServiceConflictResult>;
24 26
25export function isConflictResult(result: unknown, conflictType: Conflict): boolean { 27export function isConflictResult(
26 const parsedConflictResult = serviceConflictResult.safeParse(result); 28 result: unknown,
27 return parsedConflictResult.success && parsedConflictResult.data.conflict === conflictType; 29 conflictType: Conflict,
30): boolean {
31 const parsedConflictResult = ServiceConflictResult.safeParse(result);
32 return (
33 parsedConflictResult.success &&
34 parsedConflictResult.data.conflict === conflictType
35 );
28} 36}
29 37
30export const severity = z.enum(['error', 'warning', 'info', 'ignore']); 38export const Severity = z.enum(['error', 'warning', 'info', 'ignore']);
31 39
32export type Severity = z.infer<typeof severity>; 40export type Severity = z.infer<typeof Severity>;
33 41
34export const issue = z.object({ 42export const Issue = z.object({
35 description: z.string().min(1), 43 description: z.string().min(1),
36 severity, 44 severity: Severity,
37 line: z.number().int(), 45 line: z.number().int(),
38 column: z.number().int().nonnegative(), 46 column: z.number().int().nonnegative(),
39 offset: z.number().int().nonnegative(), 47 offset: z.number().int().nonnegative(),
40 length: z.number().int().nonnegative(), 48 length: z.number().int().nonnegative(),
41}); 49});
42 50
43export type Issue = z.infer<typeof issue>; 51export type Issue = z.infer<typeof Issue>;
44 52
45export const validationResult = z.object({ 53export const ValidationResult = z.object({
46 issues: issue.array(), 54 issues: Issue.array(),
47}); 55});
48 56
49export type ValidationResult = z.infer<typeof validationResult>; 57export type ValidationResult = z.infer<typeof ValidationResult>;
50 58
51export const replaceRegion = z.object({ 59export const ReplaceRegion = z.object({
52 offset: z.number().int().nonnegative(), 60 offset: z.number().int().nonnegative(),
53 length: z.number().int().nonnegative(), 61 length: z.number().int().nonnegative(),
54 text: z.string(), 62 text: z.string(),
55}); 63});
56 64
57export type ReplaceRegion = z.infer<typeof replaceRegion>; 65export type ReplaceRegion = z.infer<typeof ReplaceRegion>;
58 66
59export const textRegion = z.object({ 67export const TextRegion = z.object({
60 offset: z.number().int().nonnegative(), 68 offset: z.number().int().nonnegative(),
61 length: z.number().int().nonnegative(), 69 length: z.number().int().nonnegative(),
62}); 70});
63 71
64export type TextRegion = z.infer<typeof textRegion>; 72export type TextRegion = z.infer<typeof TextRegion>;
65 73
66export const contentAssistEntry = z.object({ 74export const ContentAssistEntry = z.object({
67 prefix: z.string(), 75 prefix: z.string(),
68 proposal: z.string().min(1), 76 proposal: z.string().min(1),
69 label: z.string().optional(), 77 label: z.string().optional(),
70 description: z.string().min(1).optional(), 78 description: z.string().min(1).optional(),
71 documentation: z.string().min(1).optional(), 79 documentation: z.string().min(1).optional(),
72 escapePosition: z.number().int().nonnegative().optional(), 80 escapePosition: z.number().int().nonnegative().optional(),
73 textReplacements: replaceRegion.array(), 81 textReplacements: ReplaceRegion.array(),
74 editPositions: textRegion.array(), 82 editPositions: TextRegion.array(),
75 kind: z.string().min(1), 83 kind: z.string().min(1),
76}); 84});
77 85
78export type ContentAssistEntry = z.infer<typeof contentAssistEntry>; 86export type ContentAssistEntry = z.infer<typeof ContentAssistEntry>;
79 87
80export const contentAssistResult = documentStateResult.extend({ 88export const ContentAssistResult = DocumentStateResult.extend({
81 entries: contentAssistEntry.array(), 89 entries: ContentAssistEntry.array(),
82}); 90});
83 91
84export type ContentAssistResult = z.infer<typeof contentAssistResult>; 92export type ContentAssistResult = z.infer<typeof ContentAssistResult>;
85 93
86export const highlightingRegion = z.object({ 94export const HighlightingRegion = z.object({
87 offset: z.number().int().nonnegative(), 95 offset: z.number().int().nonnegative(),
88 length: z.number().int().nonnegative(), 96 length: z.number().int().nonnegative(),
89 styleClasses: z.string().min(1).array(), 97 styleClasses: z.string().min(1).array(),
90}); 98});
91 99
92export type HighlightingRegion = z.infer<typeof highlightingRegion>; 100export type HighlightingRegion = z.infer<typeof HighlightingRegion>;
93 101
94export const highlightingResult = z.object({ 102export const highlightingResult = z.object({
95 regions: highlightingRegion.array(), 103 regions: HighlightingRegion.array(),
96}); 104});
97 105
98export type HighlightingResult = z.infer<typeof highlightingResult>; 106export type HighlightingResult = z.infer<typeof highlightingResult>;
99 107
100export const occurrencesResult = documentStateResult.extend({ 108export const OccurrencesResult = DocumentStateResult.extend({
101 writeRegions: textRegion.array(), 109 writeRegions: TextRegion.array(),
102 readRegions: textRegion.array(), 110 readRegions: TextRegion.array(),
103}); 111});
104 112
105export type OccurrencesResult = z.infer<typeof occurrencesResult>; 113export type OccurrencesResult = z.infer<typeof OccurrencesResult>;
106 114
107export const formattingResult = documentStateResult.extend({ 115export const FormattingResult = DocumentStateResult.extend({
108 formattedText: z.string(), 116 formattedText: z.string(),
109 replaceRegion: textRegion, 117 replaceRegion: TextRegion,
110}); 118});
111 119
112export type FormattingResult = z.infer<typeof formattingResult>; 120export type FormattingResult = z.infer<typeof FormattingResult>;