diff options
author | 2021-11-16 17:09:58 +0100 | |
---|---|---|
committer | 2021-11-16 17:09:58 +0100 | |
commit | b4ba1dc1fec50a8114d49879dcb30891b8a8107e (patch) | |
tree | c40e4af57988becdcf7d363a2fb7107f668b8ba0 /language-web/src | |
parent | chore(lang): fix Sonar issue (diff) | |
parent | feat(web): xtext formatter client (diff) | |
download | refinery-b4ba1dc1fec50a8114d49879dcb30891b8a8107e.tar.gz refinery-b4ba1dc1fec50a8114d49879dcb30891b8a8107e.tar.zst refinery-b4ba1dc1fec50a8114d49879dcb30891b8a8107e.zip |
Merge pull request #13 from kris7t/xtext-formatter
Xtext formatter support
Diffstat (limited to 'language-web/src')
-rw-r--r-- | language-web/src/main/js/editor/EditorButtons.tsx | 9 | ||||
-rw-r--r-- | language-web/src/main/js/editor/EditorStore.ts | 6 | ||||
-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 | 125 | ||||
-rw-r--r-- | language-web/src/main/js/xtext/ValidationService.ts | 11 | ||||
-rw-r--r-- | language-web/src/main/js/xtext/XtextClient.ts | 13 | ||||
-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 | 287 |
11 files changed, 292 insertions, 353 deletions
diff --git a/language-web/src/main/js/editor/EditorButtons.tsx b/language-web/src/main/js/editor/EditorButtons.tsx index 09ce33dd..150aa00d 100644 --- a/language-web/src/main/js/editor/EditorButtons.tsx +++ b/language-web/src/main/js/editor/EditorButtons.tsx | |||
@@ -7,6 +7,7 @@ import ToggleButtonGroup from '@mui/material/ToggleButtonGroup'; | |||
7 | import CheckIcon from '@mui/icons-material/Check'; | 7 | import CheckIcon from '@mui/icons-material/Check'; |
8 | import ErrorIcon from '@mui/icons-material/Error'; | 8 | import ErrorIcon from '@mui/icons-material/Error'; |
9 | import FormatListNumberedIcon from '@mui/icons-material/FormatListNumbered'; | 9 | import FormatListNumberedIcon from '@mui/icons-material/FormatListNumbered'; |
10 | import FormatPaint from '@mui/icons-material/FormatPaint'; | ||
10 | import InfoOutlinedIcon from '@mui/icons-material/InfoOutlined'; | 11 | import InfoOutlinedIcon from '@mui/icons-material/InfoOutlined'; |
11 | import RedoIcon from '@mui/icons-material/Redo'; | 12 | import RedoIcon from '@mui/icons-material/Redo'; |
12 | import SearchIcon from '@mui/icons-material/Search'; | 13 | import SearchIcon from '@mui/icons-material/Search'; |
@@ -47,7 +48,6 @@ export const EditorButtons = observer(() => { | |||
47 | disabled={!editorStore.canUndo} | 48 | disabled={!editorStore.canUndo} |
48 | onClick={() => editorStore.undo()} | 49 | onClick={() => editorStore.undo()} |
49 | aria-label="Undo" | 50 | aria-label="Undo" |
50 | value="undo" | ||
51 | > | 51 | > |
52 | <UndoIcon fontSize="small" /> | 52 | <UndoIcon fontSize="small" /> |
53 | </IconButton> | 53 | </IconButton> |
@@ -55,7 +55,6 @@ export const EditorButtons = observer(() => { | |||
55 | disabled={!editorStore.canRedo} | 55 | disabled={!editorStore.canRedo} |
56 | onClick={() => editorStore.redo()} | 56 | onClick={() => editorStore.redo()} |
57 | aria-label="Redo" | 57 | aria-label="Redo" |
58 | value="redo" | ||
59 | > | 58 | > |
60 | <RedoIcon fontSize="small" /> | 59 | <RedoIcon fontSize="small" /> |
61 | </IconButton> | 60 | </IconButton> |
@@ -88,6 +87,12 @@ export const EditorButtons = observer(() => { | |||
88 | {getLintIcon(editorStore.highestDiagnosticLevel)} | 87 | {getLintIcon(editorStore.highestDiagnosticLevel)} |
89 | </ToggleButton> | 88 | </ToggleButton> |
90 | </ToggleButtonGroup> | 89 | </ToggleButtonGroup> |
90 | <IconButton | ||
91 | onClick={() => editorStore.formatText()} | ||
92 | aria-label="Automatic format" | ||
93 | > | ||
94 | <FormatPaint fontSize="small" /> | ||
95 | </IconButton> | ||
91 | </Stack> | 96 | </Stack> |
92 | ); | 97 | ); |
93 | }); | 98 | }); |
diff --git a/language-web/src/main/js/editor/EditorStore.ts b/language-web/src/main/js/editor/EditorStore.ts index 059233f4..5760de28 100644 --- a/language-web/src/main/js/editor/EditorStore.ts +++ b/language-web/src/main/js/editor/EditorStore.ts | |||
@@ -115,6 +115,7 @@ export class EditorStore { | |||
115 | lineNumbers(), | 115 | lineNumbers(), |
116 | foldGutter(), | 116 | foldGutter(), |
117 | keymap.of([ | 117 | keymap.of([ |
118 | { key: 'Mod-Shift-f', run: () => this.formatText() }, | ||
118 | ...closeBracketsKeymap, | 119 | ...closeBracketsKeymap, |
119 | ...commentKeymap, | 120 | ...commentKeymap, |
120 | ...completionKeymap, | 121 | ...completionKeymap, |
@@ -280,4 +281,9 @@ export class EditorStore { | |||
280 | toggleLintPanel(): void { | 281 | toggleLintPanel(): void { |
281 | this.setLintPanelOpen(!this.showLintPanel); | 282 | this.setLintPanelOpen(!this.showLintPanel); |
282 | } | 283 | } |
284 | |||
285 | formatText(): boolean { | ||
286 | this.client.formatText(); | ||
287 | return true; | ||
288 | } | ||
283 | } | 289 | } |
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..e78944a9 100644 --- a/language-web/src/main/js/xtext/UpdateService.ts +++ b/language-web/src/main/js/xtext/UpdateService.ts | |||
@@ -1,6 +1,8 @@ | |||
1 | import { | 1 | import { |
2 | ChangeDesc, | 2 | ChangeDesc, |
3 | ChangeSet, | 3 | ChangeSet, |
4 | ChangeSpec, | ||
5 | StateEffect, | ||
4 | Transaction, | 6 | Transaction, |
5 | } from '@codemirror/state'; | 7 | } from '@codemirror/state'; |
6 | import { nanoid } from 'nanoid'; | 8 | import { nanoid } from 'nanoid'; |
@@ -11,10 +13,11 @@ import { ConditionVariable } from '../utils/ConditionVariable'; | |||
11 | import { getLogger } from '../utils/logger'; | 13 | import { getLogger } from '../utils/logger'; |
12 | import { Timer } from '../utils/Timer'; | 14 | import { Timer } from '../utils/Timer'; |
13 | import { | 15 | import { |
14 | IContentAssistEntry, | 16 | ContentAssistEntry, |
15 | isContentAssistResult, | 17 | contentAssistResult, |
16 | isDocumentStateResult, | 18 | documentStateResult, |
17 | isInvalidStateIdConflictResult, | 19 | formattingResult, |
20 | isConflictResult, | ||
18 | } from './xtextServiceResults'; | 21 | } from './xtextServiceResults'; |
19 | 22 | ||
20 | const UPDATE_TIMEOUT_MS = 500; | 23 | const UPDATE_TIMEOUT_MS = 500; |
@@ -23,6 +26,8 @@ const WAIT_FOR_UPDATE_TIMEOUT_MS = 1000; | |||
23 | 26 | ||
24 | const log = getLogger('xtext.UpdateService'); | 27 | const log = getLogger('xtext.UpdateService'); |
25 | 28 | ||
29 | const setDirtyChanges = StateEffect.define<ChangeSet>(); | ||
30 | |||
26 | export interface IAbortSignal { | 31 | export interface IAbortSignal { |
27 | aborted: boolean; | 32 | aborted: boolean; |
28 | } | 33 | } |
@@ -38,12 +43,12 @@ export class UpdateService { | |||
38 | * The changes being synchronized to the server if a full or delta text update is running, | 43 | * The changes being synchronized to the server if a full or delta text update is running, |
39 | * `null` otherwise. | 44 | * `null` otherwise. |
40 | */ | 45 | */ |
41 | private pendingUpdate: ChangeDesc | null = null; | 46 | private pendingUpdate: ChangeSet | null = null; |
42 | 47 | ||
43 | /** | 48 | /** |
44 | * Local changes not yet sychronized to the server and not part of the running update, if any. | 49 | * Local changes not yet sychronized to the server and not part of the running update, if any. |
45 | */ | 50 | */ |
46 | private dirtyChanges: ChangeDesc; | 51 | private dirtyChanges: ChangeSet; |
47 | 52 | ||
48 | private readonly webSocketClient: XtextWebSocketClient; | 53 | private readonly webSocketClient: XtextWebSocketClient; |
49 | 54 | ||
@@ -59,7 +64,7 @@ export class UpdateService { | |||
59 | constructor(store: EditorStore, webSocketClient: XtextWebSocketClient) { | 64 | constructor(store: EditorStore, webSocketClient: XtextWebSocketClient) { |
60 | this.resourceName = `${nanoid(7)}.problem`; | 65 | this.resourceName = `${nanoid(7)}.problem`; |
61 | this.store = store; | 66 | this.store = store; |
62 | this.dirtyChanges = this.newEmptyChangeDesc(); | 67 | this.dirtyChanges = this.newEmptyChangeSet(); |
63 | this.webSocketClient = webSocketClient; | 68 | this.webSocketClient = webSocketClient; |
64 | } | 69 | } |
65 | 70 | ||
@@ -71,8 +76,19 @@ export class UpdateService { | |||
71 | } | 76 | } |
72 | 77 | ||
73 | onTransaction(transaction: Transaction): void { | 78 | onTransaction(transaction: Transaction): void { |
79 | const setDirtyChangesEffect = transaction.effects.find( | ||
80 | (effect) => effect.is(setDirtyChanges), | ||
81 | ) as StateEffect<ChangeSet> | undefined; | ||
82 | if (setDirtyChangesEffect) { | ||
83 | const { value } = setDirtyChangesEffect; | ||
84 | if (this.pendingUpdate !== null) { | ||
85 | this.pendingUpdate = ChangeSet.empty(value.length); | ||
86 | } | ||
87 | this.dirtyChanges = value; | ||
88 | return; | ||
89 | } | ||
74 | if (transaction.docChanged) { | 90 | if (transaction.docChanged) { |
75 | this.dirtyChanges = this.dirtyChanges.composeDesc(transaction.changes.desc); | 91 | this.dirtyChanges = this.dirtyChanges.compose(transaction.changes); |
76 | this.idleUpdateTimer.reschedule(); | 92 | this.idleUpdateTimer.reschedule(); |
77 | } | 93 | } |
78 | } | 94 | } |
@@ -86,7 +102,7 @@ export class UpdateService { | |||
86 | * @return the summary of changes since the last update | 102 | * @return the summary of changes since the last update |
87 | */ | 103 | */ |
88 | computeChangesSinceLastUpdate(): ChangeDesc { | 104 | computeChangesSinceLastUpdate(): ChangeDesc { |
89 | return this.pendingUpdate?.composeDesc(this.dirtyChanges) || this.dirtyChanges; | 105 | return this.pendingUpdate?.composeDesc(this.dirtyChanges.desc) || this.dirtyChanges.desc; |
90 | } | 106 | } |
91 | 107 | ||
92 | private handleIdleUpdate() { | 108 | private handleIdleUpdate() { |
@@ -101,9 +117,8 @@ export class UpdateService { | |||
101 | this.idleUpdateTimer.reschedule(); | 117 | this.idleUpdateTimer.reschedule(); |
102 | } | 118 | } |
103 | 119 | ||
104 | private newEmptyChangeDesc() { | 120 | private newEmptyChangeSet() { |
105 | const changeSet = ChangeSet.of([], this.store.state.doc.length); | 121 | return ChangeSet.of([], this.store.state.doc.length); |
106 | return changeSet.desc; | ||
107 | } | 122 | } |
108 | 123 | ||
109 | async updateFullText(): Promise<void> { | 124 | async updateFullText(): Promise<void> { |
@@ -116,11 +131,8 @@ export class UpdateService { | |||
116 | serviceType: 'update', | 131 | serviceType: 'update', |
117 | fullText: this.store.state.doc.sliceString(0), | 132 | fullText: this.store.state.doc.sliceString(0), |
118 | }); | 133 | }); |
119 | if (isDocumentStateResult(result)) { | 134 | const { stateId } = documentStateResult.parse(result); |
120 | return [result.stateId, undefined]; | 135 | return [stateId, undefined]; |
121 | } | ||
122 | log.error('Unexpected full text update result:', result); | ||
123 | throw new Error('Full text update failed'); | ||
124 | } | 136 | } |
125 | 137 | ||
126 | /** | 138 | /** |
@@ -146,14 +158,14 @@ export class UpdateService { | |||
146 | requiredStateId: this.xtextStateId, | 158 | requiredStateId: this.xtextStateId, |
147 | ...delta, | 159 | ...delta, |
148 | }); | 160 | }); |
149 | if (isDocumentStateResult(result)) { | 161 | const parsedDocumentStateResult = documentStateResult.safeParse(result); |
150 | return [result.stateId, undefined]; | 162 | if (parsedDocumentStateResult.success) { |
163 | return [parsedDocumentStateResult.data.stateId, undefined]; | ||
151 | } | 164 | } |
152 | if (isInvalidStateIdConflictResult(result)) { | 165 | if (isConflictResult(result, 'invalidStateId')) { |
153 | return this.doFallbackToUpdateFullText(); | 166 | return this.doFallbackToUpdateFullText(); |
154 | } | 167 | } |
155 | log.error('Unexpected delta text update result:', result); | 168 | throw parsedDocumentStateResult.error; |
156 | throw new Error('Delta text update failed'); | ||
157 | }); | 169 | }); |
158 | } | 170 | } |
159 | 171 | ||
@@ -163,15 +175,15 @@ export class UpdateService { | |||
163 | } | 175 | } |
164 | log.warn('Delta update failed, performing full text update'); | 176 | log.warn('Delta update failed, performing full text update'); |
165 | this.xtextStateId = null; | 177 | this.xtextStateId = null; |
166 | this.pendingUpdate = this.pendingUpdate.composeDesc(this.dirtyChanges); | 178 | this.pendingUpdate = this.pendingUpdate.compose(this.dirtyChanges); |
167 | this.dirtyChanges = this.newEmptyChangeDesc(); | 179 | this.dirtyChanges = this.newEmptyChangeSet(); |
168 | return this.doUpdateFullText(); | 180 | return this.doUpdateFullText(); |
169 | } | 181 | } |
170 | 182 | ||
171 | async fetchContentAssist( | 183 | async fetchContentAssist( |
172 | params: Record<string, unknown>, | 184 | params: Record<string, unknown>, |
173 | signal: IAbortSignal, | 185 | signal: IAbortSignal, |
174 | ): Promise<IContentAssistEntry[]> { | 186 | ): Promise<ContentAssistEntry[]> { |
175 | await this.prepareForDeltaUpdate(); | 187 | await this.prepareForDeltaUpdate(); |
176 | if (signal.aborted) { | 188 | if (signal.aborted) { |
177 | return []; | 189 | return []; |
@@ -185,18 +197,20 @@ export class UpdateService { | |||
185 | requiredStateId: this.xtextStateId, | 197 | requiredStateId: this.xtextStateId, |
186 | ...delta, | 198 | ...delta, |
187 | }); | 199 | }); |
188 | if (isContentAssistResult(result)) { | 200 | const parsedContentAssistResult = contentAssistResult.safeParse(result); |
189 | return [result.stateId, result.entries]; | 201 | if (parsedContentAssistResult.success) { |
202 | const { stateId, entries: resultEntries } = parsedContentAssistResult.data; | ||
203 | return [stateId, resultEntries]; | ||
190 | } | 204 | } |
191 | if (isInvalidStateIdConflictResult(result)) { | 205 | if (isConflictResult(result, 'invalidStateId')) { |
206 | log.warn('Server state invalid during content assist'); | ||
192 | const [newStateId] = await this.doFallbackToUpdateFullText(); | 207 | const [newStateId] = await this.doFallbackToUpdateFullText(); |
193 | // We must finish this state update transaction to prepare for any push events | 208 | // 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 | 209 | // before querying for content assist, so we just return `null` and will query |
195 | // the content assist service later. | 210 | // the content assist service later. |
196 | return [newStateId, null]; | 211 | return [newStateId, null]; |
197 | } | 212 | } |
198 | log.error('Unextpected content assist result with delta update', result); | 213 | throw parsedContentAssistResult.error; |
199 | throw new Error('Unexpexted content assist result with delta update'); | ||
200 | }); | 214 | }); |
201 | if (entries !== null) { | 215 | if (entries !== null) { |
202 | return entries; | 216 | return entries; |
@@ -214,11 +228,36 @@ export class UpdateService { | |||
214 | ...params, | 228 | ...params, |
215 | requiredStateId: expectedStateId, | 229 | requiredStateId: expectedStateId, |
216 | }); | 230 | }); |
217 | if (isContentAssistResult(result) && result.stateId === expectedStateId) { | 231 | const { stateId, entries } = contentAssistResult.parse(result); |
218 | return result.entries; | 232 | if (stateId !== expectedStateId) { |
233 | throw new Error(`Unexpected state id, expected: ${expectedStateId} got: ${stateId}`); | ||
219 | } | 234 | } |
220 | log.error('Unexpected content assist result', result); | 235 | return entries; |
221 | throw new Error('Unexpected content assist result'); | 236 | } |
237 | |||
238 | async formatText(): Promise<void> { | ||
239 | await this.update(); | ||
240 | let { from, to } = this.store.state.selection.main; | ||
241 | if (to <= from) { | ||
242 | from = 0; | ||
243 | to = this.store.state.doc.length; | ||
244 | } | ||
245 | log.debug('Formatting from', from, 'to', to); | ||
246 | await this.withUpdate(async () => { | ||
247 | const result = await this.webSocketClient.send({ | ||
248 | resource: this.resourceName, | ||
249 | serviceType: 'format', | ||
250 | selectionStart: from, | ||
251 | selectionEnd: to, | ||
252 | }); | ||
253 | const { stateId, formattedText } = formattingResult.parse(result); | ||
254 | this.applyBeforeDirtyChanges({ | ||
255 | from, | ||
256 | to, | ||
257 | insert: formattedText, | ||
258 | }); | ||
259 | return [stateId, null]; | ||
260 | }); | ||
222 | } | 261 | } |
223 | 262 | ||
224 | private computeDelta() { | 263 | private computeDelta() { |
@@ -242,6 +281,20 @@ export class UpdateService { | |||
242 | }; | 281 | }; |
243 | } | 282 | } |
244 | 283 | ||
284 | private applyBeforeDirtyChanges(changeSpec: ChangeSpec) { | ||
285 | const pendingChanges = this.pendingUpdate?.compose(this.dirtyChanges) || this.dirtyChanges; | ||
286 | const revertChanges = pendingChanges.invert(this.store.state.doc); | ||
287 | const applyBefore = ChangeSet.of(changeSpec, revertChanges.newLength); | ||
288 | const redoChanges = pendingChanges.map(applyBefore.desc); | ||
289 | const changeSet = revertChanges.compose(applyBefore).compose(redoChanges); | ||
290 | this.store.dispatch({ | ||
291 | changes: changeSet, | ||
292 | effects: [ | ||
293 | setDirtyChanges.of(redoChanges), | ||
294 | ], | ||
295 | }); | ||
296 | } | ||
297 | |||
245 | /** | 298 | /** |
246 | * Executes an asynchronous callback that updates the state on the server. | 299 | * Executes an asynchronous callback that updates the state on the server. |
247 | * | 300 | * |
@@ -268,7 +321,7 @@ export class UpdateService { | |||
268 | throw new Error('Another update is pending, will not perform update'); | 321 | throw new Error('Another update is pending, will not perform update'); |
269 | } | 322 | } |
270 | this.pendingUpdate = this.dirtyChanges; | 323 | this.pendingUpdate = this.dirtyChanges; |
271 | this.dirtyChanges = this.newEmptyChangeDesc(); | 324 | this.dirtyChanges = this.newEmptyChangeSet(); |
272 | let newStateId: string | null = null; | 325 | let newStateId: string | null = null; |
273 | try { | 326 | try { |
274 | let result: T; | 327 | let result: T; |
@@ -282,7 +335,7 @@ export class UpdateService { | |||
282 | if (this.pendingUpdate === null) { | 335 | if (this.pendingUpdate === null) { |
283 | log.error('pendingUpdate was cleared during update'); | 336 | log.error('pendingUpdate was cleared during update'); |
284 | } else { | 337 | } else { |
285 | this.dirtyChanges = this.pendingUpdate.composeDesc(this.dirtyChanges); | 338 | this.dirtyChanges = this.pendingUpdate.compose(this.dirtyChanges); |
286 | } | 339 | } |
287 | this.pendingUpdate = null; | 340 | this.pendingUpdate = null; |
288 | this.webSocketClient.forceReconnectOnError(); | 341 | this.webSocketClient.forceReconnectOnError(); |
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..0898e725 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,14 +71,16 @@ 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 | ||
80 | contentAssist(context: CompletionContext): Promise<CompletionResult> { | 77 | contentAssist(context: CompletionContext): Promise<CompletionResult> { |
81 | return this.contentAssistService.contentAssist(context); | 78 | return this.contentAssistService.contentAssist(context); |
82 | } | 79 | } |
80 | |||
81 | formatText(): void { | ||
82 | this.updateService.formatText().catch((e) => { | ||
83 | log.error('Error while formatting text', e); | ||
84 | }); | ||
85 | } | ||
83 | } | 86 | } |
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..f79b059c 100644 --- a/language-web/src/main/js/xtext/xtextServiceResults.ts +++ b/language-web/src/main/js/xtext/xtextServiceResults.ts | |||
@@ -1,239 +1,112 @@ | |||
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 | 2 | ||
62 | length: number; | 3 | export const pongResult = z.object({ |
63 | } | 4 | pong: z.string().nonempty(), |
5 | }); | ||
64 | 6 | ||
65 | export function isIssue(value: unknown): value is IIssue { | 7 | export type PongResult = z.infer<typeof pongResult>; |
66 | const issue = value as IIssue; | ||
67 | return typeof issue === 'object' | ||
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 | 8 | ||
76 | export interface IValidationResult { | 9 | export const documentStateResult = z.object({ |
77 | issues: IIssue[]; | 10 | stateId: z.string().nonempty(), |
78 | } | 11 | }); |
79 | 12 | ||
80 | function isArrayOfType<T>(value: unknown, check: (entry: unknown) => entry is T): value is T[] { | 13 | export type DocumentStateResult = z.infer<typeof documentStateResult>; |
81 | return Array.isArray(value) && (value as T[]).every(check); | ||
82 | } | ||
83 | 14 | ||
84 | export function isValidationResult(result: unknown): result is IValidationResult { | 15 | export const conflict = z.enum(['invalidStateId', 'canceled']); |
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 | 16 | ||
93 | length: number; | 17 | export type Conflict = z.infer<typeof conflict>; |
94 | 18 | ||
95 | text: string; | 19 | export const serviceConflictResult = z.object({ |
96 | } | 20 | conflict, |
21 | }); | ||
97 | 22 | ||
98 | export function isReplaceRegion(value: unknown): value is IReplaceRegion { | 23 | export type ServiceConflictResult = z.infer<typeof serviceConflictResult>; |
99 | const replaceRegion = value as IReplaceRegion; | ||
100 | return typeof replaceRegion === 'object' | ||
101 | && typeof replaceRegion.offset === 'number' | ||
102 | && typeof replaceRegion.length === 'number' | ||
103 | && typeof replaceRegion.text === 'string'; | ||
104 | } | ||
105 | |||
106 | export interface ITextRegion { | ||
107 | offset: number; | ||
108 | |||
109 | length: number; | ||
110 | } | ||
111 | |||
112 | export function isTextRegion(value: unknown): value is ITextRegion { | ||
113 | const textRegion = value as ITextRegion; | ||
114 | return typeof textRegion === 'object' | ||
115 | && typeof textRegion.offset === 'number' | ||
116 | && typeof textRegion.length === 'number'; | ||
117 | } | ||
118 | 24 | ||
119 | export const VALID_XTEXT_CONTENT_ASSIST_ENTRY_KINDS = [ | 25 | export function isConflictResult(result: unknown, conflictType: Conflict): boolean { |
120 | 'TEXT', | 26 | const parsedConflictResult = serviceConflictResult.safeParse(result); |
121 | 'METHOD', | 27 | return parsedConflictResult.success && parsedConflictResult.data.conflict === conflictType; |
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 | } | 28 | } |
149 | 29 | ||
150 | export interface IContentAssistEntry { | 30 | export const severity = z.enum(['error', 'warning', 'info', 'ignore']); |
151 | prefix: string; | ||
152 | 31 | ||
153 | proposal: string; | 32 | export type Severity = z.infer<typeof severity>; |
154 | 33 | ||
155 | label?: string; | 34 | export const issue = z.object({ |
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 | }); | ||
156 | 42 | ||
157 | description?: string; | 43 | export type Issue = z.infer<typeof issue>; |
158 | 44 | ||
159 | documentation?: string; | 45 | export const validationResult = z.object({ |
46 | issues: issue.array(), | ||
47 | }); | ||
160 | 48 | ||
161 | escapePosition?: number; | 49 | export type ValidationResult = z.infer<typeof validationResult>; |
162 | 50 | ||
163 | textReplacements: IReplaceRegion[]; | 51 | export const replaceRegion = z.object({ |
52 | offset: z.number().int().nonnegative(), | ||
53 | length: z.number().int().nonnegative(), | ||
54 | text: z.string(), | ||
55 | }); | ||
164 | 56 | ||
165 | editPositions: ITextRegion[]; | 57 | export type ReplaceRegion = z.infer<typeof replaceRegion>; |
166 | 58 | ||
167 | kind: XtextContentAssistEntryKind | string; | 59 | export const textRegion = z.object({ |
168 | } | 60 | offset: z.number().int().nonnegative(), |
61 | length: z.number().int().nonnegative(), | ||
62 | }); | ||
169 | 63 | ||
170 | function isStringOrUndefined(value: unknown): value is string | undefined { | 64 | export type TextRegion = z.infer<typeof textRegion>; |
171 | return typeof value === 'string' || typeof value === 'undefined'; | ||
172 | } | ||
173 | 65 | ||
174 | function isNumberOrUndefined(value: unknown): value is number | undefined { | 66 | export const contentAssistEntry = z.object({ |
175 | return typeof value === 'number' || typeof value === 'undefined'; | 67 | prefix: z.string(), |
176 | } | 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 | }); | ||
177 | 77 | ||
178 | export function isContentAssistEntry(value: unknown): value is IContentAssistEntry { | 78 | export type ContentAssistEntry = z.infer<typeof contentAssistEntry>; |
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 | 79 | ||
192 | export interface IContentAssistResult extends IDocumentStateResult { | 80 | export const contentAssistResult = documentStateResult.extend({ |
193 | entries: IContentAssistEntry[]; | 81 | entries: contentAssistEntry.array(), |
194 | } | 82 | }); |
195 | 83 | ||
196 | export function isContentAssistResult(result: unknown): result is IContentAssistResult { | 84 | export type ContentAssistResult = z.infer<typeof contentAssistResult>; |
197 | const contentAssistResult = result as IContentAssistResult; | ||
198 | return isDocumentStateResult(result) | ||
199 | && isArrayOfType(contentAssistResult.entries, isContentAssistEntry); | ||
200 | } | ||
201 | 85 | ||
202 | export interface IHighlightingRegion { | 86 | export const highlightingRegion = z.object({ |
203 | offset: number; | 87 | offset: z.number().int().nonnegative(), |
88 | length: z.number().int().nonnegative(), | ||
89 | styleClasses: z.string().nonempty().array(), | ||
90 | }); | ||
204 | 91 | ||
205 | length: number; | 92 | export type HighlightingRegion = z.infer<typeof highlightingRegion>; |
206 | 93 | ||
207 | styleClasses: string[]; | 94 | export const highlightingResult = z.object({ |
208 | } | 95 | regions: highlightingRegion.array(), |
96 | }); | ||
209 | 97 | ||
210 | export function isHighlightingRegion(value: unknown): value is IHighlightingRegion { | 98 | export type HighlightingResult = z.infer<typeof highlightingResult>; |
211 | const region = value as IHighlightingRegion; | ||
212 | return typeof region === 'object' | ||
213 | && typeof region.offset === 'number' | ||
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 | 99 | ||
222 | export function isHighlightingResult(result: unknown): result is IHighlightingResult { | 100 | export const occurrencesResult = documentStateResult.extend({ |
223 | const highlightingResult = result as IHighlightingResult; | 101 | writeRegions: textRegion.array(), |
224 | return typeof highlightingResult === 'object' | 102 | readRegions: textRegion.array(), |
225 | && isArrayOfType(highlightingResult.regions, isHighlightingRegion); | 103 | }); |
226 | } | ||
227 | 104 | ||
228 | export interface IOccurrencesResult extends IDocumentStateResult { | 105 | export type OccurrencesResult = z.infer<typeof occurrencesResult>; |
229 | writeRegions: ITextRegion[]; | ||
230 | 106 | ||
231 | readRegions: ITextRegion[]; | 107 | export const formattingResult = documentStateResult.extend({ |
232 | } | 108 | formattedText: z.string(), |
109 | replaceRegion: textRegion, | ||
110 | }); | ||
233 | 111 | ||
234 | export function isOccurrencesResult(result: unknown): result is IOccurrencesResult { | 112 | export type FormattingResult = z.infer<typeof formattingResult>; |
235 | const occurrencesResult = result as IOccurrencesResult; | ||
236 | return isDocumentStateResult(occurrencesResult) | ||
237 | && isArrayOfType(occurrencesResult.writeRegions, isTextRegion) | ||
238 | && isArrayOfType(occurrencesResult.readRegions, isTextRegion); | ||
239 | } | ||