diff options
author | Kristóf Marussy <kristof@marussy.com> | 2021-11-16 03:00:45 +0100 |
---|---|---|
committer | Kristóf Marussy <kristof@marussy.com> | 2021-11-16 03:11:00 +0100 |
commit | 5810a7eb3b19ef9868db170c9214686bfc613eee (patch) | |
tree | da122997d4ad58f4104d0f84b06a00fe14e7ad02 /language-web/src/main/js/xtext | |
parent | feat(lang): basic formatting (diff) | |
download | refinery-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.
Diffstat (limited to 'language-web/src/main/js/xtext')
-rw-r--r-- | language-web/src/main/js/xtext/ContentAssistService.ts | 6 | ||||
-rw-r--r-- | language-web/src/main/js/xtext/HighlightingService.ts | 12 | ||||
-rw-r--r-- | language-web/src/main/js/xtext/OccurrencesService.ts | 31 | ||||
-rw-r--r-- | language-web/src/main/js/xtext/UpdateService.ts | 46 | ||||
-rw-r--r-- | language-web/src/main/js/xtext/ValidationService.ts | 11 | ||||
-rw-r--r-- | language-web/src/main/js/xtext/XtextClient.ts | 7 | ||||
-rw-r--r-- | language-web/src/main/js/xtext/XtextWebSocketClient.ts | 67 | ||||
-rw-r--r-- | language-web/src/main/js/xtext/xtextMessages.ts | 78 | ||||
-rw-r--r-- | language-web/src/main/js/xtext/xtextServiceResults.ts | 284 |
9 files changed, 201 insertions, 341 deletions
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'; | |||
10 | import { implicitCompletion } from '../language/props'; | 10 | import { implicitCompletion } from '../language/props'; |
11 | import type { UpdateService } from './UpdateService'; | 11 | import type { UpdateService } from './UpdateService'; |
12 | import { getLogger } from '../utils/logger'; | 12 | import { getLogger } from '../utils/logger'; |
13 | import type { IContentAssistEntry } from './xtextServiceResults'; | 13 | import type { ContentAssistEntry } from './xtextServiceResults'; |
14 | 14 | ||
15 | const PROPOSALS_LIMIT = 1000; | 15 | const 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 | ||
70 | function createCompletion(entry: IContentAssistEntry): Completion { | 70 | function 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 @@ | |||
1 | import type { EditorStore } from '../editor/EditorStore'; | 1 | import type { EditorStore } from '../editor/EditorStore'; |
2 | import type { IHighlightRange } from '../editor/semanticHighlighting'; | 2 | import type { IHighlightRange } from '../editor/semanticHighlighting'; |
3 | import type { UpdateService } from './UpdateService'; | 3 | import type { UpdateService } from './UpdateService'; |
4 | import { getLogger } from '../utils/logger'; | 4 | import { highlightingResult } from './xtextServiceResults'; |
5 | import { isHighlightingResult } from './xtextServiceResults'; | ||
6 | |||
7 | const log = getLogger('xtext.ValidationService'); | ||
8 | 5 | ||
9 | export class HighlightingService { | 6 | export 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'; | |||
7 | import { Timer } from '../utils/Timer'; | 7 | import { Timer } from '../utils/Timer'; |
8 | import { XtextWebSocketClient } from './XtextWebSocketClient'; | 8 | import { XtextWebSocketClient } from './XtextWebSocketClient'; |
9 | import { | 9 | import { |
10 | isOccurrencesResult, | 10 | isConflictResult, |
11 | isServiceConflictResult, | 11 | occurrencesResult, |
12 | ITextRegion, | 12 | TextRegion, |
13 | } from './xtextServiceResults'; | 13 | } from './xtextServiceResults'; |
14 | 14 | ||
15 | const FIND_OCCURRENCES_TIMEOUT_MS = 1000; | 15 | const FIND_OCCURRENCES_TIMEOUT_MS = 1000; |
@@ -20,7 +20,7 @@ const CLEAR_OCCURRENCES_TIMEOUT_MS = 10; | |||
20 | 20 | ||
21 | const log = getLogger('xtext.OccurrencesService'); | 21 | const log = getLogger('xtext.OccurrencesService'); |
22 | 22 | ||
23 | function transformOccurrences(regions: ITextRegion[]): IOccurrence[] { | 23 | function 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'; | |||
11 | import { getLogger } from '../utils/logger'; | 11 | import { getLogger } from '../utils/logger'; |
12 | import { Timer } from '../utils/Timer'; | 12 | import { Timer } from '../utils/Timer'; |
13 | import { | 13 | import { |
14 | IContentAssistEntry, | 14 | ContentAssistEntry, |
15 | isContentAssistResult, | 15 | contentAssistResult, |
16 | isDocumentStateResult, | 16 | documentStateResult, |
17 | isInvalidStateIdConflictResult, | 17 | isConflictResult, |
18 | } from './xtextServiceResults'; | 18 | } from './xtextServiceResults'; |
19 | 19 | ||
20 | const UPDATE_TIMEOUT_MS = 500; | 20 | const 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'; | |||
3 | import type { EditorStore } from '../editor/EditorStore'; | 3 | import type { EditorStore } from '../editor/EditorStore'; |
4 | import type { UpdateService } from './UpdateService'; | 4 | import type { UpdateService } from './UpdateService'; |
5 | import { getLogger } from '../utils/logger'; | 5 | import { getLogger } from '../utils/logger'; |
6 | import { isValidationResult } from './xtextServiceResults'; | 6 | import { validationResult } from './xtextServiceResults'; |
7 | |||
8 | const log = getLogger('xtext.ValidationService'); | ||
9 | 7 | ||
10 | export class ValidationService { | 8 | export 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'; | |||
12 | import { getLogger } from '../utils/logger'; | 12 | import { getLogger } from '../utils/logger'; |
13 | import { ValidationService } from './ValidationService'; | 13 | import { ValidationService } from './ValidationService'; |
14 | import { XtextWebSocketClient } from './XtextWebSocketClient'; | 14 | import { XtextWebSocketClient } from './XtextWebSocketClient'; |
15 | import { XtextWebPushService } from './xtextMessages'; | ||
15 | 16 | ||
16 | const log = getLogger('xtext.XtextClient'); | 17 | const 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'; | |||
4 | import { PendingTask } from '../utils/PendingTask'; | 4 | import { PendingTask } from '../utils/PendingTask'; |
5 | import { Timer } from '../utils/Timer'; | 5 | import { Timer } from '../utils/Timer'; |
6 | import { | 6 | import { |
7 | isErrorResponse, | 7 | xtextWebErrorResponse, |
8 | isOkResponse, | 8 | XtextWebRequest, |
9 | isPushMessage, | 9 | xtextWebOkResponse, |
10 | IXtextWebRequest, | 10 | xtextWebPushMessage, |
11 | XtextWebPushService, | ||
11 | } from './xtextMessages'; | 12 | } from './xtextMessages'; |
12 | import { isPongResult } from './xtextServiceResults'; | 13 | import { pongResult } from './xtextServiceResults'; |
13 | 14 | ||
14 | const XTEXT_SUBPROTOCOL_V1 = 'tools.refinery.language.web.xtext.v1'; | 15 | const XTEXT_SUBPROTOCOL_V1 = 'tools.refinery.language.web.xtext.v1'; |
15 | 16 | ||
@@ -32,7 +33,7 @@ export type ReconnectHandler = () => void; | |||
32 | export type PushHandler = ( | 33 | export 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 @@ | |||
1 | export interface IXtextWebRequest { | 1 | import { z } from 'zod'; |
2 | id: string; | ||
3 | 2 | ||
4 | request: unknown; | 3 | export const xtextWebRequest = z.object({ |
5 | } | 4 | id: z.string().nonempty(), |
5 | request: z.unknown(), | ||
6 | }); | ||
6 | 7 | ||
7 | export interface IXtextWebOkResponse { | 8 | export type XtextWebRequest = z.infer<typeof xtextWebRequest>; |
8 | id: string; | ||
9 | 9 | ||
10 | response: unknown; | 10 | export const xtextWebOkResponse = z.object({ |
11 | } | 11 | id: z.string().nonempty(), |
12 | response: z.unknown(), | ||
13 | }); | ||
12 | 14 | ||
13 | export function isOkResponse(response: unknown): response is IXtextWebOkResponse { | 15 | export 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 | ||
20 | export const VALID_XTEXT_WEB_ERROR_KINDS = ['request', 'server'] as const; | 17 | export const xtextWebErrorKind = z.enum(['request', 'server']); |
21 | 18 | ||
22 | export type XtextWebErrorKind = typeof VALID_XTEXT_WEB_ERROR_KINDS[number]; | 19 | export type XtextWebErrorKind = z.infer<typeof xtextWebErrorKind>; |
23 | 20 | ||
24 | export function isXtextWebErrorKind(value: unknown): value is XtextWebErrorKind { | 21 | export 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 | ||
29 | export interface IXtextWebErrorResponse { | 27 | export type XtextWebErrorResponse = z.infer<typeof xtextWebErrorResponse>; |
30 | id: string; | ||
31 | 28 | ||
32 | error: XtextWebErrorKind; | 29 | export const xtextWebPushService = z.enum(['highlight', 'validate']); |
33 | 30 | ||
34 | message: string; | 31 | export type XtextWebPushService = z.infer<typeof xtextWebPushService>; |
35 | } | ||
36 | 32 | ||
37 | export function isErrorResponse(response: unknown): response is IXtextWebErrorResponse { | 33 | export 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 | ||
45 | export interface IXtextWebPushMessage { | 40 | export type XtextWebPushMessage = z.infer<typeof xtextWebPushMessage>; |
46 | resource: string; | ||
47 | |||
48 | stateId: string; | ||
49 | |||
50 | service: string; | ||
51 | |||
52 | push: unknown; | ||
53 | } | ||
54 | |||
55 | export 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 @@ | |||
1 | export interface IPongResult { | 1 | import { z } from 'zod'; |
2 | pong: string; | ||
3 | } | ||
4 | |||
5 | export 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 | |||
11 | export interface IDocumentStateResult { | ||
12 | stateId: string; | ||
13 | } | ||
14 | |||
15 | export 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 | |||
21 | export const VALID_CONFLICTS = ['invalidStateId', 'canceled'] as const; | ||
22 | |||
23 | export type Conflict = typeof VALID_CONFLICTS[number]; | ||
24 | |||
25 | export function isConflict(value: unknown): value is Conflict { | ||
26 | return typeof value === 'string' && VALID_CONFLICTS.includes(value as Conflict); | ||
27 | } | ||
28 | |||
29 | export interface IServiceConflictResult { | ||
30 | conflict: Conflict; | ||
31 | } | ||
32 | |||
33 | export function isServiceConflictResult(result: unknown): result is IServiceConflictResult { | ||
34 | const serviceConflictResult = result as IServiceConflictResult; | ||
35 | return typeof serviceConflictResult === 'object' | ||
36 | && isConflict(serviceConflictResult.conflict); | ||
37 | } | ||
38 | |||
39 | export function isInvalidStateIdConflictResult(result: unknown): boolean { | ||
40 | return isServiceConflictResult(result) && result.conflict === 'invalidStateId'; | ||
41 | } | ||
42 | |||
43 | export const VALID_SEVERITIES = ['error', 'warning', 'info', 'ignore'] as const; | ||
44 | |||
45 | export type Severity = typeof VALID_SEVERITIES[number]; | ||
46 | |||
47 | export function isSeverity(value: unknown): value is Severity { | ||
48 | return typeof value === 'string' && VALID_SEVERITIES.includes(value as Severity); | ||
49 | } | ||
50 | |||
51 | export 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 | ||
65 | export function isIssue(value: unknown): value is IIssue { | 3 | export 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 | ||
76 | export interface IValidationResult { | 7 | export type PongResult = z.infer<typeof pongResult>; |
77 | issues: IIssue[]; | ||
78 | } | ||
79 | 8 | ||
80 | function isArrayOfType<T>(value: unknown, check: (entry: unknown) => entry is T): value is T[] { | 9 | export const documentStateResult = z.object({ |
81 | return Array.isArray(value) && (value as T[]).every(check); | 10 | stateId: z.string().nonempty(), |
82 | } | 11 | }); |
83 | 12 | ||
84 | export function isValidationResult(result: unknown): result is IValidationResult { | 13 | export type DocumentStateResult = z.infer<typeof documentStateResult>; |
85 | const validationResult = result as IValidationResult; | ||
86 | return typeof validationResult === 'object' | ||
87 | && isArrayOfType(validationResult.issues, isIssue); | ||
88 | } | ||
89 | |||
90 | export interface IReplaceRegion { | ||
91 | offset: number; | ||
92 | 14 | ||
93 | length: number; | 15 | export const conflict = z.enum(['invalidStateId', 'canceled']); |
94 | 16 | ||
95 | text: string; | 17 | export type Conflict = z.infer<typeof conflict>; |
96 | } | ||
97 | 18 | ||
98 | export function isReplaceRegion(value: unknown): value is IReplaceRegion { | 19 | export 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 | ||
106 | export interface ITextRegion { | 23 | export type ServiceConflictResult = z.infer<typeof serviceConflictResult>; |
107 | offset: number; | ||
108 | 24 | ||
109 | length: number; | 25 | export 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 | ||
112 | export function isTextRegion(value: unknown): value is ITextRegion { | 30 | export 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 | ||
119 | export const VALID_XTEXT_CONTENT_ASSIST_ENTRY_KINDS = [ | 32 | export 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 | |||
141 | export type XtextContentAssistEntryKind = typeof VALID_XTEXT_CONTENT_ASSIST_ENTRY_KINDS[number]; | ||
142 | |||
143 | export 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 | ||
150 | export interface IContentAssistEntry { | 34 | export 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; | 43 | export type Issue = z.infer<typeof issue>; |
154 | 44 | ||
155 | label?: string; | 45 | export const validationResult = z.object({ |
46 | issues: issue.array(), | ||
47 | }); | ||
156 | 48 | ||
157 | description?: string; | 49 | export type ValidationResult = z.infer<typeof validationResult>; |
158 | 50 | ||
159 | documentation?: string; | 51 | export 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; | 57 | export type ReplaceRegion = z.infer<typeof replaceRegion>; |
162 | 58 | ||
163 | textReplacements: IReplaceRegion[]; | 59 | export const textRegion = z.object({ |
60 | offset: z.number().int().nonnegative(), | ||
61 | length: z.number().int().nonnegative(), | ||
62 | }); | ||
164 | 63 | ||
165 | editPositions: ITextRegion[]; | 64 | export type TextRegion = z.infer<typeof textRegion>; |
166 | 65 | ||
167 | kind: XtextContentAssistEntryKind | string; | 66 | export 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 | ||
170 | function isStringOrUndefined(value: unknown): value is string | undefined { | 78 | export type ContentAssistEntry = z.infer<typeof contentAssistEntry>; |
171 | return typeof value === 'string' || typeof value === 'undefined'; | ||
172 | } | ||
173 | 79 | ||
174 | function isNumberOrUndefined(value: unknown): value is number | undefined { | 80 | export const contentAssistResult = documentStateResult.extend({ |
175 | return typeof value === 'number' || typeof value === 'undefined'; | 81 | entries: contentAssistEntry.array(), |
176 | } | 82 | }); |
177 | 83 | ||
178 | export function isContentAssistEntry(value: unknown): value is IContentAssistEntry { | 84 | export 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 | ||
192 | export interface IContentAssistResult extends IDocumentStateResult { | 86 | export 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(), | |
196 | export 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 | ||
202 | export interface IHighlightingRegion { | 92 | export type HighlightingRegion = z.infer<typeof highlightingRegion>; |
203 | offset: number; | ||
204 | 93 | ||
205 | length: number; | 94 | export const highlightingResult = z.object({ |
95 | regions: highlightingRegion.array(), | ||
96 | }); | ||
206 | 97 | ||
207 | styleClasses: string[]; | 98 | export type HighlightingResult = z.infer<typeof highlightingResult>; |
208 | } | ||
209 | 99 | ||
210 | export function isHighlightingRegion(value: unknown): value is IHighlightingRegion { | 100 | export 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 | |||
218 | export interface IHighlightingResult { | ||
219 | regions: IHighlightingRegion[]; | ||
220 | } | ||
221 | 104 | ||
222 | export function isHighlightingResult(result: unknown): result is IHighlightingResult { | 105 | export type OccurrencesResult = z.infer<typeof occurrencesResult>; |
223 | const highlightingResult = result as IHighlightingResult; | ||
224 | return typeof highlightingResult === 'object' | ||
225 | && isArrayOfType(highlightingResult.regions, isHighlightingRegion); | ||
226 | } | ||
227 | |||
228 | export interface IOccurrencesResult extends IDocumentStateResult { | ||
229 | writeRegions: ITextRegion[]; | ||
230 | |||
231 | readRegions: ITextRegion[]; | ||
232 | } | ||
233 | |||
234 | export 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 | } | ||