1
1
// Copyright (c) Microsoft Corporation. All rights reserved.
2
2
// Licensed under the MIT License.
3
3
'use strict' ;
4
+ import type { KernelMessage } from '@jupyterlab/services' ;
4
5
import * as hashjs from 'hash.js' ;
5
6
import { inject , injectable , multiInject , optional } from 'inversify' ;
7
+ import stripAnsi from 'strip-ansi' ;
6
8
import { Event , EventEmitter , Position , Range , TextDocumentChangeEvent , TextDocumentContentChangeEvent } from 'vscode' ;
7
9
8
10
import { splitMultilineString } from '../../../datascience-ui/common' ;
@@ -23,12 +25,18 @@ import {
23
25
INotebookExecutionLogger
24
26
} from '../types' ;
25
27
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
+
26
32
interface IRangedCellHash extends ICellHash {
27
33
code : string ;
28
34
startOffset : number ;
29
35
endOffset : number ;
30
36
deleted : boolean ;
31
37
realCode : string ;
38
+ trimmedRightCode : string ;
39
+ firstNonBlankLineIndex : number ; // zero based. First non blank line of the real code.
32
40
}
33
41
34
42
// 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
45
53
private executionCount : number = 0 ;
46
54
private hashes : Map < string , IRangedCellHash [ ] > = new Map < string , IRangedCellHash [ ] > ( ) ;
47
55
private updateEventEmitter : EventEmitter < void > = new EventEmitter < void > ( ) ;
56
+ private traceBackRegexes = new Map < string , RegExp > ( ) ;
48
57
49
58
constructor (
50
59
@inject ( IDocumentManager ) private documentManager : IDocumentManager ,
@@ -59,6 +68,7 @@ export class CellHashProvider implements ICellHashProvider, INotebookExecutionLo
59
68
60
69
public dispose ( ) {
61
70
this . hashes . clear ( ) ;
71
+ this . traceBackRegexes . clear ( ) ;
62
72
}
63
73
64
74
public get updated ( ) : Event < void > {
@@ -83,6 +93,7 @@ export class CellHashProvider implements ICellHashProvider, INotebookExecutionLo
83
93
84
94
public onKernelRestarted ( ) {
85
95
this . hashes . clear ( ) ;
96
+ this . traceBackRegexes . clear ( ) ;
86
97
this . executionCount = 0 ;
87
98
this . updateEventEmitter . fire ( ) ;
88
99
}
@@ -112,6 +123,21 @@ export class CellHashProvider implements ICellHashProvider, INotebookExecutionLo
112
123
noop ( ) ;
113
124
}
114
125
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
+
115
141
public extractExecutableLines ( cell : ICell ) : string [ ] {
116
142
const cellMatcher = new CellMatcher ( this . configService . getSettings ( getCellResource ( cell ) ) . datascience ) ;
117
143
const lines = splitMultilineString ( cell . data . source ) ;
@@ -144,6 +170,12 @@ export class CellHashProvider implements ICellHashProvider, INotebookExecutionLo
144
170
const line = doc . lineAt ( trueStartLine ) ;
145
171
const endLine = doc . lineAt ( Math . min ( trueStartLine + stripped . length - 1 , doc . lineCount - 1 ) ) ;
146
172
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
+
147
179
// Use the original values however to track edits. This is what we need
148
180
// to move around
149
181
const startOffset = doc . offsetAt ( new Position ( cell . line , 0 ) ) ;
@@ -180,11 +212,13 @@ export class CellHashProvider implements ICellHashProvider, INotebookExecutionLo
180
212
hash : hashjs . sha1 ( ) . update ( hashedCode ) . digest ( 'hex' ) . substr ( 0 , 12 ) ,
181
213
line : line . lineNumber + 1 ,
182
214
endLine : endLine . lineNumber + 1 ,
215
+ firstNonBlankLineIndex : firstNonBlankIndex + trueStartLine ,
183
216
executionCount : expectedCount ,
184
217
startOffset,
185
218
endOffset,
186
219
deleted : false ,
187
220
code : hashedCode ,
221
+ trimmedRightCode : stripped . map ( ( s ) => s . replace ( / [ \t \r ] + \n $ / g, '\n' ) ) . join ( '' ) ,
188
222
realCode,
189
223
runtimeLine,
190
224
id : cell . id
@@ -217,6 +251,15 @@ export class CellHashProvider implements ICellHashProvider, INotebookExecutionLo
217
251
}
218
252
this . hashes . set ( cell . file , list ) ;
219
253
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
+
220
263
// Tell listeners we have new hashes.
221
264
if ( this . listeners ) {
222
265
const hashes = this . getHashes ( ) ;
@@ -306,4 +349,60 @@ export class CellHashProvider implements ICellHashProvider, INotebookExecutionLo
306
349
// No breakpoint necessary, start on the first line
307
350
return 1 ;
308
351
}
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
+ }
309
408
}
0 commit comments