aboutsummaryrefslogtreecommitdiffstats
path: root/language-web
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
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')
-rw-r--r--language-web/src/main/js/editor/EditorParent.ts6
-rw-r--r--language-web/src/main/js/editor/EditorStore.ts17
-rw-r--r--language-web/src/main/js/editor/decorationSetExtension.ts39
-rw-r--r--language-web/src/main/js/editor/findOccurrences.ts35
-rw-r--r--language-web/src/main/js/editor/semanticHighlighting.ts50
-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
9 files changed, 259 insertions, 42 deletions
diff --git a/language-web/src/main/js/editor/EditorParent.ts b/language-web/src/main/js/editor/EditorParent.ts
index ea8c13b6..b890ac3c 100644
--- a/language-web/src/main/js/editor/EditorParent.ts
+++ b/language-web/src/main/js/editor/EditorParent.ts
@@ -190,5 +190,11 @@ export const EditorParent = styled('div')(({ theme }) => {
190 textAlign: 'center', 190 textAlign: 'center',
191 }, 191 },
192 ...codeMirrorLintStyle, 192 ...codeMirrorLintStyle,
193 '.cm-problem-write': {
194 background: 'rgba(255, 255, 128, 0.3)',
195 },
196 '.cm-problem-read': {
197 background: 'rgba(255, 255, 255, 0.15)',
198 },
193 }; 199 };
194}); 200});
diff --git a/language-web/src/main/js/editor/EditorStore.ts b/language-web/src/main/js/editor/EditorStore.ts
index f47f47a0..8788e00f 100644
--- a/language-web/src/main/js/editor/EditorStore.ts
+++ b/language-web/src/main/js/editor/EditorStore.ts
@@ -30,7 +30,6 @@ import {
30 TransactionSpec, 30 TransactionSpec,
31} from '@codemirror/state'; 31} from '@codemirror/state';
32import { 32import {
33 DecorationSet,
34 drawSelection, 33 drawSelection,
35 EditorView, 34 EditorView,
36 highlightActiveLine, 35 highlightActiveLine,
@@ -43,8 +42,13 @@ import {
43 reaction, 42 reaction,
44} from 'mobx'; 43} from 'mobx';
45 44
45import { findOccurrences, IOccurrence, setOccurrences } from './findOccurrences';
46import { problemLanguageSupport } from '../language/problemLanguageSupport'; 46import { problemLanguageSupport } from '../language/problemLanguageSupport';
47import { semanticHighlighting, setSemanticHighlighting } from './semanticHighlighting'; 47import {
48 IHighlightRange,
49 semanticHighlighting,
50 setSemanticHighlighting,
51} from './semanticHighlighting';
48import type { ThemeStore } from '../theme/ThemeStore'; 52import type { ThemeStore } from '../theme/ThemeStore';
49import { getLogger } from '../utils/logger'; 53import { getLogger } from '../utils/logger';
50import { XtextClient } from '../xtext/XtextClient'; 54import { XtextClient } from '../xtext/XtextClient';
@@ -95,6 +99,7 @@ export class EditorStore {
95 EditorView.theme({}, { 99 EditorView.theme({}, {
96 dark: this.themeStore.darkMode, 100 dark: this.themeStore.darkMode,
97 }), 101 }),
102 findOccurrences,
98 highlightActiveLine(), 103 highlightActiveLine(),
99 highlightActiveLineGutter(), 104 highlightActiveLineGutter(),
100 highlightSpecialChars(), 105 highlightSpecialChars(),
@@ -204,8 +209,12 @@ export class EditorStore {
204 return null; 209 return null;
205 } 210 }
206 211
207 updateSemanticHighlighting(decorations: DecorationSet): void { 212 updateSemanticHighlighting(ranges: IHighlightRange[]): void {
208 this.dispatch(setSemanticHighlighting(decorations)); 213 this.dispatch(setSemanticHighlighting(ranges));
214 }
215
216 updateOccurrences(write: IOccurrence[], read: IOccurrence[]): void {
217 this.dispatch(setOccurrences(write, read));
209 } 218 }
210 219
211 /** 220 /**
diff --git a/language-web/src/main/js/editor/decorationSetExtension.ts b/language-web/src/main/js/editor/decorationSetExtension.ts
new file mode 100644
index 00000000..2d630c20
--- /dev/null
+++ b/language-web/src/main/js/editor/decorationSetExtension.ts
@@ -0,0 +1,39 @@
1import { StateEffect, StateField, TransactionSpec } from '@codemirror/state';
2import { EditorView, Decoration, DecorationSet } from '@codemirror/view';
3
4export type TransactionSpecFactory = (decorations: DecorationSet) => TransactionSpec;
5
6export function decorationSetExtension(): [TransactionSpecFactory, StateField<DecorationSet>] {
7 const setEffect = StateEffect.define<DecorationSet>();
8 const field = StateField.define<DecorationSet>({
9 create() {
10 return Decoration.none;
11 },
12 update(currentDecorations, transaction) {
13 let newDecorations: DecorationSet | null = null;
14 transaction.effects.forEach((effect) => {
15 if (effect.is(setEffect)) {
16 newDecorations = effect.value;
17 }
18 });
19 if (newDecorations === null) {
20 if (transaction.docChanged) {
21 return currentDecorations.map(transaction.changes);
22 }
23 return currentDecorations;
24 }
25 return newDecorations;
26 },
27 provide: (f) => EditorView.decorations.from(f),
28 });
29
30 function transactionSpecFactory(decorations: DecorationSet) {
31 return {
32 effects: [
33 setEffect.of(decorations),
34 ],
35 };
36 }
37
38 return [transactionSpecFactory, field];
39}
diff --git a/language-web/src/main/js/editor/findOccurrences.ts b/language-web/src/main/js/editor/findOccurrences.ts
new file mode 100644
index 00000000..92102746
--- /dev/null
+++ b/language-web/src/main/js/editor/findOccurrences.ts
@@ -0,0 +1,35 @@
1import { Range, RangeSet } from '@codemirror/rangeset';
2import type { TransactionSpec } from '@codemirror/state';
3import { Decoration } from '@codemirror/view';
4
5import { decorationSetExtension } from './decorationSetExtension';
6
7export interface IOccurrence {
8 from: number;
9
10 to: number;
11}
12
13const [setOccurrencesInteral, findOccurrences] = decorationSetExtension();
14
15const writeDecoration = Decoration.mark({
16 class: 'cm-problem-write',
17});
18
19const readDecoration = Decoration.mark({
20 class: 'cm-problem-read',
21});
22
23export function setOccurrences(write: IOccurrence[], read: IOccurrence[]): TransactionSpec {
24 const decorations: Range<Decoration>[] = [];
25 write.forEach(({ from, to }) => {
26 decorations.push(writeDecoration.range(from, to));
27 });
28 read.forEach(({ from, to }) => {
29 decorations.push(readDecoration.range(from, to));
30 });
31 const rangeSet = RangeSet.of(decorations, true);
32 return setOccurrencesInteral(rangeSet);
33}
34
35export { findOccurrences };
diff --git a/language-web/src/main/js/editor/semanticHighlighting.ts b/language-web/src/main/js/editor/semanticHighlighting.ts
index 2d6804f8..2aed421b 100644
--- a/language-web/src/main/js/editor/semanticHighlighting.ts
+++ b/language-web/src/main/js/editor/semanticHighlighting.ts
@@ -1,34 +1,24 @@
1import { StateEffect, StateField, TransactionSpec } from '@codemirror/state'; 1import { RangeSet } from '@codemirror/rangeset';
2import { EditorView, Decoration, DecorationSet } from '@codemirror/view'; 2import type { TransactionSpec } from '@codemirror/state';
3import { Decoration } from '@codemirror/view';
3 4
4const setSemanticHighlightingEffect = StateEffect.define<DecorationSet>(); 5import { decorationSetExtension } from './decorationSetExtension';
5 6
6export function setSemanticHighlighting(decorations: DecorationSet): TransactionSpec { 7export interface IHighlightRange {
7 return { 8 from: number;
8 effects: [ 9
9 setSemanticHighlightingEffect.of(decorations), 10 to: number;
10 ], 11
11 }; 12 classes: string[];
13}
14
15const [setSemanticHighlightingInternal, semanticHighlighting] = decorationSetExtension();
16
17export function setSemanticHighlighting(ranges: IHighlightRange[]): TransactionSpec {
18 const rangeSet = RangeSet.of(ranges.map(({ from, to, classes }) => Decoration.mark({
19 class: classes.map((c) => `cmt-problem-${c}`).join(' '),
20 }).range(from, to)), true);
21 return setSemanticHighlightingInternal(rangeSet);
12} 22}
13 23
14export const semanticHighlighting = StateField.define<DecorationSet>({ 24export { semanticHighlighting };
15 create() {
16 return Decoration.none;
17 },
18 update(currentDecorations, transaction) {
19 let newDecorations: DecorationSet | null = null;
20 transaction.effects.forEach((effect) => {
21 if (effect.is(setSemanticHighlightingEffect)) {
22 newDecorations = effect.value;
23 }
24 });
25 if (newDecorations === null) {
26 if (transaction.docChanged) {
27 return currentDecorations.map(transaction.changes);
28 }
29 return currentDecorations;
30 }
31 return newDecorations;
32 },
33 provide: (f) => EditorView.decorations.from(f),
34});
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}