Skip to content

Commit ea1b220

Browse files
authored
Do not pre-filter completions from kernel (#1022)
* Address jest deprecation warning * Only pre-filter LSP completions, not the kernel completions * Apply suggestions from code review Make newly added `resolveQuery` protected
1 parent f379b6d commit ea1b220

File tree

3 files changed

+129
-53
lines changed

3 files changed

+129
-53
lines changed

packages/jupyterlab-lsp/jest.config.js

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,12 +23,11 @@ const esModules = [
2323
].join('|');
2424

2525
let local = {
26-
globals: { 'ts-jest': { tsconfig: 'tsconfig.json' } },
2726
testRegex: `.*\.spec\.tsx?$`,
2827
transformIgnorePatterns: [`/node_modules/(?!${esModules}).*`],
2928
testLocationInResults: true,
3029
transform: {
31-
'\\.(ts|tsx)?$': 'ts-jest',
30+
'\\.(ts|tsx)?$': ['ts-jest', { tsconfig: 'tsconfig.json' }],
3231
'\\.(js|jsx)?$': './transform.js',
3332
'\\.svg$': '@jupyterlab/testing/lib/jest-raw-loader.js'
3433
},

packages/jupyterlab-lsp/src/features/completion/model.spec.ts

Lines changed: 46 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ describe('LSPCompleterModel', () => {
99
function createDummyItem(
1010
match: lsProtocol.CompletionItem,
1111
type: string = 'dummy',
12-
source: string = 'lsp'
12+
source: string = 'LSP'
1313
) {
1414
return new CompletionItem({
1515
type,
@@ -80,6 +80,51 @@ describe('LSPCompleterModel', () => {
8080
expect(sortedItems.map(item => item.sortText)).toEqual(['a', 'b', 'c']);
8181
});
8282

83+
describe('pre-filtering', () => {
84+
beforeEach(() => {
85+
// order of cursor/current matters
86+
model.current = model.original = {
87+
text: 'a',
88+
line: 0,
89+
column: 1
90+
};
91+
model.cursor = { start: 0, end: 1 };
92+
});
93+
94+
const prefixA = createDummyItem({
95+
label: 'a'
96+
});
97+
const prefixB = createDummyItem({
98+
label: 'b'
99+
});
100+
const prefixBButNotLSP = createDummyItem(
101+
{
102+
label: 'b'
103+
},
104+
'dummy',
105+
'not LSP'
106+
);
107+
108+
it('filters out non-matching LSP completions', () => {
109+
model.setCompletionItems([prefixA, prefixB]);
110+
let items = model.completionItems();
111+
expect(items.map(item => item.insertText)).toEqual(['a']);
112+
});
113+
114+
it('does not filter out non LSP completions', () => {
115+
model.setCompletionItems([prefixA, prefixBButNotLSP]);
116+
let items = model.completionItems();
117+
expect(items.map(item => item.insertText)).toEqual(['a', 'b']);
118+
});
119+
120+
it('does not filter out when turned off', () => {
121+
model.setCompletionItems([prefixA, prefixB]);
122+
model.settings.preFilterMatches = false;
123+
let items = model.completionItems();
124+
expect(items.map(item => item.insertText)).toEqual(['a']);
125+
});
126+
});
127+
83128
describe('sorting by source', () => {
84129
const testCompletionA = createDummyItem(
85130
{

packages/jupyterlab-lsp/src/features/completion/model.ts

Lines changed: 82 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -59,50 +59,6 @@ export class GenericCompleterModel<
5959
return item as T;
6060
}
6161

62-
setCompletionItems(newValue: T[]) {
63-
super.setCompletionItems(newValue);
64-
65-
if (this.current && this.cursor) {
66-
// set initial query to pre-filter items; in future we should use:
67-
// https://github.com/jupyterlab/jupyterlab/issues/9763#issuecomment-1001603348
68-
69-
// note: start/end from cursor are not ideal because these get populated from fetch
70-
// reply which will vary depending on what providers decide to return; we want the
71-
// actual position in token, the same as passed in request to fetch. We can get it
72-
// by searching for longest common prefix as seen below (or by counting characters).
73-
// Maybe upstream should expose it directly?
74-
const { start, end } = this.cursor;
75-
const { text, line, column } = this.original!;
76-
77-
const queryRange = text.substring(start, end).trim();
78-
const linePrefix = text.split('\n')[line].substring(0, column).trim();
79-
let query = '';
80-
for (let i = queryRange.length; i > 0; i--) {
81-
if (queryRange.slice(0, i) == linePrefix.slice(-i)) {
82-
query = linePrefix.slice(-i);
83-
break;
84-
}
85-
}
86-
if (!query) {
87-
return;
88-
}
89-
90-
let trimmedQuotes = false;
91-
// special case for "Completes Paths In Strings" test case
92-
if (query.startsWith('"') || query.startsWith("'")) {
93-
query = query.substring(1);
94-
trimmedQuotes = true;
95-
}
96-
if (query.endsWith('"') || query.endsWith("'")) {
97-
query = query.substring(0, -1);
98-
trimmedQuotes = true;
99-
}
100-
if (this.settings.preFilterMatches || trimmedQuotes) {
101-
this.query = query;
102-
}
103-
}
104-
}
105-
10662
private _markFragment(value: string): string {
10763
return `<mark>${value}</mark>`;
10864
}
@@ -138,7 +94,11 @@ export class GenericCompleterModel<
13894
return super.createPatch(patch);
13995
}
14096

141-
private _sortAndFilter(query: string, items: T[]): T[] {
97+
protected resolveQuery(userQuery: string, _item: T) {
98+
return userQuery;
99+
}
100+
101+
private _sortAndFilter(userQuery: string, items: T[]): T[] {
142102
let results: ICompletionMatch<T>[] = [];
143103

144104
for (let item of items) {
@@ -149,6 +109,8 @@ export class GenericCompleterModel<
149109
let filterText: string | null = null;
150110
let filterMatch: StringExt.IMatchResult | null = null;
151111

112+
const query = this.resolveQuery(userQuery, item);
113+
152114
let lowerCaseQuery = query.toLowerCase();
153115

154116
if (query) {
@@ -246,10 +208,6 @@ export namespace GenericCompleterModel {
246208
* Whether perfect matches should be included (default = true)
247209
*/
248210
includePerfectMatches?: boolean;
249-
/**
250-
* Whether matches should be pre-filtered (default = true)
251-
*/
252-
preFilterMatches?: boolean;
253211
/**
254212
* Whether kernel completions should be shown first.
255213
*/
@@ -258,7 +216,6 @@ export namespace GenericCompleterModel {
258216
export const defaultOptions: IOptions = {
259217
caseSensitive: true,
260218
includePerfectMatches: true,
261-
preFilterMatches: true,
262219
kernelCompletionsFirst: false
263220
};
264221
}
@@ -267,13 +224,73 @@ type MaybeCompletionItem = Partial<CompletionItem> &
267224
CompletionHandler.ICompletionItem;
268225

269226
export class LSPCompleterModel extends GenericCompleterModel<MaybeCompletionItem> {
227+
public settings: LSPCompleterModel.IOptions;
228+
229+
constructor(settings: LSPCompleterModel.IOptions = {}) {
230+
super();
231+
this.settings = { ...LSPCompleterModel.defaultOptions, ...settings };
232+
}
233+
270234
protected getFilterText(item: MaybeCompletionItem): string {
271235
if (item.filterText) {
272236
return item.filterText;
273237
}
274238
return super.getFilterText(item);
275239
}
276240

241+
setCompletionItems(newValue: MaybeCompletionItem[]) {
242+
super.setCompletionItems(newValue);
243+
this._preFilterQuery = '';
244+
245+
if (this.current && this.cursor) {
246+
// set initial query to pre-filter items; in future we should use:
247+
// https://github.com/jupyterlab/jupyterlab/issues/9763#issuecomment-1001603348
248+
249+
// note: start/end from cursor are not ideal because these get populated from fetch
250+
// reply which will vary depending on what providers decide to return; we want the
251+
// actual position in token, the same as passed in request to fetch. We can get it
252+
// by searching for longest common prefix as seen below (or by counting characters).
253+
// Maybe upstream should expose it directly?
254+
const { start, end } = this.cursor;
255+
const { text, line, column } = this.original!;
256+
257+
const queryRange = text.substring(start, end).trim();
258+
const linePrefix = text.split('\n')[line].substring(0, column).trim();
259+
let query = '';
260+
for (let i = queryRange.length; i > 0; i--) {
261+
if (queryRange.slice(0, i) == linePrefix.slice(-i)) {
262+
query = linePrefix.slice(-i);
263+
break;
264+
}
265+
}
266+
if (!query) {
267+
return;
268+
}
269+
270+
let trimmedQuotes = false;
271+
// special case for "Completes Paths In Strings" test case
272+
if (query.startsWith('"') || query.startsWith("'")) {
273+
query = query.substring(1);
274+
trimmedQuotes = true;
275+
}
276+
if (query.endsWith('"') || query.endsWith("'")) {
277+
query = query.substring(0, -1);
278+
trimmedQuotes = true;
279+
}
280+
if (this.settings.preFilterMatches || trimmedQuotes) {
281+
this._preFilterQuery = query;
282+
}
283+
}
284+
}
285+
286+
protected resolveQuery(userQuery: string, item: MaybeCompletionItem) {
287+
return userQuery
288+
? userQuery
289+
: item.source === 'LSP'
290+
? this._preFilterQuery
291+
: '';
292+
}
293+
277294
protected harmoniseItem(item: CompletionHandler.ICompletionItem) {
278295
if ((item as any).self) {
279296
const self = (item as any).self;
@@ -306,4 +323,19 @@ export class LSPCompleterModel extends GenericCompleterModel<MaybeCompletionItem
306323
a.score - b.score
307324
);
308325
}
326+
327+
private _preFilterQuery: string = '';
328+
}
329+
330+
export namespace LSPCompleterModel {
331+
export interface IOptions extends GenericCompleterModel.IOptions {
332+
/**
333+
* Whether matches should be pre-filtered (default = true)
334+
*/
335+
preFilterMatches?: boolean;
336+
}
337+
export const defaultOptions: IOptions = {
338+
...GenericCompleterModel.defaultOptions,
339+
preFilterMatches: true
340+
};
309341
}

0 commit comments

Comments
 (0)