Skip to content

Commit 9002be4

Browse files
authored
Support mapping cells to hash/execution counts (#6338)
* Initial idea for code hash provider * Mock document manager. More tests * Fix double run * More tests * Handle undo situation correctly * Handle more deletion cases * Add news entry * Handle multiple change events at the same time * Fix linter problems * Eliminate unnecessary restart. * Add test for multiple cells in range * Expand explanation of how many cells at once
1 parent 0ba50a1 commit 9002be4

9 files changed

Lines changed: 985 additions & 19 deletions

File tree

news/1 Enhancements/6318.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Provide code mapping service for debugging cells.

package-lock.json

Lines changed: 18 additions & 6 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
Lines changed: 212 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,212 @@
1+
// Copyright (c) Microsoft Corporation. All rights reserved.
2+
// Licensed under the MIT License.
3+
'use strict';
4+
import * as hashjs from 'hash.js';
5+
import { inject, injectable } from 'inversify';
6+
import {
7+
Event,
8+
EventEmitter,
9+
Position,
10+
Range,
11+
TextDocumentChangeEvent,
12+
TextDocumentContentChangeEvent
13+
} from 'vscode';
14+
15+
import { IDocumentManager } from '../../common/application/types';
16+
import { IConfigurationService } from '../../common/types';
17+
import { generateCells } from '../cellFactory';
18+
import { concatMultilineString } from '../common';
19+
import { Identifiers } from '../constants';
20+
import { InteractiveWindowMessages, IRemoteAddCode, SysInfoReason } from '../interactive-window/interactiveWindowTypes';
21+
import { ICellHash, ICellHashProvider, IFileHashes, IInteractiveWindowListener } from '../types';
22+
23+
interface IRangedCellHash extends ICellHash {
24+
code: string;
25+
startOffset: number;
26+
endOffset: number;
27+
deleted: boolean;
28+
realCode: string;
29+
}
30+
31+
// This class provides hashes for debugging jupyter cells. Call getHashes just before starting debugging to compute all of the
32+
// hashes for cells.
33+
@injectable()
34+
export class CellHashProvider implements ICellHashProvider, IInteractiveWindowListener {
35+
36+
// tslint:disable-next-line: no-any
37+
private postEmitter: EventEmitter<{message: string; payload: any}> = new EventEmitter<{message: string; payload: any}>();
38+
// Map of file to Map of start line to actual hash
39+
private hashes : Map<string, IRangedCellHash[]> = new Map<string, IRangedCellHash[]>();
40+
private executionCount: number = 0;
41+
42+
constructor(
43+
@inject(IDocumentManager) private documentManager: IDocumentManager,
44+
@inject(IConfigurationService) private configService: IConfigurationService
45+
)
46+
{
47+
// Watch document changes so we can update our hashes
48+
this.documentManager.onDidChangeTextDocument(this.onChangedDocument.bind(this));
49+
}
50+
51+
public dispose() {
52+
this.hashes.clear();
53+
}
54+
55+
// tslint:disable-next-line: no-any
56+
public get postMessage(): Event<{ message: string; payload: any }> {
57+
return this.postEmitter.event;
58+
}
59+
60+
// tslint:disable-next-line: no-any
61+
public onMessage(message: string, payload?: any): void {
62+
switch (message) {
63+
case InteractiveWindowMessages.RemoteAddCode:
64+
if (payload) {
65+
this.onAboutToAddCode(payload);
66+
}
67+
break;
68+
69+
case InteractiveWindowMessages.AddedSysInfo:
70+
if (payload && payload.type) {
71+
const reason = payload.type as SysInfoReason;
72+
if (reason !== SysInfoReason.Interrupt) {
73+
this.hashes.clear();
74+
}
75+
}
76+
break;
77+
78+
default:
79+
break;
80+
}
81+
}
82+
83+
public getHashes(): IFileHashes[] {
84+
return [...this.hashes.entries()].map(e => {
85+
return {
86+
file: e[0],
87+
hashes: e[1].filter(h => !h.deleted)
88+
};
89+
}).filter(e => e.hashes.length > 0);
90+
}
91+
92+
private onAboutToAddCode(args: IRemoteAddCode) {
93+
// Make sure this is valid
94+
if (args && args.code && args.line !== undefined && args.file) {
95+
// First make sure not a markdown cell. Those can be ignored. Just get out the first code cell.
96+
// Regardless of how many 'code' cells exist in the code sent to us, we'll only ever send one at most.
97+
// The code sent to this function is either a cell as defined by #%% or the selected text (which is treated as one cell)
98+
const cells = generateCells(this.configService.getSettings().datascience, args.code, args.file, args.line, true, args.id);
99+
const codeCell = cells.find(c => c.data.cell_type === 'code');
100+
if (codeCell) {
101+
// When the user adds new code, we know the execution count is increasing
102+
this.executionCount += 1;
103+
104+
// Skip hash on unknown file though
105+
if (args.file !== Identifiers.EmptyFileName) {
106+
this.addCellHash(concatMultilineString(codeCell.data.source), codeCell.line, codeCell.file, this.executionCount);
107+
}
108+
}
109+
}
110+
}
111+
112+
private onChangedDocument(e: TextDocumentChangeEvent) {
113+
// See if the document is in our list of docs to watch
114+
const perFile = this.hashes.get(e.document.fileName);
115+
if (perFile) {
116+
// Apply the content changes to the file's cells.
117+
let prevText = e.document.getText();
118+
e.contentChanges.forEach(c => {
119+
prevText = this.handleContentChange(prevText, c, perFile);
120+
});
121+
}
122+
}
123+
124+
private handleContentChange(docText: string, c: TextDocumentContentChangeEvent, hashes: IRangedCellHash[]) : string {
125+
// First compute the number of lines that changed
126+
const lineDiff = c.text.split('\n').length - docText.substr(c.rangeOffset, c.rangeLength).split('\n').length;
127+
const offsetDiff = c.text.length - c.rangeLength;
128+
129+
// Compute the inclusive offset that is changed by the cell.
130+
const endChangedOffset = c.rangeLength <= 0 ? c.rangeOffset : c.rangeOffset + c.rangeLength - 1;
131+
132+
// Also compute the text of the document with the change applied
133+
const appliedText = this.applyChange(docText, c);
134+
135+
hashes.forEach(h => {
136+
// See how this existing cell compares to the change
137+
if (h.endOffset < c.rangeOffset) {
138+
// No change. This cell is entirely before the change
139+
} else if (h.startOffset > endChangedOffset) {
140+
// This cell is after the text that got replaced. Adjust its start/end lines
141+
h.line += lineDiff;
142+
h.endLine += lineDiff;
143+
h.startOffset += offsetDiff;
144+
h.endOffset += offsetDiff;
145+
} else {
146+
// Cell intersects. Mark as deleted if not exactly the same (user could type over the exact same values)
147+
h.deleted = appliedText.substr(h.startOffset, h.endOffset - h.startOffset) !== h.realCode;
148+
}
149+
});
150+
151+
return appliedText;
152+
}
153+
154+
private applyChange(docText: string, c: TextDocumentContentChangeEvent) : string {
155+
const before = docText.substr(0, c.rangeOffset);
156+
const after = docText.substr(c.rangeOffset + c.rangeLength);
157+
return `${before}${c.text}${after}`;
158+
}
159+
160+
private addCellHash(code: string, startLine: number, file: string, expectedCount: number) {
161+
// Find the text document that matches. We need more information than
162+
// the add code gives us
163+
const doc = this.documentManager.textDocuments.find(d => d.fileName === file);
164+
if (doc) {
165+
// The code we get is not actually what's in the document. The interactiveWindow massages it somewhat.
166+
// We need the real code so that we can match document edits later.
167+
const split = code.split('\n');
168+
const lineCount = split.length;
169+
const line = doc.lineAt(startLine);
170+
const endLine = doc.lineAt(Math.min(startLine + lineCount - 1, doc.lineCount - 1));
171+
const startOffset = doc.offsetAt(new Position(startLine, 0));
172+
const endOffset = doc.offsetAt(endLine.rangeIncludingLineBreak.end);
173+
const realCode = doc.getText(new Range(line.range.start, endLine.rangeIncludingLineBreak.end));
174+
const hash : IRangedCellHash = {
175+
hash: hashjs.sha1().update(code).digest('hex').substr(0, 12),
176+
line: startLine + 1,
177+
endLine: startLine + lineCount,
178+
executionCount: expectedCount,
179+
startOffset,
180+
endOffset,
181+
deleted: false,
182+
code,
183+
realCode
184+
};
185+
186+
let list = this.hashes.get(file);
187+
if (!list) {
188+
list = [];
189+
}
190+
191+
// Figure out where to put the item in the list
192+
let inserted = false;
193+
for (let i = 0; i < list.length && !inserted; i += 1) {
194+
const pos = list[i];
195+
if (hash.line >= pos.line && hash.line <= pos.endLine) {
196+
// Stick right here. This is either the same cell or a cell that overwrote where
197+
// we were.
198+
list.splice(i, 1, hash);
199+
inserted = true;
200+
} else if (pos.line > hash.line) {
201+
// This item comes just after the cell we're inserting.
202+
list.splice(i, 0, hash);
203+
inserted = true;
204+
}
205+
}
206+
if (!inserted) {
207+
list.push(hash);
208+
}
209+
this.hashes.set(file, list);
210+
}
211+
}
212+
}

src/client/datascience/interactive-window/interactiveWindow.ts

Lines changed: 3 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -69,16 +69,10 @@ import {
6969
InteractiveWindowMessages,
7070
IRemoteAddCode,
7171
IShowDataViewer,
72-
ISubmitNewCell
72+
ISubmitNewCell,
73+
SysInfoReason
7374
} from './interactiveWindowTypes';
7475

75-
export enum SysInfoReason {
76-
Start,
77-
Restart,
78-
Interrupt,
79-
New
80-
}
81-
8276
@injectable()
8377
export class InteractiveWindow extends WebViewHost<IInteractiveWindowMapping> implements IInteractiveWindow {
8478
private disposed: boolean = false;
@@ -1088,7 +1082,7 @@ export class InteractiveWindow extends WebViewHost<IInteractiveWindowMapping> im
10881082

10891083
// For anything but start, tell the other sides of a live share session
10901084
if (reason !== SysInfoReason.Start && sysInfo) {
1091-
this.shareMessage(InteractiveWindowMessages.AddedSysInfo, { sysInfoCell: sysInfo, id: this.id });
1085+
this.shareMessage(InteractiveWindowMessages.AddedSysInfo, { type: reason, sysInfoCell: sysInfo, id: this.id });
10921086
}
10931087

10941088
// For a restart, tell our window to reset

src/client/datascience/interactive-window/interactiveWindowTypes.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,7 +74,15 @@ export interface ICopyCode {
7474
source: string;
7575
}
7676

77+
export enum SysInfoReason {
78+
Start,
79+
Restart,
80+
Interrupt,
81+
New
82+
}
83+
7784
export interface IAddedSysInfo {
85+
type: SysInfoReason;
7886
id: string;
7987
sysInfoCell: ICell;
8088
}

src/client/datascience/types.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -380,3 +380,19 @@ export interface IPlotViewer extends IDisposable {
380380
addPlot(imageHtml: string) : Promise<void>;
381381
show(): Promise<void>;
382382
}
383+
384+
export interface ICellHash {
385+
line: number; // 1 based
386+
endLine: number; // 1 based and inclusive
387+
hash: string;
388+
executionCount: number;
389+
}
390+
391+
export interface IFileHashes {
392+
file: string;
393+
hashes: ICellHash[];
394+
}
395+
396+
export interface ICellHashProvider {
397+
getHashes() : IFileHashes[];
398+
}

0 commit comments

Comments
 (0)