aboutsummaryrefslogtreecommitdiffstats
path: root/language-web/src/main/js/xtext
diff options
context:
space:
mode:
authorLibravatar Kristóf Marussy <kristof@marussy.com>2021-10-30 21:56:42 +0200
committerLibravatar Kristóf Marussy <kristof@marussy.com>2021-10-31 19:26:15 +0100
commit8c90023676fae675e89b1a20c7fc95c63fc1dd5a (patch)
treecef3c3f6716026d1e9998fec8ca9171c6fe478af /language-web/src/main/js/xtext
parentfeat(web): use 4 space for indentation (diff)
downloadrefinery-8c90023676fae675e89b1a20c7fc95c63fc1dd5a.tar.gz
refinery-8c90023676fae675e89b1a20c7fc95c63fc1dd5a.tar.zst
refinery-8c90023676fae675e89b1a20c7fc95c63fc1dd5a.zip
feat(web): find occurrences when idle
Diffstat (limited to 'language-web/src/main/js/xtext')
-rw-r--r--language-web/src/main/js/xtext/HighlightingService.ts16
-rw-r--r--language-web/src/main/js/xtext/OccurrencesService.ts116
-rw-r--r--language-web/src/main/js/xtext/XtextClient.ts9
-rw-r--r--language-web/src/main/js/xtext/xtextServiceResults.ts13
4 files changed, 146 insertions, 8 deletions
diff --git a/language-web/src/main/js/xtext/HighlightingService.ts b/language-web/src/main/js/xtext/HighlightingService.ts
index b8ceed20..451a3a52 100644
--- a/language-web/src/main/js/xtext/HighlightingService.ts
+++ b/language-web/src/main/js/xtext/HighlightingService.ts
@@ -1,7 +1,5 @@
1import { Decoration } from '@codemirror/view';
2import { Range, RangeSet } from '@codemirror/rangeset';
3
4import type { EditorStore } from '../editor/EditorStore'; 1import type { EditorStore } from '../editor/EditorStore';
2import type { IHighlightRange } from '../editor/semanticHighlighting';
5import type { UpdateService } from './UpdateService'; 3import type { UpdateService } from './UpdateService';
6import { getLogger } from '../utils/logger'; 4import { getLogger } from '../utils/logger';
7import { isHighlightingResult } from './xtextServiceResults'; 5import { isHighlightingResult } from './xtextServiceResults';
@@ -24,7 +22,7 @@ export class HighlightingService {
24 return; 22 return;
25 } 23 }
26 const allChanges = this.updateService.computeChangesSinceLastUpdate(); 24 const allChanges = this.updateService.computeChangesSinceLastUpdate();
27 const decorations: Range<Decoration>[] = []; 25 const ranges: IHighlightRange[] = [];
28 push.regions.forEach(({ offset, length, styleClasses }) => { 26 push.regions.forEach(({ offset, length, styleClasses }) => {
29 if (styleClasses.length === 0) { 27 if (styleClasses.length === 0) {
30 return; 28 return;
@@ -34,10 +32,12 @@ export class HighlightingService {
34 if (to <= from) { 32 if (to <= from) {
35 return; 33 return;
36 } 34 }
37 decorations.push(Decoration.mark({ 35 ranges.push({
38 class: styleClasses.map((c) => `cmt-problem-${c}`).join(' '), 36 from,
39 }).range(from, to)); 37 to,
38 classes: styleClasses,
39 });
40 }); 40 });
41 this.store.updateSemanticHighlighting(RangeSet.of(decorations, true)); 41 this.store.updateSemanticHighlighting(ranges);
42 } 42 }
43} 43}
diff --git a/language-web/src/main/js/xtext/OccurrencesService.ts b/language-web/src/main/js/xtext/OccurrencesService.ts
new file mode 100644
index 00000000..804f5ba2
--- /dev/null
+++ b/language-web/src/main/js/xtext/OccurrencesService.ts
@@ -0,0 +1,116 @@
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 isOccurrencesResult,
11 isServiceConflictResult,
12 ITextRegion,
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: ITextRegion[]): 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 store: EditorStore;
38
39 private webSocketClient: XtextWebSocketClient;
40
41 private updateService: UpdateService;
42
43 private hasOccurrences = false;
44
45 private findOccurrencesTimer = new Timer(() => {
46 this.handleFindOccurrences();
47 }, FIND_OCCURRENCES_TIMEOUT_MS);
48
49 private 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
91 || (isServiceConflictResult(result) && result.conflict === 'canceled')) {
92 // Stale occurrences result, the user already made some changes.
93 // We can safely ignore the occurrences and schedule a new find occurrences call.
94 this.clearOccurrences();
95 this.findOccurrencesTimer.schedule();
96 return;
97 }
98 if (!isOccurrencesResult(result) || result.stateId !== this.updateService.xtextStateId) {
99 log.error('Unexpected occurrences result', result);
100 this.clearOccurrences();
101 return;
102 }
103 const write = transformOccurrences(result.writeRegions);
104 const read = transformOccurrences(result.readRegions);
105 this.hasOccurrences = write.length > 0 || read.length > 0;
106 log.debug('Found', write.length, 'write and', read.length, 'read occurrences');
107 this.store.updateOccurrences(write, read);
108 }
109
110 private clearOccurrences() {
111 if (this.hasOccurrences) {
112 this.store.updateOccurrences([], []);
113 this.hasOccurrences = false;
114 }
115 }
116}
diff --git a/language-web/src/main/js/xtext/XtextClient.ts b/language-web/src/main/js/xtext/XtextClient.ts
index ccb58ab4..03b81b1c 100644
--- a/language-web/src/main/js/xtext/XtextClient.ts
+++ b/language-web/src/main/js/xtext/XtextClient.ts
@@ -7,6 +7,7 @@ import type { Transaction } from '@codemirror/state';
7import type { EditorStore } from '../editor/EditorStore'; 7import type { EditorStore } from '../editor/EditorStore';
8import { ContentAssistService } from './ContentAssistService'; 8import { ContentAssistService } from './ContentAssistService';
9import { HighlightingService } from './HighlightingService'; 9import { HighlightingService } from './HighlightingService';
10import { OccurrencesService } from './OccurrencesService';
10import { UpdateService } from './UpdateService'; 11import { UpdateService } from './UpdateService';
11import { getLogger } from '../utils/logger'; 12import { getLogger } from '../utils/logger';
12import { ValidationService } from './ValidationService'; 13import { ValidationService } from './ValidationService';
@@ -25,6 +26,8 @@ export class XtextClient {
25 26
26 private validationService: ValidationService; 27 private validationService: ValidationService;
27 28
29 private occurrencesService: OccurrencesService;
30
28 constructor(store: EditorStore) { 31 constructor(store: EditorStore) {
29 this.webSocketClient = new XtextWebSocketClient( 32 this.webSocketClient = new XtextWebSocketClient(
30 () => this.updateService.onConnect(), 33 () => this.updateService.onConnect(),
@@ -34,6 +37,11 @@ export class XtextClient {
34 this.contentAssistService = new ContentAssistService(this.updateService); 37 this.contentAssistService = new ContentAssistService(this.updateService);
35 this.highlightingService = new HighlightingService(store, this.updateService); 38 this.highlightingService = new HighlightingService(store, this.updateService);
36 this.validationService = new ValidationService(store, this.updateService); 39 this.validationService = new ValidationService(store, this.updateService);
40 this.occurrencesService = new OccurrencesService(
41 store,
42 this.webSocketClient,
43 this.updateService,
44 );
37 } 45 }
38 46
39 onTransaction(transaction: Transaction): void { 47 onTransaction(transaction: Transaction): void {
@@ -41,6 +49,7 @@ export class XtextClient {
41 // _before_ the current edit, so we call it before `updateService`. 49 // _before_ the current edit, so we call it before `updateService`.
42 this.contentAssistService.onTransaction(transaction); 50 this.contentAssistService.onTransaction(transaction);
43 this.updateService.onTransaction(transaction); 51 this.updateService.onTransaction(transaction);
52 this.occurrencesService.onTransaction(transaction);
44 } 53 }
45 54
46 private async onPush(resource: string, stateId: string, service: string, push: unknown) { 55 private async onPush(resource: string, stateId: string, service: string, push: unknown) {
diff --git a/language-web/src/main/js/xtext/xtextServiceResults.ts b/language-web/src/main/js/xtext/xtextServiceResults.ts
index e32d49c3..b2de1e4a 100644
--- a/language-web/src/main/js/xtext/xtextServiceResults.ts
+++ b/language-web/src/main/js/xtext/xtextServiceResults.ts
@@ -224,3 +224,16 @@ export function isHighlightingResult(result: unknown): result is IHighlightingRe
224 return typeof highlightingResult === 'object' 224 return typeof highlightingResult === 'object'
225 && isArrayOfType(highlightingResult.regions, isHighlightingRegion); 225 && isArrayOfType(highlightingResult.regions, isHighlightingRegion);
226} 226}
227
228export interface IOccurrencesResult extends IDocumentStateResult {
229 writeRegions: ITextRegion[];
230
231 readRegions: ITextRegion[];
232}
233
234export function isOccurrencesResult(result: unknown): result is IOccurrencesResult {
235 const occurrencesResult = result as IOccurrencesResult;
236 return isDocumentStateResult(occurrencesResult)
237 && isArrayOfType(occurrencesResult.writeRegions, isTextRegion)
238 && isArrayOfType(occurrencesResult.readRegions, isTextRegion);
239}