forked from DonJayamanne/pythonVSCode
-
Notifications
You must be signed in to change notification settings - Fork 1.2k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add ability to sync INotebookModel with cell text edits (#12092)
For #10496
- Loading branch information
1 parent
722d81b
commit feb74dc
Showing
9 changed files
with
234 additions
and
32 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,114 @@ | ||
// Copyright (c) Microsoft Corporation. All rights reserved. | ||
// Licensed under the MIT License. | ||
|
||
import { inject, injectable } from 'inversify'; | ||
import { TextDocument, TextDocumentChangeEvent } from 'vscode'; | ||
import type { NotebookCell, NotebookDocument } from '../../../../typings/vscode-proposed'; | ||
import { splitMultilineString } from '../../../datascience-ui/common'; | ||
import { IExtensionSingleActivationService } from '../../activation/types'; | ||
import { IDocumentManager, IVSCodeNotebook } from '../../common/application/types'; | ||
import { NativeNotebook } from '../../common/experiments/groups'; | ||
import { IDisposable, IDisposableRegistry, IExperimentsManager } from '../../common/types'; | ||
import { isNotebookCell } from '../../common/utils/misc'; | ||
import { traceError } from '../../logging'; | ||
import { INotebookEditorProvider, INotebookModel } from '../types'; | ||
|
||
@injectable() | ||
export class CellEditSyncService implements IExtensionSingleActivationService, IDisposable { | ||
private readonly disposables: IDisposable[] = []; | ||
private mappedDocuments = new WeakMap<TextDocument, { cellId: string; model: INotebookModel }>(); | ||
constructor( | ||
@inject(IDocumentManager) private readonly documentManager: IDocumentManager, | ||
@inject(IDisposableRegistry) disposableRegistry: IDisposableRegistry, | ||
@inject(IVSCodeNotebook) private readonly vscNotebook: IVSCodeNotebook, | ||
@inject(INotebookEditorProvider) private readonly editorProvider: INotebookEditorProvider, | ||
@inject(IExperimentsManager) private readonly experiment: IExperimentsManager | ||
) { | ||
disposableRegistry.push(this); | ||
} | ||
public dispose() { | ||
while (this.disposables.length) { | ||
this.disposables.pop()?.dispose(); //NOSONAR | ||
} | ||
} | ||
public async activate(): Promise<void> { | ||
if (!this.experiment.inExperiment(NativeNotebook.experiment)) { | ||
return; | ||
} | ||
this.documentManager.onDidChangeTextDocument(this.onDidChangeTextDocument, this, this.disposables); | ||
} | ||
|
||
private onDidChangeTextDocument(e: TextDocumentChangeEvent) { | ||
if (!isNotebookCell(e.document)) { | ||
return; | ||
} | ||
|
||
const details = this.getEditorsAndCell(e.document); | ||
if (!details) { | ||
return; | ||
} | ||
|
||
const cell = details.model.cells.find((item) => item.id === details.cellId); | ||
if (!cell) { | ||
traceError( | ||
`Syncing Cell Editor aborted, Unable to find corresponding ICell for ${e.document.uri.toString()}`, | ||
new Error('ICell not found') | ||
); | ||
return; | ||
} | ||
|
||
cell.data.source = splitMultilineString(e.document.getText()); | ||
} | ||
|
||
private getEditorsAndCell(cellDocument: TextDocument) { | ||
if (this.mappedDocuments.has(cellDocument)) { | ||
return this.mappedDocuments.get(cellDocument)!; | ||
} | ||
|
||
let document: NotebookDocument | undefined; | ||
let cell: NotebookCell | undefined; | ||
this.vscNotebook.notebookEditors.find((vscEditor) => { | ||
const found = vscEditor.document.cells.find((item) => item.document === cellDocument); | ||
if (found) { | ||
document = vscEditor.document; | ||
cell = found; | ||
} | ||
return !!found; | ||
}); | ||
|
||
if (!document) { | ||
traceError( | ||
`Syncing Cell Editor aborted, Unable to find corresponding Notebook for ${cellDocument.uri.toString()}`, | ||
new Error('Unable to find corresponding Notebook') | ||
); | ||
return; | ||
} | ||
if (!cell) { | ||
traceError( | ||
`Syncing Cell Editor aborted, Unable to find corresponding NotebookCell for ${cellDocument.uri.toString()}`, | ||
new Error('Unable to find corresponding NotebookCell') | ||
); | ||
return; | ||
} | ||
|
||
// Check if we have an editor associated with this document. | ||
const editor = this.editorProvider.editors.find((item) => item.file.toString() === document?.uri.toString()); | ||
if (!editor) { | ||
traceError( | ||
`Syncing Cell Editor aborted, Unable to find corresponding Editor for ${cellDocument.uri.toString()}`, | ||
new Error('Unable to find corresponding Editor') | ||
); | ||
return; | ||
} | ||
if (!editor.model) { | ||
traceError( | ||
`Syncing Cell Editor aborted, Unable to find corresponding INotebookModel for ${cellDocument.uri.toString()}`, | ||
new Error('No INotebookModel in editor') | ||
); | ||
return; | ||
} | ||
|
||
this.mappedDocuments.set(cellDocument, { model: editor.model, cellId: cell.metadata.custom!.cellId }); | ||
return this.mappedDocuments.get(cellDocument); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
99 changes: 99 additions & 0 deletions
99
src/test/datascience/notebook/cellEditSyncService.ds.test.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,99 @@ | ||
// Licensed under the MIT License. | ||
// Copyright (c) Microsoft Corporation. All rights reserved. | ||
|
||
// tslint:disable: no-var-requires no-require-imports no-invalid-this no-any | ||
|
||
import * as path from 'path'; | ||
import * as sinon from 'sinon'; | ||
import { Position, Range, Uri, window } from 'vscode'; | ||
import { IVSCodeNotebook } from '../../../client/common/application/types'; | ||
import { IDisposable } from '../../../client/common/types'; | ||
import { ICell, INotebookEditorProvider, INotebookModel } from '../../../client/datascience/types'; | ||
import { splitMultilineString } from '../../../datascience-ui/common'; | ||
import { IExtensionTestApi, waitForCondition } from '../../common'; | ||
import { EXTENSION_ROOT_DIR_FOR_TESTS } from '../../constants'; | ||
import { initialize } from '../../initialize'; | ||
import { | ||
canRunTests, | ||
closeNotebooksAndCleanUpAfterTests, | ||
createTemporaryNotebook, | ||
deleteAllCellsAndWait, | ||
insertPythonCellAndWait, | ||
swallowSavingOfNotebooks | ||
} from './helper'; | ||
|
||
suite('DataScience - VSCode Notebook (Cell Edit Syncing)', function () { | ||
this.timeout(10_000); | ||
|
||
const templateIPynb = path.join(EXTENSION_ROOT_DIR_FOR_TESTS, 'src', 'test', 'datascience', 'test.ipynb'); | ||
let testIPynb: Uri; | ||
let api: IExtensionTestApi; | ||
let editorProvider: INotebookEditorProvider; | ||
let vscNotebook: IVSCodeNotebook; | ||
const disposables: IDisposable[] = []; | ||
suiteSetup(async function () { | ||
this.timeout(10_000); | ||
api = await initialize(); | ||
if (!(await canRunTests())) { | ||
return this.skip(); | ||
} | ||
editorProvider = api.serviceContainer.get<INotebookEditorProvider>(INotebookEditorProvider); | ||
vscNotebook = api.serviceContainer.get<IVSCodeNotebook>(IVSCodeNotebook); | ||
}); | ||
suiteTeardown(() => closeNotebooksAndCleanUpAfterTests(disposables)); | ||
[true, false].forEach((isUntitled) => { | ||
suite(isUntitled ? 'Untitled Notebook' : 'Existing Notebook', () => { | ||
let model: INotebookModel; | ||
setup(async () => { | ||
sinon.restore(); | ||
await swallowSavingOfNotebooks(); | ||
|
||
// Don't use same file (due to dirty handling, we might save in dirty.) | ||
// Cuz we won't save to file, hence extension will backup in dirty file and when u re-open it will open from dirty. | ||
testIPynb = Uri.file(await createTemporaryNotebook(templateIPynb, disposables)); | ||
|
||
// Reset for tests, do this every time, as things can change due to config changes etc. | ||
const editor = isUntitled ? await editorProvider.createNew() : await editorProvider.open(testIPynb); | ||
model = editor.model!; | ||
await deleteAllCellsAndWait(); | ||
}); | ||
teardown(() => closeNotebooksAndCleanUpAfterTests(disposables)); | ||
|
||
async function assertTextInCell(cell: ICell, text: string) { | ||
await waitForCondition( | ||
async () => (cell.data.source as string[]).join('') === splitMultilineString(text).join(''), | ||
1_000, | ||
`Source; is not ${text}` | ||
); | ||
} | ||
test('Insert and edit cell', async () => { | ||
await insertPythonCellAndWait('HELLO'); | ||
const doc = vscNotebook.activeNotebookEditor?.document; | ||
const cellEditor1 = window.visibleTextEditors.find( | ||
(item) => doc?.cells.length && item.document.uri.toString() === doc?.cells[0].uri.toString() | ||
); | ||
await assertTextInCell(model.cells[0], 'HELLO'); | ||
|
||
// Edit cell. | ||
await new Promise((resolve) => | ||
cellEditor1?.edit((editor) => { | ||
editor.insert(new Position(0, 5), ' WORLD'); | ||
resolve(); | ||
}) | ||
); | ||
|
||
await assertTextInCell(model.cells[0], 'HELLO WORLD'); | ||
|
||
//Clear cell text. | ||
await new Promise((resolve) => | ||
cellEditor1?.edit((editor) => { | ||
editor.delete(new Range(0, 0, 0, 'HELLO WORLD'.length)); | ||
resolve(); | ||
}) | ||
); | ||
|
||
await assertTextInCell(model.cells[0], ''); | ||
}); | ||
}); | ||
}); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters