diff options
author | 2021-10-30 14:29:51 +0200 | |
---|---|---|
committer | 2021-10-31 19:26:13 +0100 | |
commit | 692cb2218b6684d1289660a0d97e644806c31cc5 (patch) | |
tree | 7a8f9565ff62dbdb7ebc328abb71ebcf9bb40877 | |
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
-rw-r--r-- | language-web/package.json | 1 | ||||
-rw-r--r-- | language-web/src/main/js/xtext/ContentAssistService.ts | 66 | ||||
-rw-r--r-- | language-web/src/main/js/xtext/UpdateService.ts | 5 | ||||
-rw-r--r-- | language-web/src/main/js/xtext/XtextClient.ts | 17 | ||||
-rw-r--r-- | language-web/yarn.lock | 5 |
5 files changed, 68 insertions, 26 deletions
diff --git a/language-web/package.json b/language-web/package.json index a602ec8d..9185f8fe 100644 --- a/language-web/package.json +++ b/language-web/package.json | |||
@@ -81,6 +81,7 @@ | |||
81 | "@codemirror/view": "^0.19.9", | 81 | "@codemirror/view": "^0.19.9", |
82 | "@emotion/react": "^11.4.1", | 82 | "@emotion/react": "^11.4.1", |
83 | "@emotion/styled": "^11.3.0", | 83 | "@emotion/styled": "^11.3.0", |
84 | "escape-string-regexp": "^5.0.0", | ||
84 | "@fontsource/jetbrains-mono": "^4.5.0", | 85 | "@fontsource/jetbrains-mono": "^4.5.0", |
85 | "@fontsource/roboto": "^4.5.0", | 86 | "@fontsource/roboto": "^4.5.0", |
86 | "@lezer/lr": "^0.15.4", | 87 | "@lezer/lr": "^0.15.4", |
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 | } |
diff --git a/language-web/src/main/js/xtext/UpdateService.ts b/language-web/src/main/js/xtext/UpdateService.ts index f8ab7438..bbe7bb62 100644 --- a/language-web/src/main/js/xtext/UpdateService.ts +++ b/language-web/src/main/js/xtext/UpdateService.ts | |||
@@ -53,6 +53,11 @@ export class UpdateService { | |||
53 | this.webSocketClient = webSocketClient; | 53 | this.webSocketClient = webSocketClient; |
54 | } | 54 | } |
55 | 55 | ||
56 | onConnect(): Promise<void> { | ||
57 | this.xtextStateId = null; | ||
58 | return this.updateFullText(); | ||
59 | } | ||
60 | |||
56 | onTransaction(transaction: Transaction): void { | 61 | onTransaction(transaction: Transaction): void { |
57 | const { changes } = transaction; | 62 | const { changes } = transaction; |
58 | if (!changes.empty) { | 63 | if (!changes.empty) { |
diff --git a/language-web/src/main/js/xtext/XtextClient.ts b/language-web/src/main/js/xtext/XtextClient.ts index f8b06258..92bad0d3 100644 --- a/language-web/src/main/js/xtext/XtextClient.ts +++ b/language-web/src/main/js/xtext/XtextClient.ts | |||
@@ -14,23 +14,18 @@ import { XtextWebSocketClient } from './XtextWebSocketClient'; | |||
14 | const log = getLogger('xtext.XtextClient'); | 14 | const log = getLogger('xtext.XtextClient'); |
15 | 15 | ||
16 | export class XtextClient { | 16 | export class XtextClient { |
17 | webSocketClient: XtextWebSocketClient; | 17 | private webSocketClient: XtextWebSocketClient; |
18 | 18 | ||
19 | updateService: UpdateService; | 19 | private updateService: UpdateService; |
20 | 20 | ||
21 | contentAssistService: ContentAssistService; | 21 | private contentAssistService: ContentAssistService; |
22 | 22 | ||
23 | validationService: ValidationService; | 23 | private validationService: ValidationService; |
24 | 24 | ||
25 | constructor(store: EditorStore) { | 25 | constructor(store: EditorStore) { |
26 | this.webSocketClient = new XtextWebSocketClient( | 26 | this.webSocketClient = new XtextWebSocketClient( |
27 | async () => { | 27 | () => this.updateService.onConnect(), |
28 | this.updateService.xtextStateId = null; | 28 | (resource, stateId, service, push) => this.onPush(resource, stateId, service, push), |
29 | await this.updateService.updateFullText(); | ||
30 | }, | ||
31 | async (resource, stateId, service, push) => { | ||
32 | await this.onPush(resource, stateId, service, push); | ||
33 | }, | ||
34 | ); | 29 | ); |
35 | this.updateService = new UpdateService(store, this.webSocketClient); | 30 | this.updateService = new UpdateService(store, this.webSocketClient); |
36 | this.contentAssistService = new ContentAssistService(this.updateService); | 31 | this.contentAssistService = new ContentAssistService(this.updateService); |
diff --git a/language-web/yarn.lock b/language-web/yarn.lock index 9d899a52..a29eb715 100644 --- a/language-web/yarn.lock +++ b/language-web/yarn.lock | |||
@@ -3236,6 +3236,11 @@ escape-string-regexp@^4.0.0: | |||
3236 | resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz#14ba83a5d373e3d311e5afca29cf5bfad965bf34" | 3236 | resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz#14ba83a5d373e3d311e5afca29cf5bfad965bf34" |
3237 | integrity sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA== | 3237 | integrity sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA== |
3238 | 3238 | ||
3239 | escape-string-regexp@^5.0.0: | ||
3240 | version "5.0.0" | ||
3241 | resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-5.0.0.tgz#4683126b500b61762f2dbebace1806e8be31b1c8" | ||
3242 | integrity sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw== | ||
3243 | |||
3239 | eslint-config-airbnb-base@^14.2.1: | 3244 | eslint-config-airbnb-base@^14.2.1: |
3240 | version "14.2.1" | 3245 | version "14.2.1" |
3241 | resolved "https://registry.yarnpkg.com/eslint-config-airbnb-base/-/eslint-config-airbnb-base-14.2.1.tgz#8a2eb38455dc5a312550193b319cdaeef042cd1e" | 3246 | resolved "https://registry.yarnpkg.com/eslint-config-airbnb-base/-/eslint-config-airbnb-base-14.2.1.tgz#8a2eb38455dc5a312550193b319cdaeef042cd1e" |