Skip to content

Commit b693d56

Browse files
committed
Fix path completions and eliding in path rendering
1 parent 6eacd10 commit b693d56

File tree

3 files changed

+48
-16
lines changed

3 files changed

+48
-16
lines changed

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

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -62,19 +62,24 @@ export class GenericCompleterModel<
6262
setCompletionItems(newValue: T[]) {
6363
super.setCompletionItems(newValue);
6464

65-
if (this.settings.preFilterMatches && this.current && this.cursor) {
65+
if (this.current && this.cursor) {
6666
// set initial query to pre-filter items; in future we should use:
6767
// https://github.com/jupyterlab/jupyterlab/issues/9763#issuecomment-1001603348
6868
const { start, end } = this.cursor;
6969
let query = this.current.text.substring(start, end).trim();
70+
let trimmedQuotes = false;
7071
// special case for "Completes Paths In Strings" test case
7172
if (query.startsWith('"') || query.startsWith("'")) {
7273
query = query.substring(1);
74+
trimmedQuotes = true;
7375
}
7476
if (query.endsWith('"') || query.endsWith("'")) {
7577
query = query.substring(0, -1);
78+
trimmedQuotes = true;
79+
}
80+
if (this.settings.preFilterMatches || trimmedQuotes) {
81+
this.query = query;
7682
}
77-
this.query = query;
7883
}
7984
}
8085

@@ -99,6 +104,20 @@ export class GenericCompleterModel<
99104
return index > -1 ? item.label.substring(0, index) : item.label;
100105
}
101106

107+
createPatch(patch: string) {
108+
if (this.subsetMatch) {
109+
// Prevent insertion code path when auto-populating subset on tab, to avoid problems with
110+
// prefix which is a subset of token incorrectly replacing a string with file system path.
111+
// - Q: Which code path is being blocked?
112+
// A: The code path (b) discussed in https://github.com/jupyterlab/jupyterlab/issues/15130.
113+
// - Q: Why are we short- circuiting here?
114+
// A: we want to prevent `onCompletionSelected()` from proceeding with text insertion,
115+
// but direct extension of Completer handler is difficult.
116+
return undefined;
117+
}
118+
return super.createPatch(patch);
119+
}
120+
102121
private _sortAndFilter(query: string, items: T[]): T[] {
103122
let results: ICompletionMatch<T>[] = [];
104123

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

Lines changed: 23 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -307,6 +307,13 @@ export class CompletionProvider implements ICompletionProvider<CompletionItem> {
307307
return true;
308308
}
309309
}
310+
311+
function stripQuotes(path: string): string {
312+
return path.slice(
313+
path.startsWith("'") || path.startsWith('"') ? 1 : 0,
314+
path.endsWith("'") || path.endsWith('"') ? -1 : path.length
315+
);
316+
}
310317
export function transformLSPCompletions<T>(
311318
token: CodeEditor.IToken,
312319
positionInToken: number,
@@ -317,6 +324,7 @@ export function transformLSPCompletions<T>(
317324
let prefix = token.value.slice(0, positionInToken + 1);
318325
let allNonPrefixed = true;
319326
let items: T[] = [];
327+
let prefixLength = prefix.length;
320328
lspCompletionItems.forEach(match => {
321329
let kind = match.kind ? CompletionItemKind[match.kind] : '';
322330

@@ -338,22 +346,27 @@ export function transformLSPCompletions<T>(
338346
}
339347
}
340348
// add prefix if needed
341-
else if (token.type === 'string' && prefix.includes('/')) {
349+
else if (token.type === 'String' && prefix.includes('/')) {
342350
// special case for path completion in strings, ensuring that:
343351
// '/Com<tab> → '/Completion.ipynb
344352
// when the returned insert text is `Completion.ipynb` (the token here is `'/Com`)
345353
// developed against pyls and pylsp server, may not work well in other cases
346-
const parts = prefix.split('/');
354+
355+
const parts = stripQuotes(prefix).split('/');
347356
if (
348357
text.toLowerCase().startsWith(parts[parts.length - 1].toLowerCase())
349358
) {
350359
let pathPrefix = parts.slice(0, -1).join('/') + '/';
351-
match.insertText = pathPrefix + text;
352-
// for label removing the prefix quote if present
353-
if (pathPrefix.startsWith("'") || pathPrefix.startsWith('"')) {
354-
pathPrefix = pathPrefix.substr(1);
355-
}
360+
const withStartingQuote =
361+
(prefix.startsWith("'") || prefix.startsWith('"') ? prefix[0] : '') +
362+
pathPrefix +
363+
text;
364+
match.insertText = withStartingQuote;
365+
// for label without quotes
356366
match.label = pathPrefix + match.label;
367+
if (prefix.endsWith("'") || prefix.endsWith('"')) {
368+
prefixLength = prefix.length - 1;
369+
}
357370
allNonPrefixed = false;
358371
}
359372
}
@@ -369,8 +382,8 @@ export function transformLSPCompletions<T>(
369382
// completion of dictionaries for Python with jedi-language-server was
370383
// causing an issue for dic['<tab>'] case; to avoid this let's make
371384
// sure that prefix.length >= prefix.offset
372-
if (allNonPrefixed && prefixOffset > prefix.length) {
373-
prefixOffset = prefix.length;
385+
if (allNonPrefixed && prefixOffset > prefixLength) {
386+
prefixOffset = prefixLength;
374387
}
375388

376389
let response = {
@@ -384,7 +397,7 @@ export function transformLSPCompletions<T>(
384397
// text = token.value + text;
385398
// but it did not work for "from statistics <tab>" and lead to "from statisticsimport" (no space)
386399
start: token.offset + (allNonPrefixed ? prefixOffset : 0),
387-
end: token.offset + prefix.length,
400+
end: token.offset + prefixLength,
388401
items: items,
389402
source: 'LSP'
390403
};

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

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -161,14 +161,14 @@ export class LSPCompletionRenderer
161161
const originalHTMLLabel = labelElement.childNodes;
162162
let hasMark = false;
163163
for (const node of originalHTMLLabel) {
164-
if (node.nodeType === Node.ELEMENT_NODE) {
165-
const element = node as Element;
164+
if (node instanceof HTMLElement) {
165+
const element = node as HTMLElement;
166166
const text = element.textContent;
167-
if (element.tagName === 'MARK' && text) {
167+
if (element.tagName === 'MARK' && text && text.length > 3) {
168168
const elidableElement = document.createElement('bdo');
169169
elidableElement.setAttribute('dir', 'ltr');
170170
elidableElement.textContent = text;
171-
elidableElement.title = text;
171+
element.title = text;
172172
element.replaceChildren(elidableElement);
173173
element.classList.add('lsp-elide');
174174
hasMark = true;

0 commit comments

Comments
 (0)