22// Licensed under the MIT License.
33'use strict' ;
44import * as hashjs from 'hash.js' ;
5- import { inject , injectable } from 'inversify' ;
6- import { Event , EventEmitter , Position , Range , TextDocumentChangeEvent , TextDocumentContentChangeEvent } from 'vscode' ;
5+ import { inject , injectable , multiInject , optional } from 'inversify' ;
6+ import {
7+ Event ,
8+ EventEmitter ,
9+ Position ,
10+ Range ,
11+ SourceBreakpoint ,
12+ TextDocumentChangeEvent ,
13+ TextDocumentContentChangeEvent
14+ } from 'vscode' ;
715
8- import { IDocumentManager } from '../../common/application/types' ;
16+ import { IDebugService , IDocumentManager } from '../../common/application/types' ;
917import { IConfigurationService } from '../../common/types' ;
10- import { generateCells } from '../cellFactory ' ;
18+ import { noop } from '../../common/utils/misc ' ;
1119import { CellMatcher } from '../cellMatcher' ;
1220import { splitMultilineString } from '../common' ;
1321import { Identifiers } from '../constants' ;
14- import { InteractiveWindowMessages , IRemoteAddCode , SysInfoReason } from '../interactive-window/interactiveWindowTypes' ;
15- import { ICellHash , ICellHashProvider , IFileHashes , IInteractiveWindowListener } from '../types' ;
22+ import { InteractiveWindowMessages , SysInfoReason } from '../interactive-window/interactiveWindowTypes' ;
23+ import { ICell , ICellHash , ICellHashListener , ICellHashProvider , IFileHashes , IInteractiveWindowListener , INotebookExecutionLogger } from '../types' ;
1624
1725interface IRangedCellHash extends ICellHash {
1826 code : string ;
@@ -25,19 +33,21 @@ interface IRangedCellHash extends ICellHash {
2533// This class provides hashes for debugging jupyter cells. Call getHashes just before starting debugging to compute all of the
2634// hashes for cells.
2735@injectable ( )
28- export class CellHashProvider implements ICellHashProvider , IInteractiveWindowListener {
36+ export class CellHashProvider implements ICellHashProvider , IInteractiveWindowListener , INotebookExecutionLogger {
2937
3038 // tslint:disable-next-line: no-any
31- private postEmitter : EventEmitter < { message : string ; payload : any } > = new EventEmitter < { message : string ; payload : any } > ( ) ;
39+ private postEmitter : EventEmitter < { message : string ; payload : any } > = new EventEmitter < { message : string ; payload : any } > ( ) ;
3240 // Map of file to Map of start line to actual hash
33- private hashes : Map < string , IRangedCellHash [ ] > = new Map < string , IRangedCellHash [ ] > ( ) ;
41+ private hashes : Map < string , IRangedCellHash [ ] > = new Map < string , IRangedCellHash [ ] > ( ) ;
3442 private executionCount : number = 0 ;
43+ private updateEventEmitter : EventEmitter < void > = new EventEmitter < void > ( ) ;
3544
3645 constructor (
3746 @inject ( IDocumentManager ) private documentManager : IDocumentManager ,
38- @inject ( IConfigurationService ) private configService : IConfigurationService
39- )
40- {
47+ @inject ( IConfigurationService ) private configService : IConfigurationService ,
48+ @inject ( IDebugService ) private debugService : IDebugService ,
49+ @multiInject ( ICellHashListener ) @optional ( ) private listeners : ICellHashListener [ ] | undefined
50+ ) {
4151 // Watch document changes so we can update our hashes
4252 this . documentManager . onDidChangeTextDocument ( this . onChangedDocument . bind ( this ) ) ;
4353 }
@@ -46,6 +56,10 @@ export class CellHashProvider implements ICellHashProvider, IInteractiveWindowLi
4656 this . hashes . clear ( ) ;
4757 }
4858
59+ public get updated ( ) : Event < void > {
60+ return this . updateEventEmitter . event ;
61+ }
62+
4963 // tslint:disable-next-line: no-any
5064 public get postMessage ( ) : Event < { message : string ; payload : any } > {
5165 return this . postEmitter . event ;
@@ -54,12 +68,6 @@ export class CellHashProvider implements ICellHashProvider, IInteractiveWindowLi
5468 // tslint:disable-next-line: no-any
5569 public onMessage ( message : string , payload ?: any ) : void {
5670 switch ( message ) {
57- case InteractiveWindowMessages . RemoteAddCode :
58- if ( payload ) {
59- this . onAboutToAddCode ( payload ) ;
60- }
61- break ;
62-
6371 case InteractiveWindowMessages . AddedSysInfo :
6472 if ( payload && payload . type ) {
6573 const reason = payload . type as SysInfoReason ;
@@ -83,26 +91,22 @@ export class CellHashProvider implements ICellHashProvider, IInteractiveWindowLi
8391 } ) . filter ( e => e . hashes . length > 0 ) ;
8492 }
8593
86- private onAboutToAddCode ( args : IRemoteAddCode ) {
87- // Make sure this is valid
88- if ( args && args . code && args . line !== undefined && args . file ) {
89- // First make sure not a markdown cell. Those can be ignored. Just get out the first code cell.
90- // Regardless of how many 'code' cells exist in the code sent to us, we'll only ever send one at most.
91- // The code sent to this function is either a cell as defined by #%% or the selected text (which is treated as one cell)
92- const cells = generateCells ( this . configService . getSettings ( ) . datascience , args . code , args . file , args . line , true , args . id ) ;
93- const codeCell = cells . find ( c => c . data . cell_type === 'code' ) ;
94- if ( codeCell ) {
95- // When the user adds new code, we know the execution count is increasing
96- this . executionCount += 1 ;
97-
98- // Skip hash on unknown file though
99- if ( args . file !== Identifiers . EmptyFileName ) {
100- this . addCellHash ( splitMultilineString ( codeCell . data . source ) , codeCell . line , codeCell . file , this . executionCount ) ;
101- }
94+ public async preExecute ( cell : ICell , silent : boolean ) : Promise < void > {
95+ if ( ! silent ) {
96+ // When the user adds new code, we know the execution count is increasing
97+ this . executionCount += 1 ;
98+
99+ // Skip hash on unknown file though
100+ if ( cell . file !== Identifiers . EmptyFileName ) {
101+ return this . addCellHash ( cell , this . executionCount ) ;
102102 }
103103 }
104104 }
105105
106+ public async postExecute ( _cell : ICell , _silent : boolean ) : Promise < void > {
107+ noop ( ) ;
108+ }
109+
106110 private onChangedDocument ( e : TextDocumentChangeEvent ) {
107111 // See if the document is in our list of docs to watch
108112 const perFile = this . hashes . get ( e . document . fileName ) ;
@@ -115,7 +119,7 @@ export class CellHashProvider implements ICellHashProvider, IInteractiveWindowLi
115119 }
116120 }
117121
118- private handleContentChange ( docText : string , c : TextDocumentContentChangeEvent , hashes : IRangedCellHash [ ] ) : string {
122+ private handleContentChange ( docText : string , c : TextDocumentContentChangeEvent , hashes : IRangedCellHash [ ] ) : string {
119123 // First compute the number of lines that changed
120124 const lineDiff = c . text . split ( '\n' ) . length - docText . substr ( c . rangeOffset , c . rangeLength ) . split ( '\n' ) . length ;
121125 const offsetDiff = c . text . length - c . rangeLength ;
@@ -145,25 +149,26 @@ export class CellHashProvider implements ICellHashProvider, IInteractiveWindowLi
145149 return appliedText ;
146150 }
147151
148- private applyChange ( docText : string , c : TextDocumentContentChangeEvent ) : string {
152+ private applyChange ( docText : string , c : TextDocumentContentChangeEvent ) : string {
149153 const before = docText . substr ( 0 , c . rangeOffset ) ;
150154 const after = docText . substr ( c . rangeOffset + c . rangeLength ) ;
151155 return `${ before } ${ c . text } ${ after } ` ;
152156 }
153157
154- private addCellHash ( lines : string [ ] , startLine : number , file : string , expectedCount : number ) {
158+ private async addCellHash ( cell : ICell , expectedCount : number ) : Promise < void > {
155159 // Find the text document that matches. We need more information than
156160 // the add code gives us
157- const doc = this . documentManager . textDocuments . find ( d => d . fileName === file ) ;
161+ const doc = this . documentManager . textDocuments . find ( d => d . fileName === cell . file ) ;
158162 if ( doc ) {
159163 const cellMatcher = new CellMatcher ( this . configService . getSettings ( ) . datascience ) ;
160164
161165 // Compute the code that will really be sent to jupyter
166+ const lines = splitMultilineString ( cell . data . source ) ;
162167 const stripped = lines . filter ( l => ! cellMatcher . isCode ( l ) ) ;
163168
164169 // Figure out our true 'start' line. This is what we need to tell the debugger is
165170 // actually the start of the code as that's what Jupyter will be getting.
166- let trueStartLine = startLine ;
171+ let trueStartLine = cell . line ;
167172 for ( let i = 0 ; i < stripped . length ; i += 1 ) {
168173 if ( stripped [ i ] !== lines [ i ] ) {
169174 trueStartLine += i + 1 ;
@@ -175,7 +180,7 @@ export class CellHashProvider implements ICellHashProvider, IInteractiveWindowLi
175180
176181 // Use the original values however to track edits. This is what we need
177182 // to move around
178- const startOffset = doc . offsetAt ( new Position ( startLine , 0 ) ) ;
183+ const startOffset = doc . offsetAt ( new Position ( cell . line , 0 ) ) ;
179184 const endOffset = doc . offsetAt ( endLine . rangeIncludingLineBreak . end ) ;
180185
181186 // Jupyter also removes blank lines at the end.
@@ -188,10 +193,13 @@ export class CellHashProvider implements ICellHashProvider, IInteractiveWindowLi
188193 if ( ! lastLine . endsWith ( '\n' ) ) {
189194 stripped [ stripped . length - 1 ] = `${ lastLine } \n` ;
190195 }
196+
197+ // Compute the runtime line and adjust our cell/stripped source for debugging
198+ const runtimeLine = this . adjustForDebugging ( cell , stripped , startOffset , endOffset ) ;
191199 const hashedCode = stripped . join ( '' ) ;
192- const realCode = doc . getText ( new Range ( new Position ( startLine , 0 ) , endLine . rangeIncludingLineBreak . end ) ) ;
200+ const realCode = doc . getText ( new Range ( new Position ( cell . line , 0 ) , endLine . rangeIncludingLineBreak . end ) ) ;
193201
194- const hash : IRangedCellHash = {
202+ const hash : IRangedCellHash = {
195203 hash : hashjs . sha1 ( ) . update ( hashedCode ) . digest ( 'hex' ) . substr ( 0 , 12 ) ,
196204 line : line . lineNumber + 1 ,
197205 endLine : endLine . lineNumber + 1 ,
@@ -200,10 +208,11 @@ export class CellHashProvider implements ICellHashProvider, IInteractiveWindowLi
200208 endOffset,
201209 deleted : false ,
202210 code : hashedCode ,
203- realCode
211+ realCode,
212+ runtimeLine
204213 } ;
205214
206- let list = this . hashes . get ( file ) ;
215+ let list = this . hashes . get ( cell . file ) ;
207216 if ( ! list ) {
208217 list = [ ] ;
209218 }
@@ -226,7 +235,46 @@ export class CellHashProvider implements ICellHashProvider, IInteractiveWindowLi
226235 if ( ! inserted ) {
227236 list . push ( hash ) ;
228237 }
229- this . hashes . set ( file , list ) ;
238+ this . hashes . set ( cell . file , list ) ;
239+
240+ // Tell listeners we have new hashes.
241+ if ( this . listeners ) {
242+ const hashes = this . getHashes ( ) ;
243+ await Promise . all ( this . listeners . map ( l => l . hashesUpdated ( hashes ) ) ) ;
244+ }
245+ }
246+ }
247+
248+ private adjustForDebugging ( cell : ICell , source : string [ ] , cellStartOffset : number , cellEndOffset : number ) : number {
249+ if ( this . debugService . activeDebugSession && this . configService . getSettings ( ) . datascience . stopOnFirstLineWhileDebugging ) {
250+ // See if any breakpoints in any cell that's already run or in the cell we're about to run
251+ const anyExisting = this . debugService . breakpoints . filter ( b => {
252+ // tslint:disable-next-line: no-any
253+ if ( ( b as any ) . location ) {
254+ const sb = b as SourceBreakpoint ;
255+ const sbFile = sb . location . uri . fsPath ;
256+ if ( sbFile === cell . file ) {
257+ const doc = this . documentManager . textDocuments . find ( d => d . fileName === sb . location . uri . fsPath ) ;
258+ const startOffset = doc ? doc . offsetAt ( sb . location . range . start ) : - 1 ;
259+
260+ // Check if this breakpoint is in our current code.
261+ if ( startOffset >= cellStartOffset && startOffset <= cellEndOffset ) {
262+ return true ;
263+ }
264+ }
265+ }
266+ } ) ;
267+ if ( ! anyExisting || anyExisting . length <= 0 ) {
268+ // There are no matching breakpoints, We need to inject a breakpoint into our cell
269+ source . splice ( 0 , 0 , 'breakpoint()\n' ) ;
270+ cell . data . source = source ;
271+ cell . extraLines = [ 0 ] ;
272+
273+ // Start on the second line
274+ return 2 ;
275+ }
230276 }
277+ // No breakpoint necessary, start on the first line
278+ return 1 ;
231279 }
232280}
0 commit comments