@@ -13,7 +13,9 @@ import {
13
13
Event ,
14
14
EventEmitter ,
15
15
Hover ,
16
+ MarkdownString ,
16
17
SignatureHelpContext ,
18
+ SignatureInformation ,
17
19
TextDocumentContentChangeEvent ,
18
20
Uri
19
21
} from 'vscode' ;
@@ -64,6 +66,24 @@ import {
64
66
} from './conversion' ;
65
67
import { IntellisenseDocument } from './intellisenseDocument' ;
66
68
69
+ // These regexes are used to get the text from jupyter output by recognizing escape charactor \x1b
70
+ const DocStringRegex = / \x1b \[ 1 ; 3 1 m D o c s t r i n g : \x1b \[ 0 m \s + ( [ \s \S ] * ?) \r ? \n \x1b \[ 1 ; 3 1 m / ;
71
+ const SignatureTextRegex = / \x1b \[ 1 ; 3 1 m S i g n a t u r e : \x1b \[ 0 m \s + ( [ \s \S ] * ?) \r ? \n \x1b \[ 1 ; 3 1 m / ;
72
+ const TypeRegex = / \x1b \[ 1 ; 3 1 m T y p e : \x1b \[ 0 m \s + ( .* ) / ;
73
+
74
+ // This regex is to parse the name and the signature in the signature text from Jupyter,
75
+ // Example string: some_func(param1=1, param2=2) -> int
76
+ // match[1]: some_func
77
+ // match[2]: (param1=1, param2=2) -> int
78
+ const SignatureRegex = / ( .+ ?) ( \( ( [ \s \S ] * ) \) ( \s * - > [ \s \S ] * ) ? ) / ;
79
+ const GeneralCallableSignature = '(*args, **kwargs)' ;
80
+ // This regex is to detect whether a markdown provided by the language server is a callable and get its signature.
81
+ // Example string: ```python\n(function) some_func: (*args, **kwargs) -> None\n```
82
+ // match[1]: (*args, **kwargs)
83
+ // If the string is not a callable, no match will be found.
84
+ // Example string: ```python\n(variable) some_var: Any\n```
85
+ const CallableRegex = / p y t h o n \n \( .+ ?\) \S + ?: ( \( [ \s \S ] + ?\) ) / ;
86
+
67
87
// tslint:disable:no-any
68
88
@injectable ( )
69
89
export class IntellisenseProvider implements IInteractiveWindowListener {
@@ -253,6 +273,7 @@ export class IntellisenseProvider implements IInteractiveWindowListener {
253
273
incomplete : false
254
274
} ;
255
275
}
276
+
256
277
protected async provideHover (
257
278
position : monacoEditor . Position ,
258
279
wordAtPosition : string | undefined ,
@@ -266,9 +287,27 @@ export class IntellisenseProvider implements IInteractiveWindowListener {
266
287
] ) ;
267
288
if ( ! variableHover && languageServer && document ) {
268
289
const docPos = document . convertToDocumentPosition ( cellId , position . lineNumber , position . column ) ;
269
- const result = await languageServer . provideHover ( document , docPos , token ) ;
270
- if ( result ) {
271
- return convertToMonacoHover ( result ) ;
290
+ const [ lsResult , jupyterResult ] = await Promise . all ( [
291
+ languageServer . provideHover ( document , docPos , token ) ,
292
+ Promise . race ( [
293
+ this . provideJupyterHover ( position , cellId , token ) ,
294
+ sleep ( Settings . IntellisenseTimeout ) . then ( ( ) => undefined )
295
+ ] )
296
+ ] ) ;
297
+ const jupyterHover = jupyterResult ? convertToMonacoHover ( jupyterResult ) : undefined ;
298
+ const lsHover = lsResult ? convertToMonacoHover ( lsResult ) : undefined ;
299
+ // If lsHover is not valid or it is not a callable with hints,
300
+ // while the jupyter hover is a callable with hint,
301
+ // we prefer to use jupyterHover which provides better callable hints from jupyter kernel.
302
+ const preferJupyterHover =
303
+ jupyterHover &&
304
+ jupyterHover . contents [ 0 ] &&
305
+ this . isCallableWithGoodHint ( jupyterHover . contents [ 0 ] . value ) &&
306
+ ( ! lsHover || ! lsHover . contents [ 0 ] || ! this . isCallableWithGoodHint ( lsHover . contents [ 0 ] . value ) ) ;
307
+ if ( preferJupyterHover && jupyterHover ) {
308
+ return jupyterHover ;
309
+ } else if ( lsHover ) {
310
+ return lsHover ;
272
311
}
273
312
} else if ( variableHover ) {
274
313
return convertToMonacoHover ( variableHover ) ;
@@ -278,6 +317,7 @@ export class IntellisenseProvider implements IInteractiveWindowListener {
278
317
contents : [ ]
279
318
} ;
280
319
}
320
+
281
321
protected async provideSignatureHelp (
282
322
position : monacoEditor . Position ,
283
323
context : monacoEditor . languages . SignatureHelpContext ,
@@ -349,6 +389,37 @@ export class IntellisenseProvider implements IInteractiveWindowListener {
349
389
}
350
390
}
351
391
392
+ private isCallableWithGoodHint ( markdown : string ) : boolean {
393
+ // Check whether the markdown is a callable with the hint that is not (*args, **kwargs)
394
+ const match = CallableRegex . exec ( markdown ) ;
395
+ return match !== null && match [ 1 ] !== GeneralCallableSignature ;
396
+ }
397
+
398
+ private convertDocMarkDown ( doc : string ) : string {
399
+ // For the argument definitions (Starts with :param/:type/:return), to make markdown works well, we need to:
400
+ // 1. Add one more line break;
401
+ // 2. Replace '_' with '\_';
402
+ const docLines = doc . splitLines ( { trim : false , removeEmptyEntries : false } ) ;
403
+ return docLines . map ( ( line ) => ( line . startsWith ( ':' ) ? `\n${ line . replace ( / _ / g, '\\_' ) } ` : line ) ) . join ( '\n' ) ;
404
+ }
405
+
406
+ private async provideJupyterHover (
407
+ position : monacoEditor . Position ,
408
+ cellId : string ,
409
+ token : CancellationToken
410
+ ) : Promise < Hover | undefined > {
411
+ // Currently we only get the callable information from jupyter,
412
+ // this aims to handle the case that language server cannot well recognize the dynamically created callables.
413
+ const callable = await this . getJupyterCallableInspectResult ( position , cellId , token ) ;
414
+ if ( callable ) {
415
+ const signatureMarkdown = `\`\`\`python\n(${ callable . type } ) ${ callable . name } : ${ callable . signature } \n\`\`\`` ;
416
+ const docMarkdown = this . convertDocMarkDown ( callable . doc ) ;
417
+ const result = new MarkdownString ( `${ signatureMarkdown } \n\n${ docMarkdown } ` ) ;
418
+ return { contents : [ result ] } ;
419
+ }
420
+ return undefined ;
421
+ }
422
+
352
423
private dispatchMessage < M extends IInteractiveWindowMapping , T extends keyof M > (
353
424
_message : T ,
354
425
payload : any ,
@@ -465,6 +536,92 @@ export class IntellisenseProvider implements IInteractiveWindowListener {
465
536
) ;
466
537
}
467
538
539
+ private convertCallableInspectResult ( text : string ) {
540
+ // This method will parse the inspect result from jupyter and get the following values of a callable:
541
+ // Name, Type (function or method), Signature, Documentation
542
+
543
+ const docMatch = DocStringRegex . exec ( text ) ;
544
+ // Variable type will be used in hover result, it could be function/method
545
+ const typeMatch = TypeRegex . exec ( text ) ;
546
+
547
+ const signatureTextMatch = SignatureTextRegex . exec ( text ) ;
548
+ // The signature text returned by jupyter contains escape sequences, we need to remove them.
549
+ // See https://en.wikipedia.org/wiki/ANSI_escape_code#Escape_sequences
550
+ const signatureText = signatureTextMatch ? signatureTextMatch [ 1 ] . replace ( / \x1b \[ [ ; \d ] + m / g, '' ) : '' ;
551
+ // Use this to get different parts of the signature: 1: Callable name, 2: Callable signature
552
+ const signatureMatch = SignatureRegex . exec ( signatureText ) ;
553
+
554
+ if ( docMatch && typeMatch && signatureMatch ) {
555
+ return {
556
+ name : signatureMatch [ 1 ] ,
557
+ type : typeMatch [ 1 ] ,
558
+ signature : signatureMatch [ 2 ] ,
559
+ doc : docMatch [ 1 ]
560
+ } ;
561
+ }
562
+ return undefined ;
563
+ }
564
+
565
+ private async getJupyterCallableInspectResult (
566
+ position : monacoEditor . Position ,
567
+ cellId : string ,
568
+ cancelToken : CancellationToken
569
+ ) {
570
+ try {
571
+ const [ activeNotebook , document ] = await Promise . all ( [ this . getNotebook ( cancelToken ) , this . getDocument ( ) ] ) ;
572
+ if ( activeNotebook && document ) {
573
+ const data = document . getCellData ( cellId ) ;
574
+ if ( data ) {
575
+ const offsetInCode = this . getOffsetInCode ( data . text , position ) ;
576
+ const jupyterResults = await activeNotebook . inspect ( data . text , offsetInCode , cancelToken ) ;
577
+ if ( jupyterResults && jupyterResults . hasOwnProperty ( 'text/plain' ) ) {
578
+ return this . convertCallableInspectResult ( ( jupyterResults as any ) [ 'text/plain' ] . toString ( ) ) ;
579
+ }
580
+ }
581
+ }
582
+ } catch ( e ) {
583
+ if ( ! ( e instanceof CancellationError ) ) {
584
+ traceWarning ( e ) ;
585
+ }
586
+ }
587
+ return undefined ;
588
+ }
589
+
590
+ private async provideJupyterSignatureHelp (
591
+ position : monacoEditor . Position ,
592
+ cellId : string ,
593
+ cancelToken : CancellationToken
594
+ ) : Promise < monacoEditor . languages . SignatureHelp > {
595
+ const callable = await this . getJupyterCallableInspectResult ( position , cellId , cancelToken ) ;
596
+ let signatures : SignatureInformation [ ] = [ ] ;
597
+ if ( callable ) {
598
+ const signatureInfo : SignatureInformation = {
599
+ label : callable . signature ,
600
+ documentation : callable . doc ,
601
+ parameters : [ ]
602
+ } ;
603
+ signatures = [ signatureInfo ] ;
604
+ }
605
+ return {
606
+ signatures : signatures ,
607
+ activeParameter : 0 ,
608
+ activeSignature : 0
609
+ } ;
610
+ }
611
+
612
+ private getOffsetInCode ( text : string , position : monacoEditor . Position ) {
613
+ const lines = text . splitLines ( { trim : false , removeEmptyEntries : false } ) ;
614
+ return lines . reduce ( ( a : number , c : string , i : number ) => {
615
+ if ( i < position . lineNumber - 1 ) {
616
+ return a + c . length + 1 ;
617
+ } else if ( i === position . lineNumber - 1 ) {
618
+ return a + position . column - 1 ;
619
+ } else {
620
+ return a ;
621
+ }
622
+ } , 0 ) ;
623
+ }
624
+
468
625
private async provideJupyterCompletionItems (
469
626
position : monacoEditor . Position ,
470
627
_context : monacoEditor . languages . CompletionContext ,
@@ -477,17 +634,7 @@ export class IntellisenseProvider implements IInteractiveWindowListener {
477
634
const data = document . getCellData ( cellId ) ;
478
635
479
636
if ( data ) {
480
- const lines = data . text . splitLines ( { trim : false , removeEmptyEntries : false } ) ;
481
- const offsetInCode = lines . reduce ( ( a : number , c : string , i : number ) => {
482
- if ( i < position . lineNumber - 1 ) {
483
- return a + c . length + 1 ;
484
- } else if ( i === position . lineNumber - 1 ) {
485
- return a + position . column - 1 ;
486
- } else {
487
- return a ;
488
- }
489
- } , 0 ) ;
490
-
637
+ const offsetInCode = this . getOffsetInCode ( data . text , position ) ;
491
638
const jupyterResults = await activeNotebook . getCompletion ( data . text , offsetInCode , cancelToken ) ;
492
639
if ( jupyterResults && jupyterResults . matches ) {
493
640
const filteredMatches = this . filterJupyterMatches ( document , jupyterResults , cellId , position ) ;
@@ -585,20 +732,48 @@ export class IntellisenseProvider implements IInteractiveWindowListener {
585
732
private handleSignatureHelpRequest ( request : IProvideSignatureHelpRequest ) {
586
733
const cancelSource = new CancellationTokenSource ( ) ;
587
734
this . cancellationSources . set ( request . requestId , cancelSource ) ;
588
- this . postTimedResponse (
589
- [ this . provideSignatureHelp ( request . position , request . context , request . cellId , cancelSource . token ) ] ,
590
- InteractiveWindowMessages . ProvideSignatureHelpResponse ,
591
- ( s ) => {
592
- if ( s && s [ 0 ] ) {
593
- return { signatureHelp : s [ 0 ] ! , requestId : request . requestId } ;
594
- } else {
595
- return {
596
- signatureHelp : { signatures : [ ] , activeParameter : 0 , activeSignature : 0 } ,
597
- requestId : request . requestId
598
- } ;
599
- }
735
+
736
+ const getSignatureHelp = async ( ) : Promise < monacoEditor . languages . SignatureHelp > => {
737
+ const jupyterSignatureHelp = this . provideJupyterSignatureHelp (
738
+ request . position ,
739
+ request . cellId ,
740
+ cancelSource . token
741
+ ) ;
742
+
743
+ const lsSignatureHelp = this . provideSignatureHelp (
744
+ request . position ,
745
+ request . context ,
746
+ request . cellId ,
747
+ cancelSource . token
748
+ ) ;
749
+
750
+ const defaultHelp = {
751
+ signatures : [ ] ,
752
+ activeParameter : 0 ,
753
+ activeSignature : 0
754
+ } ;
755
+
756
+ const [ lsHelp , jupyterHelp ] = await Promise . all ( [
757
+ lsSignatureHelp ,
758
+ Promise . race ( [ jupyterSignatureHelp , sleep ( Settings . IntellisenseTimeout ) . then ( ( ) => defaultHelp ) ] )
759
+ ] ) ;
760
+ // Only when language server result is not valid or the signature is (*args, **kwargs) , we prefer to use the result from jupyter.
761
+ const preferJupyterHelp =
762
+ ( ! lsHelp . signatures [ 0 ] || lsHelp . signatures [ 0 ] . label . startsWith ( GeneralCallableSignature ) ) &&
763
+ jupyterHelp . signatures [ 0 ] ;
764
+ return preferJupyterHelp ? jupyterHelp : lsHelp ;
765
+ } ;
766
+
767
+ this . postTimedResponse ( [ getSignatureHelp ( ) ] , InteractiveWindowMessages . ProvideSignatureHelpResponse , ( s ) => {
768
+ if ( s && s [ 0 ] ) {
769
+ return { signatureHelp : s [ 0 ] ! , requestId : request . requestId } ;
770
+ } else {
771
+ return {
772
+ signatureHelp : { signatures : [ ] , activeParameter : 0 , activeSignature : 0 } ,
773
+ requestId : request . requestId
774
+ } ;
600
775
}
601
- ) ;
776
+ } ) ;
602
777
}
603
778
604
779
private async update ( request : NotebookModelChange ) : Promise < void > {
0 commit comments