aboutsummaryrefslogtreecommitdiffstats
path: root/language-web/src/main/js/xtext/UpdateService.ts
diff options
context:
space:
mode:
Diffstat (limited to 'language-web/src/main/js/xtext/UpdateService.ts')
-rw-r--r--language-web/src/main/js/xtext/UpdateService.ts125
1 files changed, 89 insertions, 36 deletions
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 @@
1import { 1import {
2 ChangeDesc, 2 ChangeDesc,
3 ChangeSet, 3 ChangeSet,
4 ChangeSpec,
5 StateEffect,
4 Transaction, 6 Transaction,
5} from '@codemirror/state'; 7} from '@codemirror/state';
6import { nanoid } from 'nanoid'; 8import { nanoid } from 'nanoid';
@@ -11,10 +13,11 @@ import { ConditionVariable } from '../utils/ConditionVariable';
11import { getLogger } from '../utils/logger'; 13import { getLogger } from '../utils/logger';
12import { Timer } from '../utils/Timer'; 14import { Timer } from '../utils/Timer';
13import { 15import {
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
20const UPDATE_TIMEOUT_MS = 500; 23const UPDATE_TIMEOUT_MS = 500;
@@ -23,6 +26,8 @@ const WAIT_FOR_UPDATE_TIMEOUT_MS = 1000;
23 26
24const log = getLogger('xtext.UpdateService'); 27const log = getLogger('xtext.UpdateService');
25 28
29const setDirtyChanges = StateEffect.define<ChangeSet>();
30
26export interface IAbortSignal { 31export 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();