diff options
Diffstat (limited to 'language-web/src')
-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 | } | ||