aboutsummaryrefslogtreecommitdiffstats
path: root/language-web/src/main/js/xtext/ContentAssistService.ts
diff options
context:
space:
mode:
Diffstat (limited to 'language-web/src/main/js/xtext/ContentAssistService.ts')
-rw-r--r--language-web/src/main/js/xtext/ContentAssistService.ts133
1 files changed, 133 insertions, 0 deletions
diff --git a/language-web/src/main/js/xtext/ContentAssistService.ts b/language-web/src/main/js/xtext/ContentAssistService.ts
new file mode 100644
index 00000000..91789864
--- /dev/null
+++ b/language-web/src/main/js/xtext/ContentAssistService.ts
@@ -0,0 +1,133 @@
1import type {
2 Completion,
3 CompletionContext,
4 CompletionResult,
5} from '@codemirror/autocomplete';
6import type { ChangeSet, Transaction } from '@codemirror/state';
7
8import { getLogger } from '../logging';
9import type { UpdateService } from './UpdateService';
10
11const log = getLogger('xtext.ContentAssistService');
12
13export class ContentAssistService {
14 updateService: UpdateService;
15
16 lastCompletion: CompletionResult | null = null;
17
18 constructor(updateService: UpdateService) {
19 this.updateService = updateService;
20 }
21
22 onTransaction(transaction: Transaction): void {
23 if (this.shouldInvalidateCachedCompletion(transaction.changes)) {
24 this.lastCompletion = null;
25 }
26 }
27
28 async contentAssist(context: CompletionContext): Promise<CompletionResult> {
29 const tokenBefore = context.tokenBefore(['QualifiedName']);
30 let range: { from: number, to: number };
31 let selection: { selectionStart?: number, selectionEnd?: number };
32 if (tokenBefore === null) {
33 if (!context.explicit) {
34 return {
35 from: context.pos,
36 options: [],
37 };
38 }
39 range = {
40 from: context.pos,
41 to: context.pos,
42 };
43 selection = {};
44 } else {
45 range = {
46 from: tokenBefore.from,
47 to: tokenBefore.to,
48 };
49 selection = {
50 selectionStart: tokenBefore.from,
51 selectionEnd: tokenBefore.to,
52 };
53 }
54 if (!context.explicit && this.shouldReturnCachedCompletion(tokenBefore)) {
55 log.trace('Returning cached completion result');
56 // Postcondition of `shouldReturnCachedCompletion`: `lastCompletion !== null`
57 return {
58 ...this.lastCompletion as CompletionResult,
59 ...range,
60 };
61 }
62 this.lastCompletion = null;
63 const entries = await this.updateService.fetchContentAssist({
64 resource: this.updateService.resourceName,
65 serviceType: 'assist',
66 caretOffset: context.pos,
67 ...selection,
68 }, context);
69 if (context.aborted) {
70 return {
71 ...range,
72 options: [],
73 };
74 }
75 const options: Completion[] = [];
76 entries.forEach((entry) => {
77 options.push({
78 label: entry.proposal,
79 detail: entry.description,
80 info: entry.documentation,
81 type: entry.kind?.toLowerCase(),
82 boost: entry.kind === 'KEYWORD' ? -90 : 0,
83 });
84 });
85 log.debug('Fetched', options.length, 'completions from server');
86 this.lastCompletion = {
87 ...range,
88 options,
89 span: /^[a-zA-Z0-9_:]*$/,
90 };
91 return this.lastCompletion;
92 }
93
94 private shouldReturnCachedCompletion(
95 token: { from: number, to: number, text: string } | null,
96 ) {
97 if (token === null || this.lastCompletion === null) {
98 return false;
99 }
100 const { from, to, text } = token;
101 const { from: lastFrom, to: lastTo, span } = this.lastCompletion;
102 if (!lastTo) {
103 return true;
104 }
105 const [transformedFrom, transformedTo] = this.mapRangeInclusive(lastFrom, lastTo);
106 return from >= transformedFrom && to <= transformedTo && span && span.exec(text);
107 }
108
109 private shouldInvalidateCachedCompletion(changes: ChangeSet) {
110 if (changes.empty || this.lastCompletion === null) {
111 return false;
112 }
113 const { from: lastFrom, to: lastTo } = this.lastCompletion;
114 if (!lastTo) {
115 return true;
116 }
117 const [transformedFrom, transformedTo] = this.mapRangeInclusive(lastFrom, lastTo);
118 let invalidate = false;
119 changes.iterChangedRanges((fromA, toA) => {
120 if (fromA < transformedFrom || toA > transformedTo) {
121 invalidate = true;
122 }
123 });
124 return invalidate;
125 }
126
127 private mapRangeInclusive(lastFrom: number, lastTo: number): [number, number] {
128 const changes = this.updateService.computeChangesSinceLastUpdate();
129 const transformedFrom = changes.mapPos(lastFrom);
130 const transformedTo = changes.mapPos(lastTo, 1);
131 return [transformedFrom, transformedTo];
132 }
133}