@@ -33,9 +33,24 @@ import {
33
33
import { LabIcon } from '@jupyterlab/ui-components' ;
34
34
import { ILSPLogConsole } from '../../tokens' ;
35
35
import { CompletionLabIntegration } from './completion' ;
36
- import { LazyCompletionItem } from './item' ;
36
+ import {
37
+ ICompletionsSource ,
38
+ IExtendedCompletionItem ,
39
+ LazyCompletionItem
40
+ } from './item' ;
37
41
import ICompletionItemsResponseType = CompletionHandler . ICompletionItemsResponseType ;
38
42
43
+ /**
44
+ * Completion items reply from a specific source
45
+ */
46
+ export interface ICompletionsReply
47
+ extends CompletionHandler . ICompletionItemsReply {
48
+ // TODO: it is not clear when the source is set here and when on IExtendedCompletionItem.
49
+ // it might be good to separate the two stages for both interfaces
50
+ source : ICompletionsSource ;
51
+ items : IExtendedCompletionItem [ ] ;
52
+ }
53
+
39
54
/**
40
55
* A LSP connector for completion handlers.
41
56
*/
@@ -255,7 +270,10 @@ export class LSPConnector
255
270
kernel_promise . catch ( p => p ) ,
256
271
lsp_promise . catch ( p => p )
257
272
] ) . then ( ( [ kernel , lsp ] ) =>
258
- this . merge_replies ( this . transform_reply ( kernel ) , lsp , this . _editor )
273
+ this . merge_replies (
274
+ [ this . transform_reply ( kernel ) , lsp ] ,
275
+ this . _editor
276
+ )
259
277
) ;
260
278
}
261
279
}
@@ -295,7 +313,7 @@ export class LSPConnector
295
313
cursor : IVirtualPosition ,
296
314
document : VirtualDocument ,
297
315
position_in_token : number
298
- ) : Promise < CompletionHandler . ICompletionItemsReply > {
316
+ ) : Promise < ICompletionsReply > {
299
317
let connection = this . get_connection ( document . uri ) ;
300
318
301
319
this . console . debug ( 'Fetching' ) ;
@@ -322,7 +340,7 @@ export class LSPConnector
322
340
this . console . debug ( 'Transforming' ) ;
323
341
let prefix = token . value . slice ( 0 , position_in_token + 1 ) ;
324
342
let all_non_prefixed = true ;
325
- let items : CompletionHandler . ICompletionItem [ ] = [ ] ;
343
+ let items : IExtendedCompletionItem [ ] = [ ] ;
326
344
lspCompletionItems . forEach ( match => {
327
345
let kind = match . kind ? CompletionItemKind [ match . kind ] : '' ;
328
346
let completionItem = new LazyCompletionItem (
@@ -374,7 +392,11 @@ export class LSPConnector
374
392
// but it did not work for "from statistics <tab>" and lead to "from statisticsimport" (no space)
375
393
start : token . offset + ( all_non_prefixed ? prefix_offset : 0 ) ,
376
394
end : token . offset + prefix . length ,
377
- items : items
395
+ items : items ,
396
+ source : {
397
+ name : 'LSP' ,
398
+ priority : 2
399
+ }
378
400
} ;
379
401
if ( response . start > response . end ) {
380
402
console . warn (
@@ -396,11 +418,9 @@ export class LSPConnector
396
418
return ( this . options . themeManager . get_icon ( type ) as LabIcon ) || undefined ;
397
419
}
398
420
399
- private transform_reply (
400
- reply : CompletionHandler . IReply
401
- ) : CompletionHandler . ICompletionItemsReply {
421
+ private transform_reply ( reply : CompletionHandler . IReply ) : ICompletionsReply {
402
422
this . console . log ( 'Transforming kernel reply:' , reply ) ;
403
- let items : CompletionHandler . ICompletionItem [ ] ;
423
+ let items : IExtendedCompletionItem [ ] ;
404
424
const metadata = reply . metadata || { } ;
405
425
const types = metadata . _jupyter_types_experimental as JSONArray ;
406
426
@@ -419,73 +439,113 @@ export class LSPConnector
419
439
return {
420
440
label : match ,
421
441
insertText : match ,
422
- icon : this . icon_for ( 'Kernel' ) ,
423
442
sortText : this . kernel_completions_first ? 'a' : 'z'
424
443
} ;
425
444
} ) ;
426
445
}
427
- return { start : reply . start , end : reply . end , items } ;
446
+ return {
447
+ start : reply . start ,
448
+ end : reply . end ,
449
+ source : {
450
+ name : 'Kernel' ,
451
+ priority : 1 ,
452
+ fallbackIcon : this . icon_for ( 'Kernel' )
453
+ } ,
454
+ items
455
+ } ;
428
456
}
429
457
430
- private merge_replies (
431
- kernel : CompletionHandler . ICompletionItemsReply ,
432
- lsp : CompletionHandler . ICompletionItemsReply ,
458
+ protected merge_replies (
459
+ replies : ICompletionsReply [ ] ,
433
460
editor : CodeEditor . IEditor
434
- ) : CompletionHandler . ICompletionItemsReply {
435
- this . console . debug ( 'Merging completions:' , lsp , kernel ) ;
461
+ ) : ICompletionsReply {
462
+ this . console . debug ( 'Merging completions:' , replies ) ;
463
+
464
+ replies = replies . filter ( reply => {
465
+ if ( reply instanceof Error ) {
466
+ this . console . warn (
467
+ `Caught ${ reply . source . name } completions error` ,
468
+ reply
469
+ ) ;
470
+ return false ;
471
+ }
472
+ // ignore if no matches
473
+ if ( ! reply . items . length ) {
474
+ return false ;
475
+ }
476
+ // otherwise keep
477
+ return true ;
478
+ } ) ;
436
479
437
- if ( kernel instanceof Error ) {
438
- this . console . warn ( 'Caught kernel completions error' , kernel ) ;
439
- }
440
- if ( lsp instanceof Error ) {
441
- this . console . warn ( 'Caught LSP completions error' , lsp ) ;
442
- }
480
+ replies . sort ( ( a , b ) => b . source . priority - a . source . priority ) ;
443
481
444
- if ( kernel instanceof Error || ! kernel . items . length ) {
445
- return lsp ;
446
- }
447
- if ( lsp instanceof Error || ! lsp . items . length ) {
448
- return kernel ;
449
- }
482
+ this . console . debug ( 'Sorted replies:' , replies ) ;
450
483
451
- let prefix = '' ;
484
+ const minEnd = Math . min ( ... replies . map ( reply => reply . end ) ) ;
452
485
453
- // if the kernel used a wider range, get the previous characters to strip the prefix off,
454
- // so that both use the same range
455
- if ( lsp . start > kernel . start ) {
486
+ // if any of the replies uses a wider range, we need to align them
487
+ // so that all responses use the same range
488
+ const minStart = Math . min ( ...replies . map ( reply => reply . start ) ) ;
489
+ const maxStart = Math . max ( ...replies . map ( reply => reply . start ) ) ;
490
+
491
+ if ( minStart != maxStart ) {
456
492
const cursor = editor . getCursorPosition ( ) ;
457
493
const line = editor . getLine ( cursor . line ) ;
458
- prefix = line . substring ( kernel . start , lsp . start ) ;
459
- this . console . debug ( 'Removing kernel prefix: ' , prefix ) ;
460
- } else if ( lsp . start < kernel . start ) {
461
- this . console . warn ( 'Kernel start > LSP start' ) ;
462
- }
463
494
464
- // combine completions, de-duping by insertText; LSP completions will show up first, kernel second.
465
- const aggregatedItems = lsp . items . concat (
466
- kernel . items . map ( item => {
495
+ replies = replies . map ( reply => {
496
+ // no prefix to strip, return as-is
497
+ if ( reply . start == maxStart ) {
498
+ return reply ;
499
+ }
500
+ let prefix = line . substring ( reply . start , maxStart ) ;
501
+ this . console . debug ( `Removing ${ reply . source . name } prefix: ` , prefix ) ;
467
502
return {
468
- ...item ,
469
- insertText : item . insertText . startsWith ( prefix )
470
- ? item . insertText . substr ( prefix . length )
471
- : item . insertText
503
+ ...reply ,
504
+ items : reply . items . map ( item => {
505
+ item . insertText = item . insertText . startsWith ( prefix )
506
+ ? item . insertText . substr ( prefix . length )
507
+ : item . insertText ;
508
+ return item ;
509
+ } )
472
510
} ;
473
- } )
474
- ) ;
511
+ } ) ;
512
+ }
513
+
475
514
const insertTextSet = new Set < string > ( ) ;
476
- const processedItems = new Array < CompletionHandler . ICompletionItem > ( ) ;
515
+ const processedItems = new Array < IExtendedCompletionItem > ( ) ;
516
+
517
+ for ( const reply of replies ) {
518
+ reply . items . forEach ( item => {
519
+ // trimming because:
520
+ // IPython returns 'import' and 'import '; while the latter is more useful,
521
+ // user should not see two suggestions with identical labels and nearly-identical
522
+ // behaviour as they could not distinguish the two either way
523
+ let text = item . insertText . trim ( ) ;
524
+ if ( insertTextSet . has ( text ) ) {
525
+ return ;
526
+ }
527
+ insertTextSet . add ( text ) ;
528
+ // extra processing (adding icon/source name) is delayed until
529
+ // we are sure that the item will be kept (as otherwise it could
530
+ // lead to processing hundreds of suggestions - e.g. from numpy
531
+ // multiple times if multiple sources provide them).
532
+ let processedItem = item as IExtendedCompletionItem ;
533
+ processedItem . source = reply . source ;
534
+ if ( ! processedItem . icon ) {
535
+ processedItem . icon = reply . source . fallbackIcon ;
536
+ }
537
+ processedItems . push ( processedItem ) ;
538
+ } ) ;
539
+ }
477
540
478
- aggregatedItems . forEach ( item => {
479
- if ( insertTextSet . has ( item . insertText ) ) {
480
- return ;
481
- }
482
- insertTextSet . add ( item . insertText ) ;
483
- processedItems . push ( item ) ;
484
- } ) ;
485
- // TODO: Sort items
486
541
// Return reply with processed items.
487
- this . console . debug ( 'Merged: ' , { ...lsp , items : processedItems } ) ;
488
- return { ...lsp , items : processedItems } ;
542
+ this . console . debug ( 'Merged: ' , processedItems ) ;
543
+ return {
544
+ start : maxStart ,
545
+ end : minEnd ,
546
+ source : null ,
547
+ items : processedItems
548
+ } ;
489
549
}
490
550
491
551
list (
0 commit comments