-
Notifications
You must be signed in to change notification settings - Fork 1.2k
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
Changes from all commits
750f977
49c411d
94f9ac9
b205ea5
2737798
16d83ae
0e0dcbd
0215046
3466c81
f584b10
f01dc46
7e357ea
5d69a7a
2b80529
e8f1c3e
f5e3864
584b59b
9e1be55
bde0c96
2006f9b
e51a870
c184ef5
71dafc8
f6bfb8f
d8283b6
5403b83
b84b85f
00a958f
973bc15
94bdf9d
e0a3470
be6722c
d0af006
d089d70
c0c88db
a6b5591
7c4084f
ac3c342
e93fbe4
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
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 |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
Run by line for notebook cells minimal implementation. |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -22,6 +22,7 @@ import { | |
ICellHashListener, | ||
ICellHashProvider, | ||
IFileHashes, | ||
INotebook, | ||
INotebookExecutionLogger | ||
} from '../types'; | ||
|
||
|
@@ -148,25 +149,25 @@ export class CellHashProvider implements ICellHashProvider, INotebookExecutionLo | |
return lines; | ||
} | ||
|
||
public generateHashFileName(cell: ICell, expectedCount: number): string { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
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); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Why only first 12 characters? There was a problem hiding this comment. Choose a reason for hiding this commentThe 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)); | ||
|
||
|
@@ -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(''); | ||
|
@@ -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; | ||
|
@@ -406,3 +431,11 @@ export class CellHashProvider implements ICellHashProvider, INotebookExecutionLo | |
return traceFrame; | ||
} | ||
} | ||
|
||
export function getCellHashProvider(notebook: INotebook): ICellHashProvider | undefined { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Figured it made sense to centralize this here. There was a problem hiding this comment. Choose a reason for hiding this commentThe 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. There was a problem hiding this comment. Choose a reason for hiding this commentThe 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) There was a problem hiding this comment. Choose a reason for hiding this commentThe 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) There was a problem hiding this comment. Choose a reason for hiding this commentThe 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. There was a problem hiding this comment. Choose a reason for hiding this commentThe 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) There was a problem hiding this comment. Choose a reason for hiding this commentThe 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) There was a problem hiding this comment. Choose a reason for hiding this commentThe 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) There was a problem hiding this comment. Choose a reason for hiding this commentThe 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. There was a problem hiding this comment. Choose a reason for hiding this commentThe 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; | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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'; | ||
|
@@ -13,6 +13,7 @@ import { | |
CompletionItem, | ||
Event, | ||
EventEmitter, | ||
Hover, | ||
SignatureHelpContext, | ||
TextDocumentContentChangeEvent, | ||
Uri | ||
|
@@ -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, | ||
|
@@ -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() { | ||
|
@@ -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) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 { | ||
|
@@ -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]) { | ||
|
@@ -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}`] | ||
}; | ||
} | ||
} | ||
} | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Continue will say stop.
There was a problem hiding this comment.
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)