From 692cb2218b6684d1289660a0d97e644806c31cc5 Mon Sep 17 00:00:00 2001 From: Kristóf Marussy Date: Sat, 30 Oct 2021 14:29:51 +0200 Subject: feat(web): server-side content assist filtering --- .../src/main/js/xtext/ContentAssistService.ts | 66 +++++++++++++++++----- language-web/src/main/js/xtext/UpdateService.ts | 5 ++ language-web/src/main/js/xtext/XtextClient.ts | 17 ++---- 3 files changed, 62 insertions(+), 26 deletions(-) (limited to 'language-web/src') 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 { CompletionResult, } from '@codemirror/autocomplete'; import type { ChangeSet, Transaction } from '@codemirror/state'; +import escapeStringRegexp from 'escape-string-regexp'; import { getLogger } from '../logging'; import type { UpdateService } from './UpdateService'; +import type { IContentAssistEntry } from './xtextServiceResults'; + +const PROPOSALS_LIMIT = 1000; + +const IDENTIFIER_REGEXP_STR = '[a-zA-Z0-9_]*'; const log = getLogger('xtext.ContentAssistService'); +function createCompletion(entry: IContentAssistEntry): Completion { + let boost; + switch (entry.kind) { + case 'KEYWORD': + boost = -99; + break; + case 'TEXT': + case 'SNIPPET': + boost = -90; + break; + default: + boost = 0; + break; + } + return { + label: entry.proposal, + detail: entry.description, + info: entry.documentation, + type: entry.kind?.toLowerCase(), + boost, + }; +} + +function computeSpan(prefix: string, entryCount: number) { + const escapedPrefix = escapeStringRegexp(prefix); + if (entryCount < PROPOSALS_LIMIT) { + // Proposals with the current prefix fit the proposals limit. + // We can filter client side as long as the current prefix is preserved. + return new RegExp(`^${escapedPrefix}${IDENTIFIER_REGEXP_STR}$`); + } + // The current prefix overflows the proposals limits, + // so we have to fetch the completions again on the next keypress. + // Hopefully, it'll return a shorter list and we'll be able to filter client side. + return new RegExp(`^${escapedPrefix}$`); +} + export class ContentAssistService { updateService: UpdateService; @@ -28,7 +70,7 @@ export class ContentAssistService { async contentAssist(context: CompletionContext): Promise { const tokenBefore = context.tokenBefore(['QualifiedName']); let range: { from: number, to: number }; - let selection: { selectionStart?: number, selectionEnd?: number }; + let prefix = ''; if (tokenBefore === null) { if (!context.explicit) { return { @@ -40,16 +82,16 @@ export class ContentAssistService { from: context.pos, to: context.pos, }; - selection = {}; + prefix = ''; } else { range = { from: tokenBefore.from, to: tokenBefore.to, }; - selection = { - selectionStart: tokenBefore.from, - selectionEnd: tokenBefore.to, - }; + const prefixLength = context.pos - tokenBefore.from; + if (prefixLength > 0) { + prefix = tokenBefore.text.substring(0, context.pos - tokenBefore.from); + } } if (!context.explicit && this.shouldReturnCachedCompletion(tokenBefore)) { log.trace('Returning cached completion result'); @@ -64,7 +106,7 @@ export class ContentAssistService { resource: this.updateService.resourceName, serviceType: 'assist', caretOffset: context.pos, - ...selection, + proposalsLimit: PROPOSALS_LIMIT, }, context); if (context.aborted) { return { @@ -74,19 +116,13 @@ export class ContentAssistService { } const options: Completion[] = []; entries.forEach((entry) => { - options.push({ - label: entry.proposal, - detail: entry.description, - info: entry.documentation, - type: entry.kind?.toLowerCase(), - boost: entry.kind === 'KEYWORD' ? -90 : 0, - }); + options.push(createCompletion(entry)); }); log.debug('Fetched', options.length, 'completions from server'); this.lastCompletion = { ...range, options, - span: /^[a-zA-Z0-9_:]*$/, + span: computeSpan(prefix, entries.length), }; return this.lastCompletion; } 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 { this.webSocketClient = webSocketClient; } + onConnect(): Promise { + this.xtextStateId = null; + return this.updateFullText(); + } + onTransaction(transaction: Transaction): void { const { changes } = transaction; 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'; const log = getLogger('xtext.XtextClient'); export class XtextClient { - webSocketClient: XtextWebSocketClient; + private webSocketClient: XtextWebSocketClient; - updateService: UpdateService; + private updateService: UpdateService; - contentAssistService: ContentAssistService; + private contentAssistService: ContentAssistService; - validationService: ValidationService; + private validationService: ValidationService; constructor(store: EditorStore) { this.webSocketClient = new XtextWebSocketClient( - async () => { - this.updateService.xtextStateId = null; - await this.updateService.updateFullText(); - }, - async (resource, stateId, service, push) => { - await this.onPush(resource, stateId, service, push); - }, + () => this.updateService.onConnect(), + (resource, stateId, service, push) => this.onPush(resource, stateId, service, push), ); this.updateService = new UpdateService(store, this.webSocketClient); this.contentAssistService = new ContentAssistService(this.updateService); -- cgit v1.2.3-54-g00ecf