Skip to content

Commit 8c9ca6f

Browse files
committed
Merge branch 'completion-items' of ssh://github.com/kiteco/jupyterlab-lsp into kiteco-completion-items
2 parents 913689d + dddad10 commit 8c9ca6f

File tree

1 file changed

+165
-121
lines changed
  • packages/jupyterlab-lsp/src/adapters/jupyterlab/components

1 file changed

+165
-121
lines changed

packages/jupyterlab-lsp/src/adapters/jupyterlab/components/completion.ts

Lines changed: 165 additions & 121 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import {
66
CompletionConnector
77
} from '@jupyterlab/completer';
88
import { CodeEditor } from '@jupyterlab/codeeditor';
9-
import { ReadonlyJSONObject } from '@lumino/coreutils';
9+
import { JSONArray, JSONObject } from '@lumino/coreutils';
1010
import { completionItemKindNames, CompletionTriggerKind } from '../../../lsp';
1111
import * as lsProtocol from 'vscode-languageserver-protocol';
1212
import { PositionConverter } from '../../../converter';
@@ -156,7 +156,7 @@ export class LSPConnector extends DataConnector<
156156
if (document.language === kernelLanguage) {
157157
return Promise.all([
158158
this._kernel_connector.fetch(request),
159-
this.hint(
159+
this.fetch_lsp(
160160
token,
161161
typed_character,
162162
virtual_start,
@@ -166,12 +166,12 @@ export class LSPConnector extends DataConnector<
166166
position_in_token
167167
)
168168
]).then(([kernel, lsp]) =>
169-
this.merge_replies(kernel, lsp, this._editor)
169+
this.merge_replies(this.transform_reply(kernel), lsp, this._editor)
170170
);
171171
}
172172
}
173173

174-
return this.hint(
174+
return this.fetch_lsp(
175175
token,
176176
typed_character,
177177
virtual_start,
@@ -189,7 +189,7 @@ export class LSPConnector extends DataConnector<
189189
}
190190
}
191191

