Skip to content

Commit 257b970

Browse files
authored
Support break on enter for debugging (#6450)
* Support break on enter for debugging * Fix breakpoints not messing up entry breakpoint * Fix problem with multiple files
1 parent c827add commit 257b970

12 files changed

Lines changed: 287 additions & 235 deletions

File tree

news/1 Enhancements/6449.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Support break on enter for debugging a cell.

package.json

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1361,6 +1361,12 @@
13611361
"description": "Path to ptsvd experimental bits for debugging cells.",
13621362
"scope": "resource"
13631363
},
1364+
"python.dataScience.stopOnFirstLineWhileDebugging": {
1365+
"type": "boolean",
1366+
"default": true,
1367+
"description": "When debugging a cell, stop on the first line if no other breakpoints in the current cell.",
1368+
"scope": "resource"
1369+
},
13641370
"python.disableInstallationCheck": {
13651371
"type": "boolean",
13661372
"default": false,

package.nls.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@
3434
"python.command.python.datascience.runallcellsabove.palette.title": "Run Cells Above Current Cell",
3535
"python.command.python.datascience.runcurrentcellandallbelow.palette.title": "Run Current Cell and Below",
3636
"python.command.python.datascience.debugcurrentcell.palette.title": "Debug Current Cell",
37-
"python.command.python.datascience.debugcurrentcell.title": "Debug Cell",
37+
"python.command.python.datascience.debugcell.title": "Debug Cell",
3838
"python.command.python.datascience.runtoline.title": "Run To Line in Python Interactive window",
3939
"python.command.python.datascience.runfromline.title": "Run From Line in Python Interactive window",
4040
"python.command.python.datascience.runcurrentcell.title": "Run Current Cell",

src/client/common/types.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -322,6 +322,7 @@ export interface IDataScienceSettings {
322322
enablePlotViewer?: boolean;
323323
codeLenses?: string;
324324
ptvsdDistPath?: string;
325+
stopOnFirstLineWhileDebugging?: boolean;
325326
}
326327

327328
export const IConfigurationService = Symbol('IConfigurationService');

src/client/datascience/editor-integration/cellhashprovider.ts

Lines changed: 93 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -2,17 +2,25 @@
22
// Licensed under the MIT License.
33
'use strict';
44
import * 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';
917
import { IConfigurationService } from '../../common/types';
10-
import { generateCells } from '../cellFactory';
18+
import { noop } from '../../common/utils/misc';
1119
import { CellMatcher } from '../cellMatcher';
1220
import { splitMultilineString } from '../common';
1321
import { 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

1725
interface 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

Comments
 (0)