aboutsummaryrefslogtreecommitdiffstats
path: root/subprojects/frontend/src/xtext/OccurrencesService.ts
diff options
context:
space:
mode:
Diffstat (limited to 'subprojects/frontend/src/xtext/OccurrencesService.ts')
-rw-r--r--subprojects/frontend/src/xtext/OccurrencesService.ts127
1 files changed, 127 insertions, 0 deletions
diff --git a/subprojects/frontend/src/xtext/OccurrencesService.ts b/subprojects/frontend/src/xtext/OccurrencesService.ts
new file mode 100644
index 00000000..bc865537
--- /dev/null
+++ b/subprojects/frontend/src/xtext/OccurrencesService.ts
@@ -0,0 +1,127 @@
1import { Transaction } from '@codemirror/state';
2
3import type { EditorStore } from '../editor/EditorStore';
4import type { IOccurrence } from '../editor/findOccurrences';
5import type { UpdateService } from './UpdateService';
6import { getLogger } from '../utils/logger';
7import { Timer } from '../utils/Timer';
8import { XtextWebSocketClient } from './XtextWebSocketClient';
9import {
10 isConflictResult,
11 occurrencesResult,
12 TextRegion,
13} from './xtextServiceResults';
14
15const FIND_OCCURRENCES_TIMEOUT_MS = 1000;
16
17// Must clear occurrences asynchronously from `onTransaction`,
18// because we must not emit a conflicting transaction when handling the pending transaction.
19const CLEAR_OCCURRENCES_TIMEOUT_MS = 10;
20
21const log = getLogger('xtext.OccurrencesService');
22
23function transformOccurrences(regions: TextRegion[]): IOccurrence[] {
24 const occurrences: IOccurrence[] = [];
25 regions.forEach(({ offset, length }) => {
26 if (length > 0) {
27 occurrences.push({
28 from: offset,
29 to: offset + length,
30 });
31 }
32 });
33 return occurrences;
34}
35
36export class OccurrencesService {
37 private readonly store: EditorStore;
38
39 private readonly webSocketClient: XtextWebSocketClient;
40
41 private readonly updateService: UpdateService;
42
43 private hasOccurrences = false;
44
45 private readonly findOccurrencesTimer = new Timer(() => {
46 this.handleFindOccurrences();
47 }, FIND_OCCURRENCES_TIMEOUT_MS);
48
49 private readonly clearOccurrencesTimer = new Timer(() => {
50 this.clearOccurrences();
51 }, CLEAR_OCCURRENCES_TIMEOUT_MS);
52
53 constructor(
54 store: EditorStore,
55 webSocketClient: XtextWebSocketClient,
56 updateService: UpdateService,
57 ) {
58 this.store = store;
59 this.webSocketClient = webSocketClient;
60 this.updateService = updateService;
61 }
62
63 onTransaction(transaction: Transaction): void {
64 if (transaction.docChanged) {
65 this.clearOccurrencesTimer.schedule();
66 this.findOccurrencesTimer.reschedule();
67 }
68 if (transaction.isUserEvent('select')) {
69 this.findOccurrencesTimer.reschedule();
70 }
71 }
72
73 private handleFindOccurrences() {
74 this.clearOccurrencesTimer.cancel();
75 this.updateOccurrences().catch((error) => {
76 log.error('Unexpected error while updating occurrences', error);
77 this.clearOccurrences();
78 });
79 }
80
81 private async updateOccurrences() {
82 await this.updateService.update();
83 const result = await this.webSocketClient.send({
84 resource: this.updateService.resourceName,
85 serviceType: 'occurrences',
86 expectedStateId: this.updateService.xtextStateId,
87 caretOffset: this.store.state.selection.main.head,
88 });
89 const allChanges = this.updateService.computeChangesSinceLastUpdate();
90 if (!allChanges.empty || isConflictResult(result, 'canceled')) {
91 // Stale occurrences result, the user already made some changes.
92 // We can safely ignore the occurrences and schedule a new find occurrences call.
93 this.clearOccurrences();
94 this.findOccurrencesTimer.schedule();
95 return;
96 }
97 const parsedOccurrencesResult = occurrencesResult.safeParse(result);
98 if (!parsedOccurrencesResult.success) {
99 log.error(
100 'Unexpected occurences result',
101 result,
102 'not an OccurrencesResult: ',
103 parsedOccurrencesResult.error,
104 );
105 this.clearOccurrences();
106 return;
107 }
108 const { stateId, writeRegions, readRegions } = parsedOccurrencesResult.data;
109 if (stateId !== this.updateService.xtextStateId) {
110 log.error('Unexpected state id, expected:', this.updateService.xtextStateId, 'got:', stateId);
111 this.clearOccurrences();
112 return;
113 }
114 const write = transformOccurrences(writeRegions);
115 const read = transformOccurrences(readRegions);
116 this.hasOccurrences = write.length > 0 || read.length > 0;
117 log.debug('Found', write.length, 'write and', read.length, 'read occurrences');
118 this.store.updateOccurrences(write, read);
119 }
120
121 private clearOccurrences() {
122 if (this.hasOccurrences) {
123 this.store.updateOccurrences([], []);
124 this.hasOccurrences = false;
125 }
126 }
127}