192-
async hint(
192+
async fetch_lsp(
193193
token: CodeEditor.IToken,
194194
typed_character: string,
195195
start: IVirtualPosition,
@@ -200,15 +200,10 @@ export class LSPConnector extends DataConnector<
200200
): Promise<CompletionHandler.IReply> {
201201
let connection = this._connections.get(document.id_path);
202202

203-
// nope - do not do this; we need to get the signature (yes)
204-
// but only in order to bump the priority of the parameters!
205-
// unfortunately there is no abstraction of scores exposed
206-
// to the matches...
207-
// Suggested in https://github.com/jupyterlab/jupyterlab/issues/7044, TODO PR
208-
203+
console.log('[LSP][Completer] Fetching and Transforming');
209204
console.log('[LSP][Completer] Token:', token);
210205

211-
let completion_items = ((await connection.getCompletion(
206+
let lspCompletionItems = ((await connection.getCompletion(
212207
cursor,
213208
{
214209
start,
@@ -222,22 +217,23 @@ export class LSPConnector extends DataConnector<
222217
)) || []) as lsProtocol.CompletionItem[];
223218

224219
let prefix = token.value.slice(0, position_in_token + 1);
225-
226-
let matches: Array<string> = [];
227-
const types: Array<IItemType> = [];
228220
let all_non_prefixed = true;
229-
for (let match of completion_items) {
230-
// there are more interesting things to be extracted and passed to the metadata:
231-
// detail: "__main__"
232-
// documentation: "mean(data)↵↵Return the sample arithmetic mean of data.↵↵>>> mean([1, 2, 3, 4, 4])↵2.8↵↵>>> from fractions import Fraction as F↵>>> mean([F(3, 7), F(1, 21), F(5, 3), F(1, 3)])↵Fraction(13, 21)↵↵>>> from decimal import Decimal as D↵>>> mean([D("0.5"), D("0.75"), D("0.625"), D("0.375")])↵Decimal('0.5625')↵↵If ``data`` is empty, StatisticsError will be raised."
233-
// insertText: "mean"
234-
// kind: 3
235-
// label: "mean(data)"
236-
// sortText: "amean"
237-
238-
// TODO: add support for match.textEdit
221+
let items: CompletionHandler.ICompletionItem[] = [];
222+
lspCompletionItems.forEach(match => {
223+
let completionItem = {
224+
label: match.label,
225+
insertText: match.insertText,
226+
type: match.kind ? completionItemKindNames[match.kind] : '',
227+
documentation: lsProtocol.MarkupContent.is(match.documentation)
228+
? match.documentation.value
229+
: match.documentation,
230+
filterText: match.filterText,
231+
deprecated: match.deprecated,
232+
data: { ...match }
233+
};
234+
235+
// Update prefix values
239236
let text = match.insertText ? match.insertText : match.label;
240-
241237
if (text.toLowerCase().startsWith(prefix.toLowerCase())) {
242238
all_non_prefixed = false;
243239
if (prefix !== token.value) {
@@ -252,12 +248,8 @@ export class LSPConnector extends DataConnector<
252248
}
253249
}
254250

255-
matches.push(text);
256-
types.push({
257-
text: text,
258-
type: match.kind ? completionItemKindNames[match.kind] : ''
259-
});
260-
}
251+
items.push(completionItem);
252+
});
261253

262254
return {
263255
// note in the ContextCompleter it was:
@@ -271,102 +263,161 @@ export class LSPConnector extends DataConnector<
271263
// but it did not work for "from statistics <tab>" and lead to "from statisticsimport" (no space)
272264
start: token.offset + (all_non_prefixed ? 1 : 0),
273265
end: token.offset + prefix.length,
274-
matches: matches,
275-
metadata: {
276-
_jupyter_types_experimental: types
277-
}
266+
matches: [],
267+
metadata: {},
268+
items
278269
};
279270
}
280271

272+
private transform_reply(
273+
reply: CompletionHandler.IReply
274+
): CompletionHandler.IReply {
275+
console.log('[LSP][Completer] Transforming kernel reply:', reply);
276+
const items = new Array<CompletionHandler.ICompletionItem>();
277+
const metadata = reply.metadata || {};
278+
const types = metadata._jupyter_types_experimental as JSONArray;
279+
280+
if (types) {
281+
types.forEach((item: JSONObject) => {
282+
// For some reason the _jupyter_types_experimental list has two entries
283+
// for each match, with one having a type of "<unknown>". Discard those
284+
// and use undefined to indicate an unknown type.
285+
const text = item.text as string;
286+
const type = item.type as string;
287+
items.push({ label: text, type });
288+
});
289+
} else {
290+
const matches = reply.matches;
291+
matches.forEach(match => {
292+
items.push({ label: match });
293+
});
294+
}
295+
return { ...reply, items };
296+
}
297+
281298
private merge_replies(
282299
kernel: CompletionHandler.IReply,
283300
lsp: CompletionHandler.IReply,
284301
editor: CodeEditor.IEditor
285-
) {
286-
// This is based on https://github.com/jupyterlab/jupyterlab/blob/f1bc02ced61881df94c49929837c49c022f5b115/packages/completer/src/connector.ts#L78
287-
// Copyright (c) Jupyter Development Team.
288-
// Distributed under the terms of the Modified BSD License.
289-
290-
// If one is empty, return the other.
291-
if (kernel.matches.length === 0) {
292-
return lsp;
293-
} else if (lsp.matches.length === 0) {
294-
return kernel;
295-
}
302+
): CompletionHandler.IReply {
296303
console.log('[LSP][Completer] Merging completions:', lsp, kernel);
297-
298-
// Populate the result with a copy of the lsp matches.
299-
const matches = lsp.matches.slice();
300-
const types = lsp.metadata._jupyter_types_experimental as Array<IItemType>;
301-
302-
// Cache all the lsp matches in a memo.
303-
const memo = new Set<string>(matches);
304-
const memo_types = new Map<string, string>(
305-
types.map(v => [v.text, v.type])
306-
);
307-
308-
let prefix = '';
309-
310-
// if the kernel used a wider range, get the previous characters to strip the prefix off,
311-
// so that both use the same range
312-
if (lsp.start > kernel.start) {
313-
const cursor = editor.getCursorPosition();
314-
const line = editor.getLine(cursor.line);
315-
prefix = line.substring(kernel.start, lsp.start);
316-
console.log('[LSP][Completer] Removing kernel prefix: ', prefix);
317-
} else if (lsp.start < kernel.start) {
318-
console.warn('[LSP][Completer] Kernel start > LSP start');
304+
if (!kernel.items.length) {
305+
return lsp;
319306
}
320-
321-
let remove_prefix = (value: string) => {
322-
if (value.startsWith(prefix)) {
323-
return value.substr(prefix.length);
324-
}
325-
return value;
326-
};
327-
328-
// TODO push the CompletionItem suggestion with proper sorting, this is a mess
329-
let priority_matches = new Set<string>();
330-
331-
if (kernel.metadata._jupyter_types_experimental == null) {
332-
let kernel_types = kernel.metadata._jupyter_types_experimental as Array<
333-
IItemType
334-
>;
335-
kernel_types.forEach(itemType => {
336-
let text = remove_prefix(itemType.text);
337-
if (!memo_types.has(text)) {
338-
memo_types.set(text, itemType.type);
339-
if (itemType.type !== '<unknown>') {
340-
priority_matches.add(text);
341-
}
342-
}
343-
});
307+
if (!lsp.items.length) {
308+
return kernel;
344309
}
345-
346-
// Add each context match that is not in the memo to the result.
347-
kernel.matches.forEach(match => {
348-
match = remove_prefix(match);
349-
if (!memo.has(match) && !priority_matches.has(match)) {
350-
matches.push(match);
310+
// Combine ICompletionItems across multiple IReply objects
311+
const aggregatedItems = lsp.items.concat(kernel.items);
312+
// De-dupe and filter items
313+
const labelSet = new Set<String>();
314+
const processedItems = new Array<CompletionHandler.ICompletionItem>();
315+
// TODO: Integrate prefix stripping?
316+
aggregatedItems.forEach(item => {
317+
if (
318+
labelSet.has(item.label) ||
319+
(item.type && item.type === '<unknown>')
320+
) {
321+
return;
351322
}
323+
labelSet.add(item.label);
324+
processedItems.push(item);
352325
});
353-
354-
let final_matches: Array<string> = Array.from(priority_matches).concat(
355-
matches
356-
);
357-
let merged_types: Array<IItemType> = Array.from(
358-
memo_types.entries()
359-
).map(([key, value]) => ({ text: key, type: value }));
360-
361-
return {
362-
...lsp,
363-
matches: final_matches,
364-
metadata: {
365-
_jupyter_types_experimental: merged_types
366-
}
367-
};
326+
// TODO: Sort items
327+
// Return reply with processed items.
328+
return { ...lsp, items: processedItems };
368329
}
369330

331+
// TODO: Remove this
332+
// private merge_replies_old(
333+
// kernel: CompletionHandler.IReply,
334+
// lsp: CompletionHandler.IReply,
335+
// editor: CodeEditor.IEditor
336+
// ) {
337+
// // This is based on https://github.com/jupyterlab/jupyterlab/blob/f1bc02ced61881df94c49929837c49c022f5b115/packages/completer/src/connector.ts#L78
338+
// // Copyright (c) Jupyter Development Team.
339+
// // Distributed under the terms of the Modified BSD License.
340+
341+
// // If one is empty, return the other.
342+
// if (kernel.matches.length === 0) {
343+
// return lsp;
344+
// } else if (lsp.matches.length === 0) {
345+
// return kernel;
346+
// }
347+
// console.log('[LSP][Completer] Merging completions:', lsp, kernel);
348+
349+
// // Populate the result with a copy of the lsp matches.
350+
// const matches = lsp.matches.slice();
351+
// const types = lsp.metadata._jupyter_types_experimental as Array<IItemType>;
352+
353+
// // Cache all the lsp matches in a memo.
354+
// const memo = new Set<string>(matches);
355+
// const memo_types = new Map<string, string>(
356+
// types.map(v => [v.text, v.type])
357+
// );
358+
359+
// let prefix = '';
360+
361+
// // if the kernel used a wider range, get the previous characters to strip the prefix off,
362+
// // so that both use the same range
363+
// if (lsp.start > kernel.start) {
364+
// const cursor = editor.getCursorPosition();
365+
// const line = editor.getLine(cursor.line);
366+
// prefix = line.substring(kernel.start, lsp.start);
367+
// console.log('[LSP][Completer] Removing kernel prefix: ', prefix);
368+
// } else if (lsp.start < kernel.start) {
369+
// console.warn('[LSP][Completer] Kernel start > LSP start');
370+
// }
371+
372+
// let remove_prefix = (value: string) => {
373+
// if (value.startsWith(prefix)) {
374+
// return value.substr(prefix.length);
375+
// }
376+
// return value;
377+
// };
378+
379+
// // TODO push the CompletionItem suggestion with proper sorting, this is a mess
380+
// let priority_matches = new Set<string>();
381+
382+
// if (kernel.metadata._jupyter_types_experimental == null) {
383+
// let kernel_types = kernel.metadata._jupyter_types_experimental as Array<
384+
// IItemType
385+
// >;
386+
// kernel_types.forEach(itemType => {
387+
// let text = remove_prefix(itemType.text);
388+
// if (!memo_types.has(text)) {
389+
// memo_types.set(text, itemType.type);
390+
// if (itemType.type !== '<unknown>') {
391+
// priority_matches.add(text);
392+
// }
393+
// }
394+
// });
395+
// }
396+
397+
// // Add each context match that is not in the memo to the result.
398+
// kernel.matches.forEach(match => {
399+
// match = remove_prefix(match);
400+
// if (!memo.has(match) && !priority_matches.has(match)) {
401+
// matches.push(match);
402+
// }
403+
// });
404+
405+
// let final_matches: Array<string> = Array.from(priority_matches).concat(
406+
// matches
407+
// );
408+
// let merged_types: Array<IItemType> = Array.from(
409+
// memo_types.entries()
410+
// ).map(([key, value]) => ({ text: key, type: value }));
411+
412+
// return {
413+
// ...lsp,
414+
// matches: final_matches,
415+
// metadata: {
416+
// _jupyter_types_experimental: merged_types
417+
// }
418+
// };
419+
// }
420+
370421
with_trigger_kind(kind: CompletionTriggerKind, fn: Function) {
371422
try {
372423
this.trigger_kind = kind;
@@ -399,10 +450,3 @@ export namespace LSPConnector {
399450
session?: Session.ISessionConnection;
400451
}
401452
}
402-
403-
interface IItemType extends ReadonlyJSONObject {
404-
// the item value
405-
text: string;
406-
// the item type
407-
type: string;
408-
}

0 commit comments

Comments
 (0)