diff options
author | Kristóf Marussy <kristof@marussy.com> | 2021-10-30 14:29:51 +0200 |
---|---|---|
committer | Kristóf Marussy <kristof@marussy.com> | 2021-10-31 19:26:13 +0100 |
commit | 692cb2218b6684d1289660a0d97e644806c31cc5 (patch) | |
tree | 7a8f9565ff62dbdb7ebc328abb71ebcf9bb40877 /language-web/src/main/js/xtext/ContentAssistService.ts | |
parent | chore(web): refactor xtext client (diff) | |
download | refinery-692cb2218b6684d1289660a0d97e644806c31cc5.tar.gz refinery-692cb2218b6684d1289660a0d97e644806c31cc5.tar.zst refinery-692cb2218b6684d1289660a0d97e644806c31cc5.zip |
feat(web): server-side content assist filtering
Diffstat (limited to 'language-web/src/main/js/xtext/ContentAssistService.ts')
-rw-r--r-- | language-web/src/main/js/xtext/ContentAssistService.ts | 66 |
1 files changed, 51 insertions, 15 deletions
diff --git a/language-web/src/main/js/xtext/ContentAssistService.ts b/language-web/src/main/js/xtext/ContentAssistService.ts index 91789864..e9fdd12e 100644 --- a/language-web/src/main/js/xtext/ContentAssistService.ts +++ b/language-web/src/main/js/xtext/ContentAssistService.ts | |||
@@ -4,12 +4,54 @@ import type { | |||
4 | CompletionResult, | 4 | CompletionResult, |
5 | } from '@codemirror/autocomplete'; | 5 | } from '@codemirror/autocomplete'; |
6 | import type { ChangeSet, Transaction } from '@codemirror/state'; | 6 | import type { ChangeSet, Transaction } from '@codemirror/state'; |
7 | import escapeStringRegexp from 'escape-string-regexp'; | ||
7 | 8 | ||
8 | import { getLogger } from '../logging'; | 9 | import { getLogger } from '../logging'; |
9 | import type { UpdateService } from './UpdateService'; | 10 | import type { UpdateService } from './UpdateService'; |
11 | import type { IContentAssistEntry } from './xtextServiceResults'; | ||
12 | |||
13 | const PROPOSALS_LIMIT = 1000; | ||
14 | |||
15 | const IDENTIFIER_REGEXP_STR = '[a-zA-Z0-9_]*'; | ||
10 | 16 | ||
11 | const log = getLogger('xtext.ContentAssistService'); | 17 | const log = getLogger('xtext.ContentAssistService'); |
12 | 18 | ||
19 | function createCompletion(entry: IContentAssistEntry): Completion { | ||
20 | let boost; | ||
21 | switch (entry.kind) { | ||
22 | case 'KEYWORD': | ||
23 | boost = -99; | ||
24 | break; | ||
25 | case 'TEXT': | ||
26 | case 'SNIPPET': | ||
27 | boost = -90; | ||
28 | break; | ||
29 | default: | ||
30 | boost = 0; | ||
31 | break; | ||
32 | } | ||
33 | return { | ||
34 | label: entry.proposal, | ||
35 | detail: entry.description, | ||
36 | info: entry.documentation, | ||
37 | type: entry.kind?.toLowerCase(), | ||
38 | boost, | ||
39 | }; | ||
40 | } | ||
41 | |||
42 | function computeSpan(prefix: string, entryCount: number) { | ||
43 | const escapedPrefix = escapeStringRegexp(prefix); | ||
44 | if (entryCount < PROPOSALS_LIMIT) { | ||
45 | // Proposals with the current prefix fit the proposals limit. | ||
46 | // We can filter client side as long as the current prefix is preserved. | ||
47 | return new RegExp(`^${escapedPrefix}${IDENTIFIER_REGEXP_STR}$`); | ||
48 | } | ||
49 | // The current prefix overflows the proposals limits, | ||
50 | // so we have to fetch the completions again on the next keypress. | ||
51 | // Hopefully, it'll return a shorter list and we'll be able to filter client side. | ||
52 | return new RegExp(`^${escapedPrefix}$`); | ||
53 | } | ||
54 | |||
13 | export class ContentAssistService { | 55 | export class ContentAssistService { |
14 | updateService: UpdateService; | 56 | updateService: UpdateService; |
15 | 57 | ||
@@ -28,7 +70,7 @@ export class ContentAssistService { | |||
28 | async contentAssist(context: CompletionContext): Promise<CompletionResult> { | 70 | async contentAssist(context: CompletionContext): Promise<CompletionResult> { |
29 | const tokenBefore = context.tokenBefore(['QualifiedName']); | 71 | const tokenBefore = context.tokenBefore(['QualifiedName']); |
30 | let range: { from: number, to: number }; | 72 | let range: { from: number, to: number }; |
31 | let selection: { selectionStart?: number, selectionEnd?: number }; | 73 | let prefix = ''; |
32 | if (tokenBefore === null) { | 74 | if (tokenBefore === null) { |
33 | if (!context.explicit) { | 75 | if (!context.explicit) { |
34 | return { | 76 | return { |
@@ -40,16 +82,16 @@ export class ContentAssistService { | |||
40 | from: context.pos, | 82 | from: context.pos, |
41 | to: context.pos, | 83 | to: context.pos, |
42 | }; | 84 | }; |
43 | selection = {}; | 85 | prefix = ''; |
44 | } else { | 86 | } else { |
45 | range = { | 87 | range = { |
46 | from: tokenBefore.from, | 88 | from: tokenBefore.from, |
47 | to: tokenBefore.to, | 89 | to: tokenBefore.to, |
48 | }; | 90 | }; |
49 | selection = { | 91 | const prefixLength = context.pos - tokenBefore.from; |
50 | selectionStart: tokenBefore.from, | 92 | if (prefixLength > 0) { |
51 | selectionEnd: tokenBefore.to, | 93 | prefix = tokenBefore.text.substring(0, context.pos - tokenBefore.from); |
52 | }; | 94 | } |
53 | } | 95 | } |
54 | if (!context.explicit && this.shouldReturnCachedCompletion(tokenBefore)) { | 96 | if (!context.explicit && this.shouldReturnCachedCompletion(tokenBefore)) { |
55 | log.trace('Returning cached completion result'); | 97 | log.trace('Returning cached completion result'); |
@@ -64,7 +106,7 @@ export class ContentAssistService { | |||
64 | resource: this.updateService.resourceName, | 106 | resource: this.updateService.resourceName, |
65 | serviceType: 'assist', | 107 | serviceType: 'assist', |
66 | caretOffset: context.pos, | 108 | caretOffset: context.pos, |
67 | ...selection, | 109 | proposalsLimit: PROPOSALS_LIMIT, |
68 | }, context); | 110 | }, context); |
69 | if (context.aborted) { | 111 | if (context.aborted) { |
70 | return { | 112 | return { |
@@ -74,19 +116,13 @@ export class ContentAssistService { | |||
74 | } | 116 | } |
75 | const options: Completion[] = []; | 117 | const options: Completion[] = []; |
76 | entries.forEach((entry) => { | 118 | entries.forEach((entry) => { |
77 | options.push({ | 119 | options.push(createCompletion(entry)); |
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 | }); | 120 | }); |
85 | log.debug('Fetched', options.length, 'completions from server'); | 121 | log.debug('Fetched', options.length, 'completions from server'); |
86 | this.lastCompletion = { | 122 | this.lastCompletion = { |
87 | ...range, | 123 | ...range, |
88 | options, | 124 | options, |
89 | span: /^[a-zA-Z0-9_:]*$/, | 125 | span: computeSpan(prefix, entries.length), |
90 | }; | 126 | }; |
91 | return this.lastCompletion; | 127 | return this.lastCompletion; |
92 | } | 128 | } |