Skip to content

Run by line partial implementation #11608

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 39 commits into from
May 6, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
39 commits
Select commit Hold shift + click to select a range
750f977
Preliminary getting variables from the debugger
rchiodo Apr 30, 2020
49c411d
Implement updating as stepping
rchiodo Apr 30, 2020
94f9ac9
Get debugger and regular kernel to use same scripts
rchiodo Apr 30, 2020
b205ea5
Put back old code and create experiment to enable/disable this
rchiodo May 1, 2020
2737798
Implement experiment and begin on test for debugger
rchiodo May 1, 2020
16d83ae
Merge remote-tracking branch 'origin/master' into rchiodo/debugger_va…
rchiodo May 1, 2020
0e0dcbd
Merge remote-tracking branch 'origin/master' into rchiodo/debugger_va…
rchiodo May 1, 2020
0215046
Finish fixing tests
rchiodo May 1, 2020
3466c81
Add news entry
rchiodo May 1, 2020
f584b10
Create multiplexer
rchiodo May 2, 2020
f01dc46
Basic idea of glyph updating and control
rchiodo May 4, 2020
7e357ea
Forget the svgs
rchiodo May 4, 2020
5d69a7a
Rework to go through interactive path
rchiodo May 4, 2020
2b80529
Highlight line kinda showing up
rchiodo May 4, 2020
e8f1c3e
Fix variables to come out.
rchiodo May 5, 2020
f5e3864
Fix ip line sticking around too long
rchiodo May 5, 2020
584b59b
Colors and icon working for ip
rchiodo May 5, 2020
9e1be55
Implement F10 and F5
rchiodo May 5, 2020
bde0c96
Hover and step into icons
rchiodo May 5, 2020
2006f9b
Remove unnecessary variables and support step into
rchiodo May 5, 2020
e51a870
Update frame id for variable
rchiodo May 5, 2020
c184ef5
Merge remote-tracking branch 'origin/master' into rchiodo/debugger_cl…
rchiodo May 5, 2020
71dafc8
Fixup after merge
rchiodo May 5, 2020
f6bfb8f
Exclude everything glob for just my code
rchiodo May 5, 2020
d8283b6
Try fixing order for messages to debugger
rchiodo May 5, 2020
5403b83
Fix request problem and dark theme
rchiodo May 5, 2020
b84b85f
Add news entry
rchiodo May 5, 2020
00a958f
Some extra comments and fix sonar problems
rchiodo May 5, 2020
973bc15
Add loc strings and make constants for code icons
rchiodo May 6, 2020
94bdf9d
Sonar problems and removing unused code
rchiodo May 6, 2020
e0a3470
Change name to debugInfo
rchiodo May 6, 2020
be6722c
Try again to exclude the codicon files
rchiodo May 6, 2020
d0af006
Wrong base directory
rchiodo May 6, 2020
d089d70
Fixes from code review and run by line new icon
rchiodo May 6, 2020
c0c88db
Merge remote-tracking branch 'origin/master' into rchiodo/debugger_cl…
rchiodo May 6, 2020
a6b5591
Fix functional test failures
rchiodo May 6, 2020
7c4084f
Fix unit test failure (yay, caught a bug)
rchiodo May 6, 2020
ac3c342
Fix stupid sonar warnings
rchiodo May 6, 2020
e93fbe4
Fix some more failing tests
rchiodo May 6, 2020
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .sonarcloud.properties
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
sonar.sources=src/client,src/datascience-ui
sonar.exclusions=src/datascience-ui/**/codicon*.*
sonar.tests=src/test
sonar.cfamily.build-wrapper-output.bypass=true
sonar.cpd.exclusions=src/datascience-ui/**/redux/actions.ts,src/client/**/raw-kernel/rawKernel.ts,src/client/datascience/jupyter/*ariable*.ts
14 changes: 9 additions & 5 deletions build/webpack/loaders/externalizeDependencies.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,17 +2,21 @@
// Licensed under the MIT License.

const common = require('../common');
function replaceModule(contents, moduleName, quotes) {
const stringToSearch = `${quotes}${moduleName}${quotes}`;
const stringToReplaceWith = `${quotes}./node_modules/${moduleName}${quotes}`;
function replaceModule(prefixRegex, prefix, contents, moduleName, quotes) {
const stringToSearch = `${prefixRegex}${quotes}${moduleName}${quotes}`;
const stringToReplaceWith = `${prefix}${quotes}./node_modules/${moduleName}${quotes}`;
return contents.replace(new RegExp(stringToSearch, 'gm'), stringToReplaceWith);
}
// tslint:disable:no-default-export no-invalid-this
function default_1(source) {
common.nodeModulesToReplacePaths.forEach((moduleName) => {
if (source.indexOf(moduleName) > 0) {
source = replaceModule(source, moduleName, '"');
source = replaceModule(source, moduleName, "'");
source = replaceModule('import\\(', 'import(', source, moduleName, '"');
source = replaceModule('import\\(', 'import(', source, moduleName, "'");
source = replaceModule('require\\(', 'require(', source, moduleName, '"');
source = replaceModule('require\\(', 'require(', source, moduleName, "'");
source = replaceModule('from ', 'from ', source, moduleName, '"');
source = replaceModule('from ', 'from ', source, moduleName, "'");
}
});
return source;
Expand Down
1 change: 1 addition & 0 deletions news/3 Code Health/11607.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Run by line for notebook cells minimal implementation.
4 changes: 3 additions & 1 deletion package.nls.json
Original file line number Diff line number Diff line change
Expand Up @@ -489,5 +489,7 @@
"DataScience.unhandledMessage": "Unhandled kernel message from a widget: {0} : {1}",
"DataScience.qgridWidgetScriptVersionCompatibilityWarning": "Unable to load a compatible version of the widget 'qgrid'. Consider downgrading to version 1.1.1.",
"DataScience.kernelStarted": "Started kernel {0}",
"DataScience.libraryRequiredToLaunchJupyterKernelNotInstalledInterpreter": "Data Science library {1} is not installed in interpreter {0}. Install?"
"DataScience.libraryRequiredToLaunchJupyterKernelNotInstalledInterpreter": "Data Science library {1} is not installed in interpreter {0}. Install?",
"DataScience.runByLine": "Run by line",
"DataScience.continueRunByLine": "Stop"

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Continue will say stop.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yep that's on purpose.


In reply to: 420490258 [](ancestors = 420490258)

}
2 changes: 2 additions & 0 deletions src/client/common/utils/localize.ts
Original file line number Diff line number Diff line change
Expand Up @@ -925,6 +925,8 @@ export namespace DataScience {
);

export const kernelStarted = localize('DataScience.kernelStarted', 'Started kernel {0}.');
export const runByLine = localize('DataScience.runByLine', 'Run by line');
export const continueRunByLine = localize('DataScience.continueRunByLine', 'Stop');
}

export namespace DebugConfigStrings {
Expand Down
2 changes: 2 additions & 0 deletions src/client/datascience/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -404,6 +404,8 @@ export namespace Identifiers {
export const OLD_VARIABLES = 'OLD_VARIABLES';
export const KERNEL_VARIABLES = 'KERNEL_VARIABLES';
export const DEBUGGER_VARIABLES = 'DEBUGGER_VARIABLES';
export const MULTIPLEXING_DEBUGSERVICE = 'MULTIPLEXING_DEBUGSERVICE';
export const RUN_BY_LINE_DEBUGSERVICE = 'RUN_BY_LINE_DEBUGSERVICE';
}

export namespace CodeSnippits {
Expand Down
101 changes: 67 additions & 34 deletions src/client/datascience/editor-integration/cellhashprovider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import {
ICellHashListener,
ICellHashProvider,
IFileHashes,
INotebook,
INotebookExecutionLogger
} from '../types';

Expand Down Expand Up @@ -148,25 +149,25 @@ export class CellHashProvider implements ICellHashProvider, INotebookExecutionLo
return lines;
}

public generateHashFileName(cell: ICell, expectedCount: number): string {
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

generateHashFileName [](start = 11, length = 20)

Not really using this now, but theoretically once the debugpy problems are fixed, this file name will be used to specify what code is 'just my code'

// First get the true lines from the cell
const { stripped } = this.extractStrippedLines(cell);

// Then use that to make a hash value
const hashedCode = stripped.join('');
const hash = hashjs.sha1().update(hashedCode).digest('hex').substr(0, 12);

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why only first 12 characters?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's what jupyter uses to generate the file name internally.

return `<ipython-input-${expectedCount}-${hash}>`;
}

// tslint:disable-next-line: cyclomatic-complexity
public async addCellHash(cell: ICell, expectedCount: number): Promise<void> {
// Find the text document that matches. We need more information than
// the add code gives us
const doc = this.documentManager.textDocuments.find((d) => this.fileSystem.arePathsSame(d.fileName, cell.file));
if (doc) {
// Compute the code that will really be sent to jupyter
const lines = splitMultilineString(cell.data.source);
const stripped = this.extractExecutableLines(cell);

// Figure out our true 'start' line. This is what we need to tell the debugger is
// actually the start of the code as that's what Jupyter will be getting.
let trueStartLine = cell.line;
for (let i = 0; i < stripped.length; i += 1) {
if (stripped[i] !== lines[i]) {
trueStartLine += i + 1;
break;
}
}
const { stripped, trueStartLine } = this.extractStrippedLines(cell);

const line = doc.lineAt(trueStartLine);
const endLine = doc.lineAt(Math.min(trueStartLine + stripped.length - 1, doc.lineCount - 1));

Expand All @@ -181,28 +182,6 @@ export class CellHashProvider implements ICellHashProvider, INotebookExecutionLo
const startOffset = doc.offsetAt(new Position(cell.line, 0));
const endOffset = doc.offsetAt(endLine.rangeIncludingLineBreak.end);

// Jupyter also removes blank lines at the end. Make sure only one
let lastLinePos = stripped.length - 1;
let nextToLastLinePos = stripped.length - 2;
while (nextToLastLinePos > 0) {
const lastLine = stripped[lastLinePos];
const nextToLastLine = stripped[nextToLastLinePos];
if (
(lastLine.length === 0 || lastLine === '\n') &&
(nextToLastLine.length === 0 || nextToLastLine === '\n')
) {
stripped.splice(lastLinePos, 1);
lastLinePos -= 1;
nextToLastLinePos -= 1;
} else {
break;
}
}
// Make sure the last line with actual content ends with a linefeed
if (!stripped[lastLinePos].endsWith('\n') && stripped[lastLinePos].length > 0) {
stripped[lastLinePos] = `${stripped[lastLinePos]}\n`;
}

// Compute the runtime line and adjust our cell/stripped source for debugging
const runtimeLine = this.adjustRuntimeForDebugging(cell, stripped, startOffset, endOffset);
const hashedCode = stripped.join('');
Expand Down Expand Up @@ -291,6 +270,52 @@ export class CellHashProvider implements ICellHashProvider, INotebookExecutionLo
}
}

private extractStrippedLines(cell: ICell): { stripped: string[]; trueStartLine: number } {
// Compute the code that will really be sent to jupyter
const lines = splitMultilineString(cell.data.source);
const stripped = this.extractExecutableLines(cell);

// Figure out our true 'start' line. This is what we need to tell the debugger is
// actually the start of the code as that's what Jupyter will be getting.
let trueStartLine = cell.line;
for (let i = 0; i < stripped.length; i += 1) {
if (stripped[i] !== lines[i]) {
trueStartLine += i + 1;
break;
}
}

// Find the first non blank line
let firstNonBlankIndex = 0;
while (firstNonBlankIndex < stripped.length && stripped[firstNonBlankIndex].trim().length === 0) {
firstNonBlankIndex += 1;
}

// Jupyter also removes blank lines at the end. Make sure only one
let lastLinePos = stripped.length - 1;
let nextToLastLinePos = stripped.length - 2;
while (nextToLastLinePos > 0) {
const lastLine = stripped[lastLinePos];
const nextToLastLine = stripped[nextToLastLinePos];
if (
(lastLine.length === 0 || lastLine === '\n') &&
(nextToLastLine.length === 0 || nextToLastLine === '\n')
) {
stripped.splice(lastLinePos, 1);
lastLinePos -= 1;
nextToLastLinePos -= 1;
} else {
break;
}
}
// Make sure the last line with actual content ends with a linefeed
if (!stripped[lastLinePos].endsWith('\n') && stripped[lastLinePos].length > 0) {
stripped[lastLinePos] = `${stripped[lastLinePos]}\n`;
}

return { stripped, trueStartLine };
}

private handleContentChange(docText: string, c: TextDocumentContentChangeEvent, hashes: IRangedCellHash[]) {
// First compute the number of lines that changed
const lineDiff = c.range.start.line - c.range.end.line + c.text.split('\n').length - 1;
Expand Down Expand Up @@ -406,3 +431,11 @@ export class CellHashProvider implements ICellHashProvider, INotebookExecutionLo
return traceFrame;
}
}

export function getCellHashProvider(notebook: INotebook): ICellHashProvider | undefined {
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

getCellHashProvider [](start = 16, length = 19)

Figured it made sense to centralize this here.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If you just need the loggers looks like this should just take INotebookExecutionLogger[] instead of the entire notebook. So it doesn't have to expose the whole interface.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Then the caller has to know that it comes from an array of loggers. Two pieces of information isntead of one. They have to have a notebook, why tell them the cell hash provider is on the loggers?


In reply to: 420474295 [](ancestors = 420474295)

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As in the CellHashProvider knows it's in the logger list for a notebook, nobody else needs to know this. They just know it's associated with a notebook. We might change how the cell hash provider is associated with a notebook in the future, but we'd likely still have it associated with a notebook itself.


In reply to: 420482187 [](ancestors = 420482187,420474295)

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

My objection here is more how much is on the INotebook interface. The CellHashProvider could just call notebook.Dispose() here. Or notebook.restartKernel(). I feel like that's something we do a lot, especially with INotebook and IJupyterServer.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

But then the caller would have to understand about loggers. I don't think that scopes it down at all. I think it does the opposite. CellHashProvider knows it's a logger on a notebook. Nothing else needs to know that. However other things need some way to get the provider. I suppose other ways we could have done this would be to add a function to the notebook to retrieve it (that seems worse), add some sort of helper class that all you pass in is the notebook identity (that doesn't sound too bad), or this way.


In reply to: 420884909 [](ancestors = 420884909)

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe we should split the notebook interface then. Something that's smaller (or readonly).


In reply to: 420881550 [](ancestors = 420881550)

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LIke IExecutableNotebook and INotebook where all of the state modifying items are on IExecutableNotebook.


In reply to: 420915749 [](ancestors = 420915749,420881550)

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't know if we need to do it now, but if you remember the question that I was asking at the end of the SOLID talk about oversized interfaces, INotebook was totally one of the interfaces that I was thinking about. I think part of the advice there was that breaking up the interface might be a good first step to at least make it hard in code to mess with something that should not be messed with. Splitting that might be part of a bigger overall code-health fix, so that might be better to not mess with in this PR.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

With Ian on this, we're passing something that's too broad.

const logger = notebook.getLoggers().find((f) => f instanceof CellHashProvider);
if (logger) {
// tslint:disable-next-line: no-any
return (logger as any) as ICellHashProvider;
}
}
12 changes: 8 additions & 4 deletions src/client/datascience/interactive-common/debugListener.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,12 @@
'use strict';
import '../../common/extensions';

import { inject, injectable } from 'inversify';
import { inject, injectable, named } from 'inversify';
import { DebugSession, Event, EventEmitter } from 'vscode';

import { IDebugService } from '../../common/application/types';
import { noop } from '../../common/utils/misc';
import { IInteractiveWindowListener } from '../types';
import { Identifiers } from '../constants';
import { IInteractiveWindowListener, IJupyterDebugService } from '../types';
import { InteractiveWindowMessages } from './interactiveWindowTypes';

// tslint:disable: no-any
Expand All @@ -18,7 +18,11 @@ export class DebugListener implements IInteractiveWindowListener {
message: string;
payload: any;
}>();
constructor(@inject(IDebugService) private debugService: IDebugService) {
constructor(
@inject(IJupyterDebugService)
@named(Identifiers.MULTIPLEXING_DEBUGSERVICE)
private debugService: IJupyterDebugService
) {
this.debugService.onDidChangeActiveDebugSession(this.onChangeDebugSession.bind(this));
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
'use strict';
import '../../../common/extensions';

import { inject, injectable } from 'inversify';
import { inject, injectable, named } from 'inversify';
import * as monacoEditor from 'monaco-editor/esm/vs/editor/editor.api';
import * as path from 'path';
import * as uuid from 'uuid/v4';
Expand All @@ -13,6 +13,7 @@ import {
CompletionItem,
Event,
EventEmitter,
Hover,
SignatureHelpContext,
TextDocumentContentChangeEvent,
Uri
Expand All @@ -31,7 +32,14 @@ import { HiddenFileFormatString } from '../../../constants';
import { IInterpreterService, PythonInterpreter } from '../../../interpreter/contracts';
import { sendTelemetryWhenDone } from '../../../telemetry';
import { Identifiers, Settings, Telemetry } from '../../constants';
import { ICell, IInteractiveWindowListener, INotebook, INotebookCompletion, INotebookProvider } from '../../types';
import {
ICell,
IInteractiveWindowListener,
IJupyterVariables,
INotebook,
INotebookCompletion,
INotebookProvider
} from '../../types';
import {
ICancelIntellisenseRequest,
IInteractiveWindowMapping,
Expand Down Expand Up @@ -80,7 +88,8 @@ export class IntellisenseProvider implements IInteractiveWindowListener {
@inject(IFileSystem) private fileSystem: IFileSystem,
@inject(INotebookProvider) private notebookProvider: INotebookProvider,
@inject(IInterpreterService) private interpreterService: IInterpreterService,
@inject(ILanguageServerCache) private languageServerCache: ILanguageServerCache
@inject(ILanguageServerCache) private languageServerCache: ILanguageServerCache,
@inject(IJupyterVariables) @named(Identifiers.ALL_VARIABLES) private variableProvider: IJupyterVariables
) {}

public dispose() {
Expand Down Expand Up @@ -244,16 +253,23 @@ export class IntellisenseProvider implements IInteractiveWindowListener {
}
protected async provideHover(
position: monacoEditor.Position,
wordAtPosition: string | undefined,
cellId: string,
token: CancellationToken
): Promise<monacoEditor.languages.Hover> {
const [languageServer, document] = await Promise.all([this.getLanguageServer(), this.getDocument()]);
if (languageServer && document) {
const [languageServer, document, variableHover] = await Promise.all([
this.getLanguageServer(),
this.getDocument(),
this.getVariableHover(wordAtPosition)
]);
if (!variableHover && languageServer && document) {
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If there's a word under the cursor and the debugger (or kernel) knows about it, it takes precedence over normal hover.

const docPos = document.convertToDocumentPosition(cellId, position.lineNumber, position.column);
const result = await languageServer.provideHover(document, docPos, token);
if (result) {
return convertToMonacoHover(result);
}
} else if (variableHover) {
return convertToMonacoHover(variableHover);
}

return {
Expand Down Expand Up @@ -435,7 +451,7 @@ export class IntellisenseProvider implements IInteractiveWindowListener {
const cancelSource = new CancellationTokenSource();
this.cancellationSources.set(request.requestId, cancelSource);
this.postTimedResponse(
[this.provideHover(request.position, request.cellId, cancelSource.token)],
[this.provideHover(request.position, request.wordAtPosition, request.cellId, cancelSource.token)],
InteractiveWindowMessages.ProvideHoverResponse,
(h) => {
if (h && h[0]) {
Expand Down Expand Up @@ -731,4 +747,18 @@ export class IntellisenseProvider implements IInteractiveWindowListener {
? this.notebookProvider.getOrCreateNotebook({ identity: this.notebookIdentity, getOnly: true })
: undefined;
}

private async getVariableHover(wordAtPosition: string | undefined): Promise<Hover | undefined> {
if (wordAtPosition) {
const notebook = await this.getNotebook();
if (notebook) {
const variable = await this.variableProvider.getMatchingVariable(notebook, wordAtPosition);
if (variable && variable.value && variable.name) {
return {
contents: [`${variable.name} : ${variable.value}`]
};
}
}
}
}
}
Loading