Skip to content

Commit af44f8b

Browse files
jadestrongzhangyuqiangrchl
authored
feat: provide filterText property in completions (typescript-language-server#678)
Co-authored-by: zhangyuqiang <[email protected]> Co-authored-by: Rafal Chlodnicki <[email protected]>
1 parent dfea05e commit af44f8b

File tree

4 files changed

+102
-14
lines changed

4 files changed

+102
-14
lines changed

.gitignore

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
1-
/lib/
2-
/node_modules/
3-
/.rollup.cache/
4-
.DS_Store
51
*.log
62
*.tsbuildinfo
3+
.DS_Store
4+
/.rollup.cache/
5+
/lib/
6+
node_modules/

src/completion.ts

Lines changed: 72 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -23,14 +23,24 @@ interface ParameterListParts {
2323
readonly hasOptionalParameters: boolean;
2424
}
2525

26+
export interface CompletionContext {
27+
readonly isMemberCompletion: boolean;
28+
readonly dotAccessorContext?: {
29+
range: lsp.Range;
30+
text: string;
31+
};
32+
readonly line: string;
33+
readonly optionalReplacementRange: lsp.Range | undefined;
34+
}
35+
2636
export function asCompletionItem(
2737
entry: ts.server.protocol.CompletionEntry,
28-
optionalReplacementSpan: ts.server.protocol.TextSpan | undefined,
2938
file: string, position: lsp.Position,
3039
document: LspDocument,
3140
filePathConverter: IFilePathToResourceConverter,
3241
options: WorkspaceConfigurationCompletionOptions,
3342
features: SupportedFeatures,
43+
completionContext: CompletionContext,
3444
): lsp.CompletionItem | null {
3545
const item: lsp.CompletionItem = {
3646
label: entry.name,
@@ -71,7 +81,26 @@ export function asCompletionItem(
7181
item.detail = Previewer.plainWithLinks(sourceDisplay, filePathConverter);
7282
}
7383

84+
const { line, optionalReplacementRange, isMemberCompletion, dotAccessorContext } = completionContext;
85+
let range = getRangeFromReplacementSpan(replacementSpan, optionalReplacementRange, position, document, features);
7486
let { insertText } = entry;
87+
item.filterText = getFilterText(entry, optionalReplacementRange, line, insertText);
88+
89+
if (isMemberCompletion && dotAccessorContext && !entry.isSnippet) {
90+
item.filterText = dotAccessorContext.text + (insertText || item.label);
91+
if (!range) {
92+
if (features.completionInsertReplaceSupport && optionalReplacementRange) {
93+
range = {
94+
insert: dotAccessorContext.range,
95+
replace: Range.union(dotAccessorContext.range, optionalReplacementRange),
96+
};
97+
} else {
98+
range = { replace: dotAccessorContext.range };
99+
}
100+
insertText = item.filterText;
101+
}
102+
}
103+
75104
if (entry.kindModifiers) {
76105
const kindModifiers = new Set(entry.kindModifiers.split(/,|\s+/g));
77106
if (kindModifiers.has(KindModifiers.optional)) {
@@ -101,20 +130,21 @@ export function asCompletionItem(
101130
}
102131
}
103132
}
104-
const range = getRangeFromReplacementSpan(replacementSpan, optionalReplacementSpan, position, document, features);
133+
105134
if (range) {
106135
item.textEdit = range.insert
107136
? lsp.InsertReplaceEdit.create(insertText || item.label, range.insert, range.replace)
108137
: lsp.TextEdit.replace(range.replace, insertText || item.label);
109138
} else {
110139
item.insertText = insertText;
111140
}
141+
112142
return item;
113143
}
114144

115145
function getRangeFromReplacementSpan(
116146
replacementSpan: ts.server.protocol.TextSpan | undefined,
117-
optionalReplacementSpan: ts.server.protocol.TextSpan | undefined,
147+
optionalReplacementRange: lsp.Range | undefined,
118148
position: lsp.Position,
119149
document: LspDocument,
120150
features: SupportedFeatures,
@@ -125,15 +155,50 @@ function getRangeFromReplacementSpan(
125155
replace: ensureRangeIsOnSingleLine(Range.fromTextSpan(replacementSpan), document),
126156
};
127157
}
128-
if (features.completionInsertReplaceSupport && optionalReplacementSpan) {
129-
const range = ensureRangeIsOnSingleLine(Range.fromTextSpan(optionalReplacementSpan), document);
158+
if (features.completionInsertReplaceSupport && optionalReplacementRange) {
159+
const range = ensureRangeIsOnSingleLine(optionalReplacementRange, document);
130160
return {
131161
insert: lsp.Range.create(range.start, position),
132-
replace: ensureRangeIsOnSingleLine(range, document),
162+
replace: range,
133163
};
134164
}
135165
}
136166

167+
function getFilterText(entry: ts.server.protocol.CompletionEntry, wordRange: lsp.Range | undefined, line: string, insertText: string | undefined): string | undefined {
168+
// Handle private field completions
169+
if (entry.name.startsWith('#')) {
170+
const wordStart = wordRange ? line.charAt(wordRange.start.character) : undefined;
171+
if (insertText) {
172+
if (insertText.startsWith('this.#')) {
173+
return wordStart === '#' ? insertText : insertText.replace(/&this\.#/, '');
174+
} else {
175+
return wordStart;
176+
}
177+
} else {
178+
return wordStart === '#' ? undefined : entry.name.replace(/^#/, '');
179+
}
180+
}
181+
182+
// For `this.` completions, generally don't set the filter text since we don't want them to be overly prioritized. #74164
183+
if (insertText?.startsWith('this.')) {
184+
return undefined;
185+
}
186+
187+
// Handle the case:
188+
// ```
189+
// const xyz = { 'ab c': 1 };
190+
// xyz.ab|
191+
// ```
192+
// In which case we want to insert a bracket accessor but should use `.abc` as the filter text instead of
193+
// the bracketed insert text.
194+
if (insertText?.startsWith('[')) {
195+
return insertText.replace(/^\[['"](.+)[['"]\]$/, '.$1');
196+
}
197+
198+
// In all other cases, fallback to using the insertText
199+
return insertText;
200+
}
201+
137202
function ensureRangeIsOnSingleLine(range: lsp.Range, document: LspDocument): lsp.Range {
138203
if (range.start.line !== range.end.line) {
139204
return lsp.Range.create(range.start, document.getLineEnd(range.start.line));
@@ -279,7 +344,7 @@ function createSnippetOfFunctionCall(item: lsp.CompletionItem, detail: ts.server
279344
const { displayParts } = detail;
280345
const parameterListParts = getParameterListParts(displayParts);
281346
const snippet = new SnippetString();
282-
snippet.appendText(`${item.insertText || item.label}(`);
347+
snippet.appendText(`${item.insertText || item.textEdit?.newText || item.label}(`);
283348
appendJoinedPlaceholders(snippet, parameterListParts.parts, ', ');
284349
if (parameterListParts.hasOptionalParameters) {
285350
snippet.appendTabstop();

src/lsp-server.ts

Lines changed: 20 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ import { TspClient } from './tsp-client.js';
1515
import { DiagnosticEventQueue } from './diagnostic-queue.js';
1616
import { toDocumentHighlight, uriToPath, toSymbolKind, toLocation, toSelectionRange, pathToUri, toTextEdit, normalizePath } from './protocol-translation.js';
1717
import { LspDocuments, LspDocument } from './document.js';
18-
import { asCompletionItem, asResolvedCompletionItem, getCompletionTriggerCharacter } from './completion.js';
18+
import { asCompletionItem, asResolvedCompletionItem, CompletionContext, getCompletionTriggerCharacter } from './completion.js';
1919
import { asSignatureHelp, toTsTriggerReason } from './hover.js';
2020
import { Commands, TypescriptVersionNotification } from './commands.js';
2121
import { provideQuickFix } from './quickfix.js';
@@ -625,13 +625,30 @@ export class LspServer {
625625
if (!body) {
626626
return lsp.CompletionList.create();
627627
}
628-
const { entries, isIncomplete, optionalReplacementSpan } = body;
628+
const { entries, isIncomplete, optionalReplacementSpan, isMemberCompletion } = body;
629+
const line = document.getLine(params.position.line);
630+
let dotAccessorContext: CompletionContext['dotAccessorContext'];
631+
if (isMemberCompletion) {
632+
const dotMatch = line.slice(0, params.position.character).match(/\??\.\s*$/) || undefined;
633+
if (dotMatch) {
634+
const startPosition = lsp.Position.create(params.position.line, params.position.character - dotMatch[0].length);
635+
const range = lsp.Range.create(startPosition, params.position);
636+
const text = document.getText(range);
637+
dotAccessorContext = { range, text };
638+
}
639+
}
640+
const completionContext: CompletionContext = {
641+
isMemberCompletion,
642+
dotAccessorContext,
643+
line,
644+
optionalReplacementRange: optionalReplacementSpan ? Range.fromTextSpan(optionalReplacementSpan) : undefined,
645+
};
629646
const completions: lsp.CompletionItem[] = [];
630647
for (const entry of entries || []) {
631648
if (entry.kind === 'warning') {
632649
continue;
633650
}
634-
const completion = asCompletionItem(entry, optionalReplacementSpan, file, params.position, document, this.documents, completionOptions, this.features);
651+
const completion = asCompletionItem(entry, file, params.position, document, this.documents, completionOptions, this.features, completionContext);
635652
if (!completion) {
636653
continue;
637654
}

src/utils/typeConverters.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,12 @@ export namespace Range {
4848
}
4949
return lsp.Range.create(start, end);
5050
}
51+
52+
export function union(one: lsp.Range, other: lsp.Range): lsp.Range {
53+
const start = Position.Min(other.start, one.start);
54+
const end = Position.Max(other.end, one.end);
55+
return lsp.Range.create(start, end);
56+
}
5157
}
5258

5359
export namespace Position {

0 commit comments

Comments
 (0)