@@ -13,7 +13,9 @@ import {
1313 Event ,
1414 EventEmitter ,
1515 Hover ,
16+ MarkdownString ,
1617 SignatureHelpContext ,
18+ SignatureInformation ,
1719 TextDocumentContentChangeEvent ,
1820 Uri
1921} from 'vscode' ;
@@ -64,6 +66,24 @@ import {
6466} from './conversion' ;
6567import { IntellisenseDocument } from './intellisenseDocument' ;
6668
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+
6787// tslint:disable:no-any
6888@injectable ( )
6989export class IntellisenseProvider implements IInteractiveWindowListener {
@@ -253,6 +273,7 @@ export class IntellisenseProvider implements IInteractiveWindowListener {
253273 incomplete : false
254274 } ;
255275 }
276+
256277 protected async provideHover (
257278 position : monacoEditor . Position ,
258279 wordAtPosition : string | undefined ,
@@ -266,9 +287,27 @@ export class IntellisenseProvider implements IInteractiveWindowListener {
266287 ] ) ;
267288 if ( ! variableHover && languageServer && document ) {
268289 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 ;
272311 }
273312 } else if ( variableHover ) {
274313 return convertToMonacoHover ( variableHover ) ;
@@ -278,6 +317,7 @@ export class IntellisenseProvider implements IInteractiveWindowListener {
278317 contents : [ ]
279318 } ;
280319 }
320+
281321 protected async provideSignatureHelp (
282322 position : monacoEditor . Position ,
283323 context : monacoEditor . languages . SignatureHelpContext ,
@@ -349,6 +389,37 @@ export class IntellisenseProvider implements IInteractiveWindowListener {
349389 }
350390 }
351391
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+
352423 private dispatchMessage < M extends IInteractiveWindowMapping , T extends keyof M > (
353424 _message : T ,
354425 payload : any ,
@@ -465,6 +536,92 @@ export class IntellisenseProvider implements IInteractiveWindowListener {
465536 ) ;
466537 }
467538
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+
468625 private async provideJupyterCompletionItems (
469626 position : monacoEditor . Position ,
470627 _context : monacoEditor . languages . CompletionContext ,
@@ -477,17 +634,7 @@ export class IntellisenseProvider implements IInteractiveWindowListener {
477634 const data = document . getCellData ( cellId ) ;
478635
479636 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 ) ;
491638 const jupyterResults = await activeNotebook . getCompletion ( data . text , offsetInCode , cancelToken ) ;
492639 if ( jupyterResults && jupyterResults . matches ) {
493640 const filteredMatches = this . filterJupyterMatches ( document , jupyterResults , cellId , position ) ;
@@ -585,20 +732,48 @@ export class IntellisenseProvider implements IInteractiveWindowListener {
585732 private handleSignatureHelpRequest ( request : IProvideSignatureHelpRequest ) {
586733 const cancelSource = new CancellationTokenSource ( ) ;
587734 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+ } ;
600775 }
601- ) ;
776+ } ) ;
602777 }
603778
604779 private async update ( request : NotebookModelChange ) : Promise < void > {
0 commit comments