diff options
Diffstat (limited to 'language-web/src')
-rw-r--r-- | language-web/src/main/js/utils/ConditionVariable.ts | 64 | ||||
-rw-r--r-- | language-web/src/main/js/xtext/ContentAssistService.ts | 10 | ||||
-rw-r--r-- | language-web/src/main/js/xtext/UpdateService.ts | 52 |
3 files changed, 82 insertions, 44 deletions
diff --git a/language-web/src/main/js/utils/ConditionVariable.ts b/language-web/src/main/js/utils/ConditionVariable.ts new file mode 100644 index 00000000..0910dfa6 --- /dev/null +++ b/language-web/src/main/js/utils/ConditionVariable.ts | |||
@@ -0,0 +1,64 @@ | |||
1 | import { getLogger } from './logger'; | ||
2 | import { PendingTask } from './PendingTask'; | ||
3 | |||
4 | const log = getLogger('utils.ConditionVariable'); | ||
5 | |||
6 | export type Condition = () => boolean; | ||
7 | |||
8 | export class ConditionVariable { | ||
9 | condition: Condition; | ||
10 | |||
11 | defaultTimeout: number; | ||
12 | |||
13 | listeners: PendingTask<void>[] = []; | ||
14 | |||
15 | constructor(condition: Condition, defaultTimeout = 0) { | ||
16 | this.condition = condition; | ||
17 | this.defaultTimeout = defaultTimeout; | ||
18 | } | ||
19 | |||
20 | async waitFor(timeoutMs: number | null = null): Promise<void> { | ||
21 | if (this.condition()) { | ||
22 | return; | ||
23 | } | ||
24 | const timeoutOrDefault = timeoutMs || this.defaultTimeout; | ||
25 | let nowMs = Date.now(); | ||
26 | const endMs = nowMs + timeoutOrDefault; | ||
27 | while (!this.condition() && nowMs < endMs) { | ||
28 | const remainingMs = endMs - nowMs; | ||
29 | const promise = new Promise<void>((resolve, reject) => { | ||
30 | if (this.condition()) { | ||
31 | resolve(); | ||
32 | return; | ||
33 | } | ||
34 | const task = new PendingTask(resolve, reject, remainingMs); | ||
35 | this.listeners.push(task); | ||
36 | }); | ||
37 | // We must keep waiting until the update has completed, | ||
38 | // so the tasks can't be started in parallel. | ||
39 | // eslint-disable-next-line no-await-in-loop | ||
40 | await promise; | ||
41 | nowMs = Date.now(); | ||
42 | } | ||
43 | if (!this.condition()) { | ||
44 | log.error('Condition still does not hold after', timeoutOrDefault, 'ms'); | ||
45 | throw new Error('Failed to wait for condition'); | ||
46 | } | ||
47 | } | ||
48 | |||
49 | notifyAll(): void { | ||
50 | this.clearListenersWith((listener) => listener.resolve()); | ||
51 | } | ||
52 | |||
53 | rejectAll(error: unknown): void { | ||
54 | this.clearListenersWith((listener) => listener.reject(error)); | ||
55 | } | ||
56 | |||
57 | private clearListenersWith(callback: (listener: PendingTask<void>) => void) { | ||
58 | // Copy `listeners` so that we don't get into a race condition | ||
59 | // if one of the listeners adds another listener. | ||
60 | const { listeners } = this; | ||
61 | this.listeners = []; | ||
62 | listeners.forEach(callback); | ||
63 | } | ||
64 | } | ||
diff --git a/language-web/src/main/js/xtext/ContentAssistService.ts b/language-web/src/main/js/xtext/ContentAssistService.ts index 9cbb385f..ec6b80d2 100644 --- a/language-web/src/main/js/xtext/ContentAssistService.ts +++ b/language-web/src/main/js/xtext/ContentAssistService.ts | |||
@@ -3,7 +3,7 @@ import type { | |||
3 | CompletionContext, | 3 | CompletionContext, |
4 | CompletionResult, | 4 | CompletionResult, |
5 | } from '@codemirror/autocomplete'; | 5 | } from '@codemirror/autocomplete'; |
6 | import type { ChangeSet, Transaction } from '@codemirror/state'; | 6 | import type { Transaction } from '@codemirror/state'; |
7 | import escapeStringRegexp from 'escape-string-regexp'; | 7 | import escapeStringRegexp from 'escape-string-regexp'; |
8 | 8 | ||
9 | import type { UpdateService } from './UpdateService'; | 9 | import type { UpdateService } from './UpdateService'; |
@@ -62,7 +62,7 @@ export class ContentAssistService { | |||
62 | } | 62 | } |
63 | 63 | ||
64 | onTransaction(transaction: Transaction): void { | 64 | onTransaction(transaction: Transaction): void { |
65 | if (this.shouldInvalidateCachedCompletion(transaction.changes)) { | 65 | if (this.shouldInvalidateCachedCompletion(transaction)) { |
66 | this.lastCompletion = null; | 66 | this.lastCompletion = null; |
67 | } | 67 | } |
68 | } | 68 | } |
@@ -142,8 +142,8 @@ export class ContentAssistService { | |||
142 | return from >= transformedFrom && to <= transformedTo && span && span.exec(text); | 142 | return from >= transformedFrom && to <= transformedTo && span && span.exec(text); |
143 | } | 143 | } |
144 | 144 | ||
145 | private shouldInvalidateCachedCompletion(changes: ChangeSet) { | 145 | private shouldInvalidateCachedCompletion(transaction: Transaction) { |
146 | if (changes.empty || this.lastCompletion === null) { | 146 | if (!transaction.docChanged || this.lastCompletion === null) { |
147 | return false; | 147 | return false; |
148 | } | 148 | } |
149 | const { from: lastFrom, to: lastTo } = this.lastCompletion; | 149 | const { from: lastFrom, to: lastTo } = this.lastCompletion; |
@@ -152,7 +152,7 @@ export class ContentAssistService { | |||
152 | } | 152 | } |
153 | const [transformedFrom, transformedTo] = this.mapRangeInclusive(lastFrom, lastTo); | 153 | const [transformedFrom, transformedTo] = this.mapRangeInclusive(lastFrom, lastTo); |
154 | let invalidate = false; | 154 | let invalidate = false; |
155 | changes.iterChangedRanges((fromA, toA) => { | 155 | transaction.changes.iterChangedRanges((fromA, toA) => { |
156 | if (fromA < transformedFrom || toA > transformedTo) { | 156 | if (fromA < transformedFrom || toA > transformedTo) { |
157 | invalidate = true; | 157 | invalidate = true; |
158 | } | 158 | } |
diff --git a/language-web/src/main/js/xtext/UpdateService.ts b/language-web/src/main/js/xtext/UpdateService.ts index 3ab1daf9..838f9d5b 100644 --- a/language-web/src/main/js/xtext/UpdateService.ts +++ b/language-web/src/main/js/xtext/UpdateService.ts | |||
@@ -7,8 +7,8 @@ import { nanoid } from 'nanoid'; | |||
7 | 7 | ||
8 | import type { EditorStore } from '../editor/EditorStore'; | 8 | import type { EditorStore } from '../editor/EditorStore'; |
9 | import type { XtextWebSocketClient } from './XtextWebSocketClient'; | 9 | import type { XtextWebSocketClient } from './XtextWebSocketClient'; |
10 | import { ConditionVariable } from '../utils/ConditionVariable'; | ||
10 | import { getLogger } from '../utils/logger'; | 11 | import { getLogger } from '../utils/logger'; |
11 | import { PendingTask } from '../utils/PendingTask'; | ||
12 | import { Timer } from '../utils/Timer'; | 12 | import { Timer } from '../utils/Timer'; |
13 | import { | 13 | import { |
14 | IContentAssistEntry, | 14 | IContentAssistEntry, |
@@ -40,7 +40,10 @@ export class UpdateService { | |||
40 | 40 | ||
41 | private webSocketClient: XtextWebSocketClient; | 41 | private webSocketClient: XtextWebSocketClient; |
42 | 42 | ||
43 | private updateListeners: PendingTask<void>[] = []; | 43 | private updatedCondition = new ConditionVariable( |
44 | () => this.pendingUpdate === null && this.xtextStateId !== null, | ||
45 | WAIT_FOR_UPDATE_TIMEOUT_MS, | ||
46 | ); | ||
44 | 47 | ||
45 | private idleUpdateTimer = new Timer(() => { | 48 | private idleUpdateTimer = new Timer(() => { |
46 | this.handleIdleUpdate(); | 49 | this.handleIdleUpdate(); |
@@ -59,9 +62,8 @@ export class UpdateService { | |||
59 | } | 62 | } |
60 | 63 | ||
61 | onTransaction(transaction: Transaction): void { | 64 | onTransaction(transaction: Transaction): void { |
62 | const { changes } = transaction; | 65 | if (transaction.docChanged) { |
63 | if (!changes.empty) { | 66 | this.dirtyChanges = this.dirtyChanges.composeDesc(transaction.changes.desc); |
64 | this.dirtyChanges = this.dirtyChanges.composeDesc(changes.desc); | ||
65 | this.idleUpdateTimer.reschedule(); | 67 | this.idleUpdateTimer.reschedule(); |
66 | } | 68 | } |
67 | } | 69 | } |
@@ -221,13 +223,7 @@ export class UpdateService { | |||
221 | [newStateId, result] = await callback(); | 223 | [newStateId, result] = await callback(); |
222 | this.xtextStateId = newStateId; | 224 | this.xtextStateId = newStateId; |
223 | this.pendingUpdate = null; | 225 | this.pendingUpdate = null; |
224 | // Copy `updateListeners` so that we don't get into a race condition | 226 | this.updatedCondition.notifyAll(); |
225 | // if one of the listeners adds another listener. | ||
226 | const listeners = this.updateListeners; | ||
227 | this.updateListeners = []; | ||
228 | listeners.forEach((listener) => { | ||
229 | listener.resolve(); | ||
230 | }); | ||
231 | return result; | 227 | return result; |
232 | } catch (e) { | 228 | } catch (e) { |
233 | log.error('Error while update', e); | 229 | log.error('Error while update', e); |
@@ -238,39 +234,17 @@ export class UpdateService { | |||
238 | } | 234 | } |
239 | this.pendingUpdate = null; | 235 | this.pendingUpdate = null; |
240 | this.webSocketClient.forceReconnectOnError(); | 236 | this.webSocketClient.forceReconnectOnError(); |
241 | const listeners = this.updateListeners; | 237 | this.updatedCondition.rejectAll(e); |
242 | this.updateListeners = []; | ||
243 | listeners.forEach((listener) => { | ||
244 | listener.reject(e); | ||
245 | }); | ||
246 | throw e; | 238 | throw e; |
247 | } | 239 | } |
248 | } | 240 | } |
249 | 241 | ||
250 | private async prepareForDeltaUpdate() { | 242 | private async prepareForDeltaUpdate() { |
251 | if (this.pendingUpdate === null) { | 243 | // If no update is pending, but the full text hasn't been uploaded to the server yet, |
252 | if (this.xtextStateId === null) { | 244 | // we must start a full text upload. |
253 | return; | 245 | if (this.pendingUpdate === null && this.xtextStateId === null) { |
254 | } | ||
255 | await this.updateFullText(); | 246 | await this.updateFullText(); |
256 | } | 247 | } |
257 | let nowMs = Date.now(); | 248 | await this.updatedCondition.waitFor(); |
258 | const endMs = nowMs + WAIT_FOR_UPDATE_TIMEOUT_MS; | ||
259 | while (this.pendingUpdate !== null && nowMs < endMs) { | ||
260 | const timeoutMs = endMs - nowMs; | ||
261 | const promise = new Promise((resolve, reject) => { | ||
262 | const task = new PendingTask(resolve, reject, timeoutMs); | ||
263 | this.updateListeners.push(task); | ||
264 | }); | ||
265 | // We must keep waiting uptil the update has completed, | ||
266 | // so the tasks can't be started in parallel. | ||
267 | // eslint-disable-next-line no-await-in-loop | ||
268 | await promise; | ||
269 | nowMs = Date.now(); | ||
270 | } | ||
271 | if (this.pendingUpdate !== null || this.xtextStateId === null) { | ||
272 | log.error('No successful update in', WAIT_FOR_UPDATE_TIMEOUT_MS, 'ms'); | ||
273 | throw new Error('Failed to wait for successful update'); | ||
274 | } | ||
275 | } | 249 | } |
276 | } | 250 | } |