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