diff options
Diffstat (limited to 'language-web/src/main/js/xtext/ContentAssistService.ts')
-rw-r--r-- | language-web/src/main/js/xtext/ContentAssistService.ts | 133 |
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 @@ | |||
1 | import type { | ||
2 | Completion, | ||
3 | CompletionContext, | ||
4 | CompletionResult, | ||
5 | } from '@codemirror/autocomplete'; | ||
6 | import type { ChangeSet, Transaction } from '@codemirror/state'; | ||
7 | |||
8 | import { getLogger } from '../logging'; | ||
9 | import type { UpdateService } from './UpdateService'; | ||
10 | |||
11 | const log = getLogger('xtext.ContentAssistService'); | ||
12 | |||
13 | export 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 | } | ||