diff options
Diffstat (limited to 'subprojects/frontend/src/xtext/UpdateService.ts')
-rw-r--r-- | subprojects/frontend/src/xtext/UpdateService.ts | 198 |
1 files changed, 79 insertions, 119 deletions
diff --git a/subprojects/frontend/src/xtext/UpdateService.ts b/subprojects/frontend/src/xtext/UpdateService.ts index 94e01ca2..d8782d90 100644 --- a/subprojects/frontend/src/xtext/UpdateService.ts +++ b/subprojects/frontend/src/xtext/UpdateService.ts | |||
@@ -6,11 +6,7 @@ import { nanoid } from 'nanoid'; | |||
6 | import type EditorStore from '../editor/EditorStore'; | 6 | import type EditorStore from '../editor/EditorStore'; |
7 | import getLogger from '../utils/getLogger'; | 7 | import getLogger from '../utils/getLogger'; |
8 | 8 | ||
9 | import UpdateStateTracker, { | 9 | import UpdateStateTracker from './UpdateStateTracker'; |
10 | type LockedState, | ||
11 | type PendingUpdate, | ||
12 | } from './UpdateStateTracker'; | ||
13 | import type { StateUpdateResult, Delta } from './UpdateStateTracker'; | ||
14 | import type XtextWebSocketClient from './XtextWebSocketClient'; | 10 | import type XtextWebSocketClient from './XtextWebSocketClient'; |
15 | import { | 11 | import { |
16 | type ContentAssistEntry, | 12 | type ContentAssistEntry, |
@@ -86,10 +82,10 @@ export default class UpdateService { | |||
86 | } | 82 | } |
87 | 83 | ||
88 | private idleUpdate(): void { | 84 | private idleUpdate(): void { |
89 | if (!this.webSocketClient.isOpen || !this.tracker.hasDirtyChanges) { | 85 | if (!this.webSocketClient.isOpen || !this.tracker.needsUpdate) { |
90 | return; | 86 | return; |
91 | } | 87 | } |
92 | if (!this.tracker.locekdForUpdate) { | 88 | if (!this.tracker.lockedForUpdate) { |
93 | this.updateOrThrow().catch((error) => { | 89 | this.updateOrThrow().catch((error) => { |
94 | if (error === E_CANCELED || error === E_TIMEOUT) { | 90 | if (error === E_CANCELED || error === E_TIMEOUT) { |
95 | log.debug('Idle update cancelled'); | 91 | log.debug('Idle update cancelled'); |
@@ -111,88 +107,64 @@ export default class UpdateService { | |||
111 | * @returns a promise resolving when the update is completed | 107 | * @returns a promise resolving when the update is completed |
112 | */ | 108 | */ |
113 | private async updateOrThrow(): Promise<void> { | 109 | private async updateOrThrow(): Promise<void> { |
114 | // We may check here for the delta to avoid locking, | 110 | if (!this.tracker.needsUpdate) { |
115 | // but we'll need to recompute the delta in the critical section, | ||
116 | // because it may have changed by the time we can acquire the lock. | ||
117 | if ( | ||
118 | !this.tracker.hasDirtyChanges && | ||
119 | this.tracker.xtextStateId !== undefined | ||
120 | ) { | ||
121 | return; | 111 | return; |
122 | } | 112 | } |
123 | await this.tracker.runExclusive((lockedState) => | 113 | await this.tracker.runExclusive(() => this.updateExclusive()); |
124 | this.updateExclusive(lockedState), | ||
125 | ); | ||
126 | } | 114 | } |
127 | 115 | ||
128 | private async updateExclusive(lockedState: LockedState): Promise<void> { | 116 | private async updateExclusive(): Promise<void> { |
129 | if (this.xtextStateId === undefined) { | 117 | if (this.xtextStateId === undefined) { |
130 | await this.updateFullTextExclusive(lockedState); | 118 | await this.updateFullTextExclusive(); |
131 | } | 119 | } |
132 | if (!this.tracker.hasDirtyChanges) { | 120 | const delta = this.tracker.prepareDeltaUpdateExclusive(); |
121 | if (delta === undefined) { | ||
133 | return; | 122 | return; |
134 | } | 123 | } |
135 | await lockedState.updateExclusive(async (pendingUpdate) => { | 124 | log.trace('Editor delta', delta); |
136 | const delta = pendingUpdate.prepareDeltaUpdateExclusive(); | 125 | const result = await this.webSocketClient.send({ |
137 | if (delta === undefined) { | 126 | resource: this.resourceName, |
138 | return undefined; | 127 | serviceType: 'update', |
139 | } | 128 | requiredStateId: this.xtextStateId, |
140 | log.trace('Editor delta', delta); | 129 | ...delta, |
141 | const result = await this.webSocketClient.send({ | ||
142 | resource: this.resourceName, | ||
143 | serviceType: 'update', | ||
144 | requiredStateId: this.xtextStateId, | ||
145 | ...delta, | ||
146 | }); | ||
147 | const parsedDocumentStateResult = DocumentStateResult.safeParse(result); | ||
148 | if (parsedDocumentStateResult.success) { | ||
149 | return parsedDocumentStateResult.data.stateId; | ||
150 | } | ||
151 | if (isConflictResult(result, 'invalidStateId')) { | ||
152 | return this.doUpdateFullTextExclusive(pendingUpdate); | ||
153 | } | ||
154 | throw parsedDocumentStateResult.error; | ||
155 | }); | 130 | }); |
131 | const parsedDocumentStateResult = DocumentStateResult.safeParse(result); | ||
132 | if (parsedDocumentStateResult.success) { | ||
133 | this.tracker.setStateIdExclusive(parsedDocumentStateResult.data.stateId); | ||
134 | return; | ||
135 | } | ||
136 | if (isConflictResult(result, 'invalidStateId')) { | ||
137 | await this.updateFullTextExclusive(); | ||
138 | } | ||
139 | throw parsedDocumentStateResult.error; | ||
156 | } | 140 | } |
157 | 141 | ||
158 | private updateFullTextOrThrow(): Promise<void> { | 142 | private updateFullTextOrThrow(): Promise<void> { |
159 | return this.tracker.runExclusive((lockedState) => | 143 | return this.tracker.runExclusive(() => this.updateFullTextExclusive()); |
160 | this.updateFullTextExclusive(lockedState), | ||
161 | ); | ||
162 | } | 144 | } |
163 | 145 | ||
164 | private async updateFullTextExclusive( | 146 | private async updateFullTextExclusive(): Promise<void> { |
165 | lockedState: LockedState, | ||
166 | ): Promise<void> { | ||
167 | await lockedState.updateExclusive((pendingUpdate) => | ||
168 | this.doUpdateFullTextExclusive(pendingUpdate), | ||
169 | ); | ||
170 | } | ||
171 | |||
172 | private async doUpdateFullTextExclusive( | ||
173 | pendingUpdate: PendingUpdate, | ||
174 | ): Promise<string> { | ||
175 | log.debug('Performing full text update'); | 147 | log.debug('Performing full text update'); |
176 | pendingUpdate.extendPendingUpdateExclusive(); | 148 | this.tracker.prepareFullTextUpdateExclusive(); |
177 | const result = await this.webSocketClient.send({ | 149 | const result = await this.webSocketClient.send({ |
178 | resource: this.resourceName, | 150 | resource: this.resourceName, |
179 | serviceType: 'update', | 151 | serviceType: 'update', |
180 | fullText: this.store.state.doc.sliceString(0), | 152 | fullText: this.store.state.doc.sliceString(0), |
181 | }); | 153 | }); |
182 | const { stateId } = DocumentStateResult.parse(result); | 154 | const { stateId } = DocumentStateResult.parse(result); |
183 | return stateId; | 155 | this.tracker.setStateIdExclusive(stateId); |
184 | } | 156 | } |
185 | 157 | ||
186 | async fetchContentAssist( | 158 | async fetchContentAssist( |
187 | params: ContentAssistParams, | 159 | params: ContentAssistParams, |
188 | signal: AbortSignal, | 160 | signal: AbortSignal, |
189 | ): Promise<ContentAssistEntry[]> { | 161 | ): Promise<ContentAssistEntry[]> { |
190 | if (this.tracker.upToDate && this.xtextStateId !== undefined) { | 162 | if (!this.tracker.hasPendingChanges && this.xtextStateId !== undefined) { |
191 | return this.fetchContentAssistFetchOnly(params, this.xtextStateId); | 163 | return this.fetchContentAssistFetchOnly(params, this.xtextStateId); |
192 | } | 164 | } |
193 | try { | 165 | try { |
194 | return await this.tracker.runExclusiveHighPriority((lockedState) => | 166 | return await this.tracker.runExclusiveHighPriority(() => |
195 | this.fetchContentAssistExclusive(params, lockedState, signal), | 167 | this.fetchContentAssistExclusive(params, signal), |
196 | ); | 168 | ); |
197 | } catch (error) { | 169 | } catch (error) { |
198 | if ((error === E_CANCELED || error === E_TIMEOUT) && signal.aborted) { | 170 | if ((error === E_CANCELED || error === E_TIMEOUT) && signal.aborted) { |
@@ -204,37 +176,29 @@ export default class UpdateService { | |||
204 | 176 | ||
205 | private async fetchContentAssistExclusive( | 177 | private async fetchContentAssistExclusive( |
206 | params: ContentAssistParams, | 178 | params: ContentAssistParams, |
207 | lockedState: LockedState, | ||
208 | signal: AbortSignal, | 179 | signal: AbortSignal, |
209 | ): Promise<ContentAssistEntry[]> { | 180 | ): Promise<ContentAssistEntry[]> { |
210 | if (this.xtextStateId === undefined) { | 181 | if (this.xtextStateId === undefined) { |
211 | await this.updateFullTextExclusive(lockedState); | 182 | await this.updateFullTextExclusive(); |
183 | if (this.xtextStateId === undefined) { | ||
184 | throw new Error('failed to obtain Xtext state id'); | ||
185 | } | ||
212 | } | 186 | } |
213 | if (signal.aborted) { | 187 | if (signal.aborted) { |
214 | return []; | 188 | return []; |
215 | } | 189 | } |
216 | if (this.tracker.hasDirtyChanges) { | 190 | let entries: ContentAssistEntry[] | undefined; |
217 | // Try to fetch while also performing a delta update. | 191 | if (this.tracker.needsUpdate) { |
218 | const fetchUpdateEntries = await lockedState.withUpdateExclusive( | 192 | entries = await this.fetchContentAssistWithDeltaExclusive( |
219 | async (pendingUpdate) => { | 193 | params, |
220 | const delta = pendingUpdate.prepareDeltaUpdateExclusive(); | 194 | this.xtextStateId, |
221 | if (delta === undefined) { | ||
222 | return { newStateId: undefined, data: undefined }; | ||
223 | } | ||
224 | log.trace('Editor delta', delta); | ||
225 | return this.doFetchContentAssistWithDeltaExclusive( | ||
226 | params, | ||
227 | pendingUpdate, | ||
228 | delta, | ||
229 | ); | ||
230 | }, | ||
231 | ); | 195 | ); |
232 | if (fetchUpdateEntries !== undefined) { | 196 | } |
233 | return fetchUpdateEntries; | 197 | if (entries !== undefined) { |
234 | } | 198 | return entries; |
235 | if (signal.aborted) { | 199 | } |
236 | return []; | 200 | if (signal.aborted) { |
237 | } | 201 | return []; |
238 | } | 202 | } |
239 | if (this.xtextStateId === undefined) { | 203 | if (this.xtextStateId === undefined) { |
240 | throw new Error('failed to obtain Xtext state id'); | 204 | throw new Error('failed to obtain Xtext state id'); |
@@ -242,32 +206,35 @@ export default class UpdateService { | |||
242 | return this.fetchContentAssistFetchOnly(params, this.xtextStateId); | 206 | return this.fetchContentAssistFetchOnly(params, this.xtextStateId); |
243 | } | 207 | } |
244 | 208 | ||
245 | private async doFetchContentAssistWithDeltaExclusive( | 209 | private async fetchContentAssistWithDeltaExclusive( |
246 | params: ContentAssistParams, | 210 | params: ContentAssistParams, |
247 | pendingUpdate: PendingUpdate, | 211 | requiredStateId: string, |
248 | delta: Delta, | 212 | ): Promise<ContentAssistEntry[] | undefined> { |
249 | ): Promise<StateUpdateResult<ContentAssistEntry[] | undefined>> { | 213 | const delta = this.tracker.prepareDeltaUpdateExclusive(); |
214 | if (delta === undefined) { | ||
215 | return undefined; | ||
216 | } | ||
217 | log.trace('Editor delta for content assist', delta); | ||
250 | const fetchUpdateResult = await this.webSocketClient.send({ | 218 | const fetchUpdateResult = await this.webSocketClient.send({ |
251 | ...params, | 219 | ...params, |
252 | resource: this.resourceName, | 220 | resource: this.resourceName, |
253 | serviceType: 'assist', | 221 | serviceType: 'assist', |
254 | requiredStateId: this.xtextStateId, | 222 | requiredStateId, |
255 | ...delta, | 223 | ...delta, |
256 | }); | 224 | }); |
257 | const parsedContentAssistResult = | 225 | const parsedContentAssistResult = |
258 | ContentAssistResult.safeParse(fetchUpdateResult); | 226 | ContentAssistResult.safeParse(fetchUpdateResult); |
259 | if (parsedContentAssistResult.success) { | 227 | if (parsedContentAssistResult.success) { |
260 | const { stateId, entries: resultEntries } = | 228 | const { |
261 | parsedContentAssistResult.data; | 229 | data: { stateId, entries }, |
262 | return { newStateId: stateId, data: resultEntries }; | 230 | } = parsedContentAssistResult; |
231 | this.tracker.setStateIdExclusive(stateId); | ||
232 | return entries; | ||
263 | } | 233 | } |
264 | if (isConflictResult(fetchUpdateResult, 'invalidStateId')) { | 234 | if (isConflictResult(fetchUpdateResult, 'invalidStateId')) { |
265 | log.warn('Server state invalid during content assist'); | 235 | log.warn('Server state invalid during content assist'); |
266 | const newStateId = await this.doUpdateFullTextExclusive(pendingUpdate); | 236 | await this.updateFullTextExclusive(); |
267 | // We must finish this state update transaction to prepare for any push events | 237 | return undefined; |
268 | // before querying for content assist, so we just return `undefined` and will query | ||
269 | // the content assist service later. | ||
270 | return { newStateId, data: undefined }; | ||
271 | } | 238 | } |
272 | throw parsedContentAssistResult.error; | 239 | throw parsedContentAssistResult.error; |
273 | } | 240 | } |
@@ -294,33 +261,30 @@ export default class UpdateService { | |||
294 | } | 261 | } |
295 | 262 | ||
296 | formatText(): Promise<void> { | 263 | formatText(): Promise<void> { |
297 | return this.tracker.runExclusiveWithRetries((lockedState) => | 264 | return this.tracker.runExclusiveWithRetries(() => |
298 | this.formatTextExclusive(lockedState), | 265 | this.formatTextExclusive(), |
299 | ); | 266 | ); |
300 | } | 267 | } |
301 | 268 | ||
302 | private async formatTextExclusive(lockedState: LockedState): Promise<void> { | 269 | private async formatTextExclusive(): Promise<void> { |
303 | await this.updateExclusive(lockedState); | 270 | await this.updateExclusive(); |
304 | let { from, to } = this.store.state.selection.main; | 271 | let { from, to } = this.store.state.selection.main; |
305 | if (to <= from) { | 272 | if (to <= from) { |
306 | from = 0; | 273 | from = 0; |
307 | to = this.store.state.doc.length; | 274 | to = this.store.state.doc.length; |
308 | } | 275 | } |
309 | log.debug('Formatting from', from, 'to', to); | 276 | log.debug('Formatting from', from, 'to', to); |
310 | await lockedState.updateExclusive(async (pendingUpdate) => { | 277 | const result = await this.webSocketClient.send({ |
311 | const result = await this.webSocketClient.send({ | 278 | resource: this.resourceName, |
312 | resource: this.resourceName, | 279 | serviceType: 'format', |
313 | serviceType: 'format', | 280 | selectionStart: from, |
314 | selectionStart: from, | 281 | selectionEnd: to, |
315 | selectionEnd: to, | 282 | }); |
316 | }); | 283 | const { stateId, formattedText } = FormattingResult.parse(result); |
317 | const { stateId, formattedText } = FormattingResult.parse(result); | 284 | this.tracker.setStateIdExclusive(stateId, { |
318 | pendingUpdate.applyBeforeDirtyChangesExclusive({ | 285 | from, |
319 | from, | 286 | to, |
320 | to, | 287 | insert: formattedText, |
321 | insert: formattedText, | ||
322 | }); | ||
323 | return stateId; | ||
324 | }); | 288 | }); |
325 | } | 289 | } |
326 | 290 | ||
@@ -335,7 +299,8 @@ export default class UpdateService { | |||
335 | } | 299 | } |
336 | throw error; | 300 | throw error; |
337 | } | 301 | } |
338 | if (!this.tracker.upToDate) { | 302 | const expectedStateId = this.xtextStateId; |
303 | if (expectedStateId === undefined || this.tracker.hasPendingChanges) { | ||
339 | // Just give up if another update is in progress. | 304 | // Just give up if another update is in progress. |
340 | return { cancelled: true }; | 305 | return { cancelled: true }; |
341 | } | 306 | } |
@@ -343,11 +308,6 @@ export default class UpdateService { | |||
343 | if (caretOffsetResult.cancelled) { | 308 | if (caretOffsetResult.cancelled) { |
344 | return { cancelled: true }; | 309 | return { cancelled: true }; |
345 | } | 310 | } |
346 | const expectedStateId = this.xtextStateId; | ||
347 | if (expectedStateId === undefined) { | ||
348 | // If there is no state on the server, don't bother with finding occurrences. | ||
349 | return { cancelled: true }; | ||
350 | } | ||
351 | const data = await this.webSocketClient.send({ | 311 | const data = await this.webSocketClient.send({ |
352 | resource: this.resourceName, | 312 | resource: this.resourceName, |
353 | serviceType: 'occurrences', | 313 | serviceType: 'occurrences', |