aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorLibravatar Kristóf Marussy <kristof@marussy.com>2021-11-16 03:00:45 +0100
committerLibravatar Kristóf Marussy <kristof@marussy.com>2021-11-16 03:11:00 +0100
commit5810a7eb3b19ef9868db170c9214686bfc613eee (patch)
treeda122997d4ad58f4104d0f84b06a00fe14e7ad02
parentfeat(lang): basic formatting (diff)
downloadrefinery-5810a7eb3b19ef9868db170c9214686bfc613eee.tar.gz
refinery-5810a7eb3b19ef9868db170c9214686bfc613eee.tar.zst
refinery-5810a7eb3b19ef9868db170c9214686bfc613eee.zip
chore(web): json validation with zod
Use the zod library instead of manually written type assertions for validating json messages from the server. This makes it easier to add and handle new messages.
-rw-r--r--language-web/package.json3
-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.ts46
-rw-r--r--language-web/src/main/js/xtext/ValidationService.ts11
-rw-r--r--language-web/src/main/js/xtext/XtextClient.ts7
-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.ts284
-rw-r--r--language-web/yarn.lock5
11 files changed, 208 insertions, 342 deletions
diff --git a/language-web/package.json b/language-web/package.json
index 3362a47a..54aad155 100644
--- a/language-web/package.json
+++ b/language-web/package.json
@@ -95,6 +95,7 @@
95 "mobx-react-lite": "^3.2.1", 95 "mobx-react-lite": "^3.2.1",
96 "nanoid": "^3.1.30", 96 "nanoid": "^3.1.30",
97 "react": "^17.0.2", 97 "react": "^17.0.2",
98 "react-dom": "^17.0.2" 98 "react-dom": "^17.0.2",
99 "zod": "^3.11.6"
99 } 100 }
100} 101}
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..fa48c5ab 100644
--- a/language-web/src/main/js/xtext/UpdateService.ts
+++ b/language-web/src/main/js/xtext/UpdateService.ts
@@ -11,10 +11,10 @@ import { ConditionVariable } from '../utils/ConditionVariable';
11import { getLogger } from '../utils/logger'; 11import { getLogger } from '../utils/logger';
12import { Timer } from '../utils/Timer'; 12import { Timer } from '../utils/Timer';
13import { 13import {
14 IContentAssistEntry, 14 ContentAssistEntry,
15 isContentAssistResult, 15 contentAssistResult,
16 isDocumentStateResult, 16 documentStateResult,
17 isInvalidStateIdConflictResult, 17 isConflictResult,
18} from './xtextServiceResults'; 18} from './xtextServiceResults';
19 19
20const UPDATE_TIMEOUT_MS = 500; 20const UPDATE_TIMEOUT_MS = 500;
@@ -116,11 +116,8 @@ export class UpdateService {
116 serviceType: 'update', 116 serviceType: 'update',
117 fullText: this.store.state.doc.sliceString(0), 117 fullText: this.store.state.doc.sliceString(0),
118 }); 118 });
119 if (isDocumentStateResult(result)) { 119 const { stateId } = documentStateResult.parse(result);
120 return [result.stateId, undefined]; 120 return [stateId, undefined];
121 }
122 log.error('Unexpected full text update result:', result);
123 throw new Error('Full text update failed');
124 } 121 }
125 122
126 /** 123 /**
@@ -146,14 +143,14 @@ export class UpdateService {
146 requiredStateId: this.xtextStateId, 143 requiredStateId: this.xtextStateId,
147 ...delta, 144 ...delta,
148 }); 145 });
149 if (isDocumentStateResult(result)) { 146 const parsedDocumentStateResult = documentStateResult.safeParse(result);
150 return [result.stateId, undefined]; 147 if (parsedDocumentStateResult.success) {
148 return [parsedDocumentStateResult.data.stateId, undefined];
151 } 149 }
152 if (isInvalidStateIdConflictResult(result)) { 150 if (isConflictResult(result, 'invalidStateId')) {
153 return this.doFallbackToUpdateFullText(); 151 return this.doFallbackToUpdateFullText();
154 } 152 }
155 log.error('Unexpected delta text update result:', result); 153 throw parsedDocumentStateResult.error;
156 throw new Error('Delta text update failed');
157 }); 154 });
158 } 155 }
159 156
@@ -171,7 +168,7 @@ export class UpdateService {
171 async fetchContentAssist( 168 async fetchContentAssist(
172 params: Record<string, unknown>, 169 params: Record<string, unknown>,
173 signal: IAbortSignal, 170 signal: IAbortSignal,
174 ): Promise<IContentAssistEntry[]> { 171 ): Promise<ContentAssistEntry[]> {
175 await this.prepareForDeltaUpdate(); 172 await this.prepareForDeltaUpdate();
176 if (signal.aborted) { 173 if (signal.aborted) {
177 return []; 174 return [];
@@ -185,18 +182,19 @@ export class UpdateService {
185 requiredStateId: this.xtextStateId, 182 requiredStateId: this.xtextStateId,
186 ...delta, 183 ...delta,
187 }); 184 });
188 if (isContentAssistResult(result)) { 185 const parsedContentAssistResult = contentAssistResult.safeParse(result);
189 return [result.stateId, result.entries]; 186 if (parsedContentAssistResult.success) {
187 const { stateId, entries: resultEntries } = parsedContentAssistResult.data;
188 return [stateId, resultEntries];
190 } 189 }
191 if (isInvalidStateIdConflictResult(result)) { 190 if (isConflictResult(result, 'invalidStateId')) {
192 const [newStateId] = await this.doFallbackToUpdateFullText(); 191 const [newStateId] = await this.doFallbackToUpdateFullText();
193 // We must finish this state update transaction to prepare for any push events 192 // 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 193 // before querying for content assist, so we just return `null` and will query
195 // the content assist service later. 194 // the content assist service later.
196 return [newStateId, null]; 195 return [newStateId, null];
197 } 196 }
198 log.error('Unextpected content assist result with delta update', result); 197 throw parsedContentAssistResult.error;
199 throw new Error('Unexpexted content assist result with delta update');
200 }); 198 });
201 if (entries !== null) { 199 if (entries !== null) {
202 return entries; 200 return entries;
@@ -214,11 +212,11 @@ export class UpdateService {
214 ...params, 212 ...params,
215 requiredStateId: expectedStateId, 213 requiredStateId: expectedStateId,
216 }); 214 });
217 if (isContentAssistResult(result) && result.stateId === expectedStateId) { 215 const { stateId, entries } = contentAssistResult.parse(result);
218 return result.entries; 216 if (stateId !== expectedStateId) {
217 throw new Error(`Unexpected state id, expected: ${expectedStateId} got: ${stateId}`);
219 } 218 }
220 log.error('Unexpected content assist result', result); 219 return entries;
221 throw new Error('Unexpected content assist result');
222 } 220 }
223 221
224 private computeDelta() { 222 private computeDelta() {
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..3922b230 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,10 +71,6 @@ 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
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..b6867e2f 100644
--- a/language-web/src/main/js/xtext/xtextServiceResults.ts
+++ b/language-web/src/main/js/xtext/xtextServiceResults.ts
@@ -1,239 +1,105 @@
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
62 length: number;
63}
64 2
65export function isIssue(value: unknown): value is IIssue { 3export const pongResult = z.object({
66 const issue = value as IIssue; 4 pong: z.string().nonempty(),
67 return typeof issue === 'object' 5});
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 6
76export interface IValidationResult { 7export type PongResult = z.infer<typeof pongResult>;
77 issues: IIssue[];
78}
79 8
80function isArrayOfType<T>(value: unknown, check: (entry: unknown) => entry is T): value is T[] { 9export const documentStateResult = z.object({
81 return Array.isArray(value) && (value as T[]).every(check); 10 stateId: z.string().nonempty(),
82} 11});
83 12
84export function isValidationResult(result: unknown): result is IValidationResult { 13export type DocumentStateResult = z.infer<typeof documentStateResult>;
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 14
93 length: number; 15export const conflict = z.enum(['invalidStateId', 'canceled']);
94 16
95 text: string; 17export type Conflict = z.infer<typeof conflict>;
96}
97 18
98export function isReplaceRegion(value: unknown): value is IReplaceRegion { 19export const serviceConflictResult = z.object({
99 const replaceRegion = value as IReplaceRegion; 20 conflict,
100 return typeof replaceRegion === 'object' 21});
101 && typeof replaceRegion.offset === 'number'
102 && typeof replaceRegion.length === 'number'
103 && typeof replaceRegion.text === 'string';
104}
105 22
106export interface ITextRegion { 23export type ServiceConflictResult = z.infer<typeof serviceConflictResult>;
107 offset: number;
108 24
109 length: number; 25export function isConflictResult(result: unknown, conflictType: Conflict): boolean {
26 const parsedConflictResult = serviceConflictResult.safeParse(result);
27 return parsedConflictResult.success && parsedConflictResult.data.conflict === conflictType;
110} 28}
111 29
112export function isTextRegion(value: unknown): value is ITextRegion { 30export const severity = z.enum(['error', 'warning', 'info', 'ignore']);
113 const textRegion = value as ITextRegion;
114 return typeof textRegion === 'object'
115 && typeof textRegion.offset === 'number'
116 && typeof textRegion.length === 'number';
117}
118 31
119export const VALID_XTEXT_CONTENT_ASSIST_ENTRY_KINDS = [ 32export type Severity = z.infer<typeof severity>;
120 'TEXT',
121 'METHOD',
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}
149 33
150export interface IContentAssistEntry { 34export const issue = z.object({
151 prefix: string; 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});
152 42
153 proposal: string; 43export type Issue = z.infer<typeof issue>;
154 44
155 label?: string; 45export const validationResult = z.object({
46 issues: issue.array(),
47});
156 48
157 description?: string; 49export type ValidationResult = z.infer<typeof validationResult>;
158 50
159 documentation?: string; 51export const replaceRegion = z.object({
52 offset: z.number().int().nonnegative(),
53 length: z.number().int().nonnegative(),
54 text: z.string(),
55});
160 56
161 escapePosition?: number; 57export type ReplaceRegion = z.infer<typeof replaceRegion>;
162 58
163 textReplacements: IReplaceRegion[]; 59export const textRegion = z.object({
60 offset: z.number().int().nonnegative(),
61 length: z.number().int().nonnegative(),
62});
164 63
165 editPositions: ITextRegion[]; 64export type TextRegion = z.infer<typeof textRegion>;
166 65
167 kind: XtextContentAssistEntryKind | string; 66export const contentAssistEntry = z.object({
168} 67 prefix: z.string(),
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});
169 77
170function isStringOrUndefined(value: unknown): value is string | undefined { 78export type ContentAssistEntry = z.infer<typeof contentAssistEntry>;
171 return typeof value === 'string' || typeof value === 'undefined';
172}
173 79
174function isNumberOrUndefined(value: unknown): value is number | undefined { 80export const contentAssistResult = documentStateResult.extend({
175 return typeof value === 'number' || typeof value === 'undefined'; 81 entries: contentAssistEntry.array(),
176} 82});
177 83
178export function isContentAssistEntry(value: unknown): value is IContentAssistEntry { 84export type ContentAssistResult = z.infer<typeof contentAssistResult>;
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 85
192export interface IContentAssistResult extends IDocumentStateResult { 86export const highlightingRegion = z.object({
193 entries: IContentAssistEntry[]; 87 offset: z.number().int().nonnegative(),
194} 88 length: z.number().int().nonnegative(),
195 89 styleClasses: z.string().nonempty().array(),
196export function isContentAssistResult(result: unknown): result is IContentAssistResult { 90});
197 const contentAssistResult = result as IContentAssistResult;
198 return isDocumentStateResult(result)
199 && isArrayOfType(contentAssistResult.entries, isContentAssistEntry);
200}
201 91
202export interface IHighlightingRegion { 92export type HighlightingRegion = z.infer<typeof highlightingRegion>;
203 offset: number;
204 93
205 length: number; 94export const highlightingResult = z.object({
95 regions: highlightingRegion.array(),
96});
206 97
207 styleClasses: string[]; 98export type HighlightingResult = z.infer<typeof highlightingResult>;
208}
209 99
210export function isHighlightingRegion(value: unknown): value is IHighlightingRegion { 100export const occurrencesResult = documentStateResult.extend({
211 const region = value as IHighlightingRegion; 101 writeRegions: textRegion.array(),
212 return typeof region === 'object' 102 readRegions: textRegion.array(),
213 && typeof region.offset === 'number' 103});
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 104
222export function isHighlightingResult(result: unknown): result is IHighlightingResult { 105export type OccurrencesResult = z.infer<typeof occurrencesResult>;
223 const highlightingResult = result as IHighlightingResult;
224 return typeof highlightingResult === 'object'
225 && isArrayOfType(highlightingResult.regions, isHighlightingRegion);
226}
227
228export interface IOccurrencesResult extends IDocumentStateResult {
229 writeRegions: ITextRegion[];
230
231 readRegions: ITextRegion[];
232}
233
234export function isOccurrencesResult(result: unknown): result is IOccurrencesResult {
235 const occurrencesResult = result as IOccurrencesResult;
236 return isDocumentStateResult(occurrencesResult)
237 && isArrayOfType(occurrencesResult.writeRegions, isTextRegion)
238 && isArrayOfType(occurrencesResult.readRegions, isTextRegion);
239}
diff --git a/language-web/yarn.lock b/language-web/yarn.lock
index 06b30508..ebebe1ff 100644
--- a/language-web/yarn.lock
+++ b/language-web/yarn.lock
@@ -7784,3 +7784,8 @@ yocto-queue@^0.1.0:
7784 version "0.1.0" 7784 version "0.1.0"
7785 resolved "https://registry.yarnpkg.com/yocto-queue/-/yocto-queue-0.1.0.tgz#0294eb3dee05028d31ee1a5fa2c556a6aaf10a1b" 7785 resolved "https://registry.yarnpkg.com/yocto-queue/-/yocto-queue-0.1.0.tgz#0294eb3dee05028d31ee1a5fa2c556a6aaf10a1b"
7786 integrity sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q== 7786 integrity sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==
7787
7788zod@^3.11.6:
7789 version "3.11.6"
7790 resolved "https://registry.yarnpkg.com/zod/-/zod-3.11.6.tgz#e43a5e0c213ae2e02aefe7cb2b1a6fa3d7f1f483"
7791 integrity sha512-daZ80A81I3/9lIydI44motWe6n59kRBfNzTuS2bfzVh1nAXi667TOTWWtatxyG+fwgNUiagSj/CWZwRRbevJIg==