diff options
author | 2021-11-16 16:47:58 +0100 | |
---|---|---|
committer | 2021-11-16 16:47:58 +0100 | |
commit | 11cbae607b5a6f59f5a019b7eb525da689d2bb30 (patch) | |
tree | c40e4af57988becdcf7d363a2fb7107f668b8ba0 /language-web/src/main/js/xtext/UpdateService.ts | |
parent | chore(web): json validation with zod (diff) | |
download | refinery-11cbae607b5a6f59f5a019b7eb525da689d2bb30.tar.gz refinery-11cbae607b5a6f59f5a019b7eb525da689d2bb30.tar.zst refinery-11cbae607b5a6f59f5a019b7eb525da689d2bb30.zip |
feat(web): xtext formatter client
Uses the xtext formatted on the server to format the document.
Also adds the capability to take (delta) changes from the server and
apply them before any pending local changes, then replay the changes.
This means that the server-side formatter is effectively acting as a
second user who is editing the document.
Diffstat (limited to 'language-web/src/main/js/xtext/UpdateService.ts')
-rw-r--r-- | language-web/src/main/js/xtext/UpdateService.ts | 79 |
1 files changed, 67 insertions, 12 deletions
diff --git a/language-web/src/main/js/xtext/UpdateService.ts b/language-web/src/main/js/xtext/UpdateService.ts index fa48c5ab..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'; |
@@ -14,6 +16,7 @@ import { | |||
14 | ContentAssistEntry, | 16 | ContentAssistEntry, |
15 | contentAssistResult, | 17 | contentAssistResult, |
16 | documentStateResult, | 18 | documentStateResult, |
19 | formattingResult, | ||
17 | isConflictResult, | 20 | isConflictResult, |
18 | } from './xtextServiceResults'; | 21 | } from './xtextServiceResults'; |
19 | 22 | ||
@@ -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> { |
@@ -160,8 +175,8 @@ export class UpdateService { | |||
160 | } | 175 | } |
161 | log.warn('Delta update failed, performing full text update'); | 176 | log.warn('Delta update failed, performing full text update'); |
162 | this.xtextStateId = null; | 177 | this.xtextStateId = null; |
163 | this.pendingUpdate = this.pendingUpdate.composeDesc(this.dirtyChanges); | 178 | this.pendingUpdate = this.pendingUpdate.compose(this.dirtyChanges); |
164 | this.dirtyChanges = this.newEmptyChangeDesc(); | 179 | this.dirtyChanges = this.newEmptyChangeSet(); |
165 | return this.doUpdateFullText(); | 180 | return this.doUpdateFullText(); |
166 | } | 181 | } |
167 | 182 | ||
@@ -188,6 +203,7 @@ export class UpdateService { | |||
188 | return [stateId, resultEntries]; | 203 | return [stateId, resultEntries]; |
189 | } | 204 | } |
190 | if (isConflictResult(result, 'invalidStateId')) { | 205 | if (isConflictResult(result, 'invalidStateId')) { |
206 | log.warn('Server state invalid during content assist'); | ||
191 | const [newStateId] = await this.doFallbackToUpdateFullText(); | 207 | const [newStateId] = await this.doFallbackToUpdateFullText(); |
192 | // 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 |
193 | // 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 |
@@ -219,6 +235,31 @@ export class UpdateService { | |||
219 | return entries; | 235 | return entries; |
220 | } | 236 | } |
221 | 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 | }); | ||
261 | } | ||
262 | |||
222 | private computeDelta() { | 263 | private computeDelta() { |
223 | if (this.dirtyChanges.empty) { | 264 | if (this.dirtyChanges.empty) { |
224 | return null; | 265 | return null; |
@@ -240,6 +281,20 @@ export class UpdateService { | |||
240 | }; | 281 | }; |
241 | } | 282 | } |
242 | 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 | |||
243 | /** | 298 | /** |
244 | * Executes an asynchronous callback that updates the state on the server. | 299 | * Executes an asynchronous callback that updates the state on the server. |
245 | * | 300 | * |
@@ -266,7 +321,7 @@ export class UpdateService { | |||
266 | throw new Error('Another update is pending, will not perform update'); | 321 | throw new Error('Another update is pending, will not perform update'); |
267 | } | 322 | } |
268 | this.pendingUpdate = this.dirtyChanges; | 323 | this.pendingUpdate = this.dirtyChanges; |
269 | this.dirtyChanges = this.newEmptyChangeDesc(); | 324 | this.dirtyChanges = this.newEmptyChangeSet(); |
270 | let newStateId: string | null = null; | 325 | let newStateId: string | null = null; |
271 | try { | 326 | try { |
272 | let result: T; | 327 | let result: T; |
@@ -280,7 +335,7 @@ export class UpdateService { | |||
280 | if (this.pendingUpdate === null) { | 335 | if (this.pendingUpdate === null) { |
281 | log.error('pendingUpdate was cleared during update'); | 336 | log.error('pendingUpdate was cleared during update'); |
282 | } else { | 337 | } else { |
283 | this.dirtyChanges = this.pendingUpdate.composeDesc(this.dirtyChanges); | 338 | this.dirtyChanges = this.pendingUpdate.compose(this.dirtyChanges); |
284 | } | 339 | } |
285 | this.pendingUpdate = null; | 340 | this.pendingUpdate = null; |
286 | this.webSocketClient.forceReconnectOnError(); | 341 | this.webSocketClient.forceReconnectOnError(); |