aboutsummaryrefslogtreecommitdiffstats
path: root/subprojects/frontend/src/xtext/UpdateService.ts
diff options
context:
space:
mode:
Diffstat (limited to 'subprojects/frontend/src/xtext/UpdateService.ts')
-rw-r--r--subprojects/frontend/src/xtext/UpdateService.ts198
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';
6import type EditorStore from '../editor/EditorStore'; 6import type EditorStore from '../editor/EditorStore';
7import getLogger from '../utils/getLogger'; 7import getLogger from '../utils/getLogger';
8 8
9import UpdateStateTracker, { 9import UpdateStateTracker from './UpdateStateTracker';
10 type LockedState,
11 type PendingUpdate,
12} from './UpdateStateTracker';
13import type { StateUpdateResult, Delta } from './UpdateStateTracker';
14import type XtextWebSocketClient from './XtextWebSocketClient'; 10import type XtextWebSocketClient from './XtextWebSocketClient';
15import { 11import {
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',