aboutsummaryrefslogtreecommitdiffstats
path: root/language-web
diff options
context:
space:
mode:
authorLibravatar Kristóf Marussy <kristof@marussy.com>2021-11-03 01:55:19 +0100
committerLibravatar Kristóf Marussy <kristof@marussy.com>2021-11-03 02:14:21 +0100
commite36be07141300b3300ce7347e9ee089c1a050da7 (patch)
tree99304e61d6eb3682658419f04de003273f44659f /language-web
parentMerge pull request #9 from kris7t/cm6-sonar-fixes (diff)
downloadrefinery-e36be07141300b3300ce7347e9ee089c1a050da7.tar.gz
refinery-e36be07141300b3300ce7347e9ee089c1a050da7.tar.zst
refinery-e36be07141300b3300ce7347e9ee089c1a050da7.zip
fix(web): fix autocomplete prefix behavior
Always try to complete the current token if it is a terminal (e.g., true, false, unknown, and error after a : or an = sign). Autocomplete still only starts without being explicitly invoked if there is a QualifiedName to complete. Discard completions with a shorter prefix than the current token, because they would be filtered out by CodeMirror anyways.
Diffstat (limited to 'language-web')
-rw-r--r--language-web/src/main/js/xtext/ContentAssistService.ts105
1 files changed, 77 insertions, 28 deletions
diff --git a/language-web/src/main/js/xtext/ContentAssistService.ts b/language-web/src/main/js/xtext/ContentAssistService.ts
index f085c5b1..65381b21 100644
--- a/language-web/src/main/js/xtext/ContentAssistService.ts
+++ b/language-web/src/main/js/xtext/ContentAssistService.ts
@@ -3,6 +3,7 @@ import type {
3 CompletionContext, 3 CompletionContext,
4 CompletionResult, 4 CompletionResult,
5} from '@codemirror/autocomplete'; 5} from '@codemirror/autocomplete';
6import { syntaxTree } from '@codemirror/language';
6import type { Transaction } from '@codemirror/state'; 7import type { Transaction } from '@codemirror/state';
7import escapeStringRegexp from 'escape-string-regexp'; 8import escapeStringRegexp from 'escape-string-regexp';
8 9
@@ -10,16 +11,69 @@ import type { UpdateService } from './UpdateService';
10import { getLogger } from '../utils/logger'; 11import { getLogger } from '../utils/logger';
11import type { IContentAssistEntry } from './xtextServiceResults'; 12import type { IContentAssistEntry } from './xtextServiceResults';
12 13
14const IMPLICIT_COMPLETION_TOKENS = [
15 'QualifiedName',
16 'true',
17 'false',
18 'unknown',
19 'error',
20];
21
13const PROPOSALS_LIMIT = 1000; 22const PROPOSALS_LIMIT = 1000;
14 23
15const IDENTIFIER_REGEXP_STR = '[a-zA-Z0-9_]*'; 24const IDENTIFIER_REGEXP_STR = '[a-zA-Z0-9_]*';
16 25
17const HIGH_PRIORITY_KEYWORDS = ['<->']; 26const HIGH_PRIORITY_KEYWORDS = ['<->', '~>'];
18
19const QUALIFIED_NAME_SEPARATOR_REGEXP = /::/g;
20 27
21const log = getLogger('xtext.ContentAssistService'); 28const log = getLogger('xtext.ContentAssistService');
22 29
30interface IFoundToken {
31 from: number;
32
33 to: number;
34
35 name: string;
36
37 text: string;
38}
39
40function findToken({ pos, state }: CompletionContext): IFoundToken | null {
41 const token = syntaxTree(state).resolveInner(pos, -1);
42 if (token === null) {
43 return null;
44 }
45 if (token.firstChild !== null) {
46 // We only autocomplete terminal nodes. If the current node is nonterminal,
47 // returning `null` makes us autocomplete with the empty prefix instead.
48 return null;
49 }
50 return {
51 from: token.from,
52 to: token.to,
53 name: token.name,
54 text: state.sliceDoc(token.from, token.to),
55 };
56}
57
58function shouldCompleteImplicitly(token: IFoundToken | null, context: CompletionContext): boolean {
59 return token !== null
60 && IMPLICIT_COMPLETION_TOKENS.includes(token.name)
61 && context.pos - token.from >= 2;
62}
63
64function computeSpan(prefix: string, entryCount: number): RegExp {
65 const escapedPrefix = escapeStringRegexp(prefix);
66 if (entryCount < PROPOSALS_LIMIT) {
67 // Proposals with the current prefix fit the proposals limit.
68 // We can filter client side as long as the current prefix is preserved.
69 return new RegExp(`^${escapedPrefix}${IDENTIFIER_REGEXP_STR}$`);
70 }
71 // The current prefix overflows the proposals limits,
72 // so we have to fetch the completions again on the next keypress.
73 // Hopefully, it'll return a shorter list and we'll be able to filter client side.
74 return new RegExp(`^${escapedPrefix}$`);
75}
76
23function createCompletion(entry: IContentAssistEntry): Completion { 77function createCompletion(entry: IContentAssistEntry): Completion {
24 let boost; 78 let boost;
25 switch (entry.kind) { 79 switch (entry.kind) {
@@ -33,7 +87,7 @@ function createCompletion(entry: IContentAssistEntry): Completion {
33 break; 87 break;
34 default: { 88 default: {
35 // Penalize qualified names (vs available unqualified names). 89 // Penalize qualified names (vs available unqualified names).
36 const extraSegments = entry.proposal.match(QUALIFIED_NAME_SEPARATOR_REGEXP)?.length || 0; 90 const extraSegments = entry.proposal.match(/::/g)?.length || 0;
37 boost = Math.max(-5 * extraSegments, -50); 91 boost = Math.max(-5 * extraSegments, -50);
38 } 92 }
39 break; 93 break;
@@ -47,19 +101,6 @@ function createCompletion(entry: IContentAssistEntry): Completion {
47 }; 101 };
48} 102}
49 103
50function computeSpan(prefix: string, entryCount: number) {
51 const escapedPrefix = escapeStringRegexp(prefix);
52 if (entryCount < PROPOSALS_LIMIT) {
53 // Proposals with the current prefix fit the proposals limit.
54 // We can filter client side as long as the current prefix is preserved.
55 return new RegExp(`^${escapedPrefix}${IDENTIFIER_REGEXP_STR}$`);
56 }
57 // The current prefix overflows the proposals limits,
58 // so we have to fetch the completions again on the next keypress.
59 // Hopefully, it'll return a shorter list and we'll be able to filter client side.
60 return new RegExp(`^${escapedPrefix}$`);
61}
62
63export class ContentAssistService { 104export class ContentAssistService {
64 private readonly updateService: UpdateService; 105 private readonly updateService: UpdateService;
65 106
@@ -76,16 +117,16 @@ export class ContentAssistService {
76 } 117 }
77 118
78 async contentAssist(context: CompletionContext): Promise<CompletionResult> { 119 async contentAssist(context: CompletionContext): Promise<CompletionResult> {
79 const tokenBefore = context.tokenBefore(['QualifiedName']); 120 const tokenBefore = findToken(context);
121 if (!context.explicit && !shouldCompleteImplicitly(tokenBefore, context)) {
122 return {
123 from: context.pos,
124 options: [],
125 };
126 }
80 let range: { from: number, to: number }; 127 let range: { from: number, to: number };
81 let prefix = ''; 128 let prefix = '';
82 if (tokenBefore === null) { 129 if (tokenBefore === null) {
83 if (!context.explicit) {
84 return {
85 from: context.pos,
86 options: [],
87 };
88 }
89 range = { 130 range = {
90 from: context.pos, 131 from: context.pos,
91 to: context.pos, 132 to: context.pos,
@@ -124,7 +165,12 @@ export class ContentAssistService {
124 } 165 }
125 const options: Completion[] = []; 166 const options: Completion[] = [];
126 entries.forEach((entry) => { 167 entries.forEach((entry) => {
127 options.push(createCompletion(entry)); 168 if (prefix === entry.prefix) {
169 // Xtext will generate completions that do not complete the current token,
170 // e.g., `(` after trying to complete an indetifier,
171 // but we ignore those, since CodeMirror won't filter for them anyways.
172 options.push(createCompletion(entry));
173 }
128 }); 174 });
129 log.debug('Fetched', options.length, 'completions from server'); 175 log.debug('Fetched', options.length, 'completions from server');
130 this.lastCompletion = { 176 this.lastCompletion = {
@@ -137,7 +183,7 @@ export class ContentAssistService {
137 183
138 private shouldReturnCachedCompletion( 184 private shouldReturnCachedCompletion(
139 token: { from: number, to: number, text: string } | null, 185 token: { from: number, to: number, text: string } | null,
140 ) { 186 ): boolean {
141 if (token === null || this.lastCompletion === null) { 187 if (token === null || this.lastCompletion === null) {
142 return false; 188 return false;
143 } 189 }
@@ -147,10 +193,13 @@ export class ContentAssistService {
147 return true; 193 return true;
148 } 194 }
149 const [transformedFrom, transformedTo] = this.mapRangeInclusive(lastFrom, lastTo); 195 const [transformedFrom, transformedTo] = this.mapRangeInclusive(lastFrom, lastTo);
150 return from >= transformedFrom && to <= transformedTo && span && span.exec(text); 196 return from >= transformedFrom
197 && to <= transformedTo
198 && typeof span !== 'undefined'
199 && span.exec(text) !== null;
151 } 200 }
152 201
153 private shouldInvalidateCachedCompletion(transaction: Transaction) { 202 private shouldInvalidateCachedCompletion(transaction: Transaction): boolean {
154 if (!transaction.docChanged || this.lastCompletion === null) { 203 if (!transaction.docChanged || this.lastCompletion === null) {
155 return false; 204 return false;
156 } 205 }