11// Copyright (c) Microsoft Corporation. All rights reserved.
22// Licensed under the MIT License.
33'use strict' ;
4+ import type { KernelMessage } from '@jupyterlab/services' ;
45import * as hashjs from 'hash.js' ;
56import { inject , injectable , multiInject , optional } from 'inversify' ;
7+ import stripAnsi from 'strip-ansi' ;
68import { Event , EventEmitter , Position , Range , TextDocumentChangeEvent , TextDocumentContentChangeEvent } from 'vscode' ;
79
810import { splitMultilineString } from '../../../datascience-ui/common' ;
@@ -23,12 +25,18 @@ import {
2325 INotebookExecutionLogger
2426} from '../types' ;
2527
28+ // tslint:disable-next-line:no-require-imports no-var-requires
29+ const _escapeRegExp = require ( 'lodash/escapeRegExp' ) as typeof import ( 'lodash/escapeRegExp' ) ; // NOSONAR
30+ const LineNumberMatchRegex = / ( ; 3 2 m [ - > ] * ?) ( \d + ) / g;
31+
2632interface IRangedCellHash extends ICellHash {
2733 code : string ;
2834 startOffset : number ;
2935 endOffset : number ;
3036 deleted : boolean ;
3137 realCode : string ;
38+ trimmedRightCode : string ;
39+ firstNonBlankLineIndex : number ; // zero based. First non blank line of the real code.
3240}
3341
3442// This class provides hashes for debugging jupyter cells. Call getHashes just before starting debugging to compute all of the
@@ -45,6 +53,7 @@ export class CellHashProvider implements ICellHashProvider, INotebookExecutionLo
4553 private executionCount : number = 0 ;
4654 private hashes : Map < string , IRangedCellHash [ ] > = new Map < string , IRangedCellHash [ ] > ( ) ;
4755 private updateEventEmitter : EventEmitter < void > = new EventEmitter < void > ( ) ;
56+ private traceBackRegexes = new Map < string , RegExp > ( ) ;
4857
4958 constructor (
5059 @inject ( IDocumentManager ) private documentManager : IDocumentManager ,
@@ -59,6 +68,7 @@ export class CellHashProvider implements ICellHashProvider, INotebookExecutionLo
5968
6069 public dispose ( ) {
6170 this . hashes . clear ( ) ;
71+ this . traceBackRegexes . clear ( ) ;
6272 }
6373
6474 public get updated ( ) : Event < void > {
@@ -83,6 +93,7 @@ export class CellHashProvider implements ICellHashProvider, INotebookExecutionLo
8393
8494 public onKernelRestarted ( ) {
8595 this . hashes . clear ( ) ;
96+ this . traceBackRegexes . clear ( ) ;
8697 this . executionCount = 0 ;
8798 this . updateEventEmitter . fire ( ) ;
8899 }
@@ -112,6 +123,21 @@ export class CellHashProvider implements ICellHashProvider, INotebookExecutionLo
112123 noop ( ) ;
113124 }
114125
126+ public preHandleIOPub ( msg : KernelMessage . IIOPubMessage ) : KernelMessage . IIOPubMessage {
127+ // When an error message comes, rewrite the traceback so we can jump back to the correct
128+ // cell. For now this only works with the interactive window
129+ if ( msg . header . msg_type === 'error' ) {
130+ return {
131+ ...msg ,
132+ content : {
133+ ...msg . content ,
134+ traceback : this . modifyTraceback ( msg as KernelMessage . IErrorMsg ) // NOSONAR
135+ }
136+ } ;
137+ }
138+ return msg ;
139+ }
140+
115141 public extractExecutableLines ( cell : ICell ) : string [ ] {
116142 const cellMatcher = new CellMatcher ( this . configService . getSettings ( getCellResource ( cell ) ) . datascience ) ;
117143 const lines = splitMultilineString ( cell . data . source ) ;
@@ -144,6 +170,12 @@ export class CellHashProvider implements ICellHashProvider, INotebookExecutionLo
144170 const line = doc . lineAt ( trueStartLine ) ;
145171 const endLine = doc . lineAt ( Math . min ( trueStartLine + stripped . length - 1 , doc . lineCount - 1 ) ) ;
146172
173+ // Find the first non blank line
174+ let firstNonBlankIndex = 0 ;
175+ while ( firstNonBlankIndex < stripped . length && stripped [ firstNonBlankIndex ] . trim ( ) . length === 0 ) {
176+ firstNonBlankIndex += 1 ;
177+ }
178+
147179 // Use the original values however to track edits. This is what we need
148180 // to move around
149181 const startOffset = doc . offsetAt ( new Position ( cell . line , 0 ) ) ;
@@ -180,11 +212,13 @@ export class CellHashProvider implements ICellHashProvider, INotebookExecutionLo
180212 hash : hashjs . sha1 ( ) . update ( hashedCode ) . digest ( 'hex' ) . substr ( 0 , 12 ) ,
181213 line : line . lineNumber + 1 ,
182214 endLine : endLine . lineNumber + 1 ,
215+ firstNonBlankLineIndex : firstNonBlankIndex + trueStartLine ,
183216 executionCount : expectedCount ,
184217 startOffset,
185218 endOffset,
186219 deleted : false ,
187220 code : hashedCode ,
221+ trimmedRightCode : stripped . map ( ( s ) => s . replace ( / [ \t \r ] + \n $ / g, '\n' ) ) . join ( '' ) ,
188222 realCode,
189223 runtimeLine,
190224 id : cell . id
@@ -217,6 +251,15 @@ export class CellHashProvider implements ICellHashProvider, INotebookExecutionLo
217251 }
218252 this . hashes . set ( cell . file , list ) ;
219253
254+ // Save a regex to find this file later when looking for
255+ // exceptions in output
256+ if ( ! this . traceBackRegexes . has ( cell . file ) ) {
257+ const fileDisplayName = this . fileSystem . getDisplayName ( cell . file ) ;
258+ const escaped = _escapeRegExp ( fileDisplayName ) ;
259+ const fileMatchRegex = new RegExp ( `\\[.*?;32m${ escaped } ` ) ;
260+ this . traceBackRegexes . set ( cell . file , fileMatchRegex ) ;
261+ }
262+
220263 // Tell listeners we have new hashes.
221264 if ( this . listeners ) {
222265 const hashes = this . getHashes ( ) ;
@@ -306,4 +349,60 @@ export class CellHashProvider implements ICellHashProvider, INotebookExecutionLo
306349 // No breakpoint necessary, start on the first line
307350 return 1 ;
308351 }
352+
353+ // This function will modify a traceback from an error message.
354+ // Tracebacks take a form like so:
355+ // "[1;31m---------------------------------------------------------------------------[0m"
356+ // "[1;31mZeroDivisionError[0m Traceback (most recent call last)"
357+ // "[1;32md:\Training\SnakePython\foo.py[0m in [0;36m<module>[1;34m[0m\n[0;32m 1[0m [0mprint[0m[1;33m([0m[1;34m'some more'[0m[1;33m)[0m[1;33m[0m[1;33m[0m[0m\n [1;32m----> 2[1;33m [0mcause_error[0m[1;33m([0m[1;33m)[0m[1;33m[0m[1;33m[0m[0m\n [0m"
358+ // "[1;32md:\Training\SnakePython\foo.py[0m in [0;36mcause_error[1;34m()[0m\n[0;32m 3[0m [0mprint[0m[1;33m([0m[1;34m'error'[0m[1;33m)[0m[1;33m[0m[1;33m[0m[0m\n [0;32m 4[0m [0mprint[0m[1;33m([0m[1;34m'now'[0m[1;33m)[0m[1;33m[0m[1;33m[0m[0m\n [1;32m----> 5[1;33m [0mprint[0m[1;33m([0m [1;36m1[0m [1;33m/[0m [1;36m0[0m[1;33m)[0m[1;33m[0m[1;33m[0m[0m\n [0m"
359+ // "[1;31mZeroDivisionError[0m: division by zero"
360+ // Each item in the array being a stack frame.
361+ private modifyTraceback ( msg : KernelMessage . IErrorMsg ) : string [ ] {
362+ // Do one frame at a time.
363+ return msg . content . traceback ? msg . content . traceback . map ( this . modifyTracebackFrame . bind ( this ) ) : [ ] ;
364+ }
365+
366+ private findCellOffset ( hashes : IRangedCellHash [ ] | undefined , codeLines : string ) : number | undefined {
367+ if ( hashes ) {
368+ // Go through all cell code looking for these code lines exactly
369+ // (although with right side trimmed as that's what a stack trace does)
370+ for ( const hash of hashes ) {
371+ const index = hash . trimmedRightCode . indexOf ( codeLines ) ;
372+ if ( index >= 0 ) {
373+ // Jupyter isn't counting blank lines at the top so use our
374+ // first non blank line
375+ return hash . firstNonBlankLineIndex ;
376+ }
377+ }
378+ }
379+ // No hash found
380+ return undefined ;
381+ }
382+
383+ private modifyTracebackFrame ( traceFrame : string ) : string {
384+ // See if this item matches any of our cell files
385+ const regexes = [ ...this . traceBackRegexes . entries ( ) ] ;
386+ const match = regexes . find ( ( e ) => e [ 1 ] . test ( traceFrame ) ) ;
387+ if ( match ) {
388+ // We have a match, pull out the source lines
389+ let sourceLines = '' ;
390+ const regex = / ( ; 3 2 m [ - > ] * ?) ( \d + ) ( .* ) / g;
391+ for ( let l = regex . exec ( traceFrame ) ; l && l . length > 3 ; l = regex . exec ( traceFrame ) ) {
392+ const newLine = stripAnsi ( l [ 3 ] ) . substr ( 1 ) ; // Seem to have a space on the front
393+ sourceLines = `${ sourceLines } ${ newLine } \n` ;
394+ }
395+
396+ // Now attempt to find a cell that matches these source lines
397+ const offset = this . findCellOffset ( this . hashes . get ( match [ 0 ] ) , sourceLines ) ;
398+ if ( offset !== undefined ) {
399+ return traceFrame . replace ( LineNumberMatchRegex , ( _s , prefix , num ) => {
400+ const n = parseInt ( num , 10 ) ;
401+ const newLine = offset + n - 1 ;
402+ return `${ prefix } <a href='file://${ match [ 0 ] } ?line=${ newLine } '>${ newLine + 1 } </a>` ;
403+ } ) ;
404+ }
405+ }
406+ return traceFrame ;
407+ }
309408}
0 commit comments