From 8f1fd7b865fb7f8c61ed416270a681c4c74180ad Mon Sep 17 00:00:00 2001 From: earthastronaut <> Date: Fri, 19 Jun 2020 15:11:20 -0600 Subject: [PATCH 01/26] refactored ICellRange definition to type.ts since it's now used across multiple modules --- src/client/datascience/cellFactory.ts | 9 +-------- .../editor-integration/codeLensFactory.ts | 7 ++++++- src/client/datascience/types.ts | 13 +++++++++++++ 3 files changed, 20 insertions(+), 9 deletions(-) diff --git a/src/client/datascience/cellFactory.ts b/src/client/datascience/cellFactory.ts index 53459f382e98..b8df01e41317 100644 --- a/src/client/datascience/cellFactory.ts +++ b/src/client/datascience/cellFactory.ts @@ -12,7 +12,7 @@ import { IDataScienceSettings, Resource } from '../common/types'; import { noop } from '../common/utils/misc'; import { CellMatcher } from './cellMatcher'; import { Identifiers } from './constants'; -import { CellState, ICell } from './types'; +import { CellState, ICell, ICellRange } from './types'; function generateCodeCell( code: string[], @@ -109,13 +109,6 @@ export function hasCells(document: TextDocument, settings?: IDataScienceSettings return false; } -// CellRange is used as the basis for creating new ICells. We only use it in this file. -export interface ICellRange { - range: Range; - title: string; - cell_type: string; -} - export function generateCellsFromString(source: string, settings?: IDataScienceSettings): ICell[] { const lines: string[] = source.splitLines({ trim: false, removeEmptyEntries: false }); diff --git a/src/client/datascience/editor-integration/codeLensFactory.ts b/src/client/datascience/editor-integration/codeLensFactory.ts index ec651d70d5b2..d6ea1ddc02ac 100644 --- a/src/client/datascience/editor-integration/codeLensFactory.ts +++ b/src/client/datascience/editor-integration/codeLensFactory.ts @@ -10,7 +10,7 @@ import { IFileSystem } from '../../common/platform/types'; import { IConfigurationService, Resource } from '../../common/types'; import * as localize from '../../common/utils/localize'; import { noop } from '../../common/utils/misc'; -import { generateCellRangesFromDocument, ICellRange } from '../cellFactory'; +import { generateCellRangesFromDocument } from '../cellFactory'; import { CodeLensCommands, Commands, Identifiers } from '../constants'; import { INotebookIdentity, @@ -20,6 +20,7 @@ import { import { ICell, ICellHashProvider, + ICellRange, ICodeLensFactory, IFileHashes, IInteractiveWindowListener, @@ -41,6 +42,7 @@ type CodeLensCacheData = { */ @injectable() export class CodeLensFactory implements ICodeLensFactory, IInteractiveWindowListener { + public cells: ICellRange[]; private updateEvent: EventEmitter = new EventEmitter(); // tslint:disable-next-line: no-any private postEmitter: EventEmitter<{ message: string; payload: any }> = new EventEmitter<{ @@ -60,6 +62,7 @@ export class CodeLensFactory implements ICodeLensFactory, IInteractiveWindowList @inject(IFileSystem) private fileSystem: IFileSystem, @inject(IDocumentManager) private documentManager: IDocumentManager ) { + this.cells = []; this.documentManager.onDidCloseTextDocument(this.onClosedDocument.bind(this)); this.configService.getSettings(undefined).onDidChange(this.onChangedSettings.bind(this)); } @@ -204,6 +207,8 @@ export class CodeLensFactory implements ICodeLensFactory, IInteractiveWindowList }); } + this.cells = cache.cellRanges; + return [...cache.documentLenses, ...cache.gotoCellLens]; } diff --git a/src/client/datascience/types.ts b/src/client/datascience/types.ts index 80808fb1b17d..62afb3e686ec 100644 --- a/src/client/datascience/types.ts +++ b/src/client/datascience/types.ts @@ -632,6 +632,7 @@ export interface ICodeWatcher { export const ICodeLensFactory = Symbol('ICodeLensFactory'); export interface ICodeLensFactory { + cells: ICellRange[]; updateRequired: Event; createCodeLenses(document: TextDocument): CodeLens[]; } @@ -654,6 +655,18 @@ export interface ICell { extraLines?: number[]; } +// CellRange is used as the basis for creating new ICells. +// Was only intended to aggregate together ranges to create an ICell +// However the "range" aspect is useful when working with plain text document +// Ultimately, it would probably be ideal to be ICell and change line to range. +// Specificially see how this is being used for the ICodeLensFactory to +// provide cells for the CodeWatcher to use. +export interface ICellRange { + range: Range; + title: string; + cell_type: string; +} + export interface IInteractiveWindowInfo { cellCount: number; undoCount: number; From fd5e8652f6a472b6754899633ed8af52c2a9c459 Mon Sep 17 00:00:00 2001 From: earthastronaut <> Date: Sat, 20 Jun 2020 10:52:06 -0600 Subject: [PATCH 02/26] add command insertCellBelowPosition * add method insertCell to CodeWatcher which inserts at current line * add method insertCellBelowPosition which inserts below current selection * link insertCellBelowPosition to command python.datascience.insertCellBelowPosition --- package.json | 16 ++++ package.nls.json | 1 + src/client/common/application/commands.ts | 1 + .../datascience/commands/commandRegistry.ts | 10 +++ src/client/datascience/constants.ts | 1 + .../editor-integration/codewatcher.ts | 77 ++++++++++++++++++- src/client/datascience/types.ts | 1 + .../codewatcher.unit.test.ts | 77 ++++++++++++++++++- 8 files changed, 182 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index 437bbe4d3731..c1655fa8a242 100644 --- a/package.json +++ b/package.json @@ -503,6 +503,11 @@ "title": "%python.command.python.datascience.debugcontinue.title%", "category": "Python" }, + { + "command": "python.datascience.insertCellBelowPosition", + "title": "%python.command.python.datascience.insertCellBelowPosition.title%", + "category": "Python" + }, { "command": "python.datascience.runcurrentcelladvance", "title": "%python.command.python.datascience.runcurrentcelladvance.title%", @@ -777,6 +782,11 @@ "command": "python.datascience.runallcells", "group": "Python2" }, + { + "when": "editorFocus && editorLangId == python && python.datascience.hascodecells && python.datascience.featureenabled && !notebookEditorFocused", + "command": "python.datascience.insertCellBelowPosition", + "group": "Python2" + }, { "when": "editorFocus && editorLangId == python && python.datascience.hascodecells && python.datascience.featureenabled && !notebookEditorFocused", "command": "python.datascience.runcurrentcell", @@ -942,6 +952,12 @@ "category": "Python", "when": "config.noExists" }, + { + "command": "python.datascience.insertCellBelowPosition", + "title": "%python.command.python.datascience.insertCellBelowPosition.title%", + "category": "Python", + "when": "python.datascience.hascodecells && python.datascience.featureenabled && python.datascience.ispythonornativeactive" + }, { "command": "python.datascience.runcurrentcell", "title": "%python.command.python.datascience.runcurrentcell.title%", diff --git a/package.nls.json b/package.nls.json index f3ce4c1222e7..a7732bc8b7c8 100644 --- a/package.nls.json +++ b/package.nls.json @@ -64,6 +64,7 @@ "python.command.python.datascience.runcurrentcelladvance.title": "Run Current Cell And Advance", "python.command.python.datascience.execSelectionInteractive.title": "Run Selection/Line in Python Interactive Window", "python.command.python.datascience.runcell.title": "Run Cell", + "python.command.python.datascience.insertCellBelowPosition.title": "Insert Cell Below Position", "python.command.python.datascience.showhistorypane.title": "Show Python Interactive Window", "python.command.python.datascience.selectjupyteruri.title": "Specify local or remote Jupyter server for connections", "python.command.python.datascience.selectjupytercommandline.title": "Specify Jupyter command line arguments", diff --git a/src/client/common/application/commands.ts b/src/client/common/application/commands.ts index d18b5cf737a5..63faf999322b 100644 --- a/src/client/common/application/commands.ts +++ b/src/client/common/application/commands.ts @@ -170,6 +170,7 @@ export interface ICommandNameArgumentTypeMapping extends ICommandNameWithoutArgu [DSCommands.DebugStop]: []; [DSCommands.DebugContinue]: []; [DSCommands.RunCurrentCellAndAddBelow]: [string]; + [DSCommands.InsertCellBelowPosition]: []; [DSCommands.ScrollToCell]: [string, string]; [DSCommands.ViewJupyterOutput]: []; [DSCommands.ExportAsPythonScript]: [INotebookModel]; diff --git a/src/client/datascience/commands/commandRegistry.ts b/src/client/datascience/commands/commandRegistry.ts index b1be73af9efc..659f6ff841b3 100644 --- a/src/client/datascience/commands/commandRegistry.ts +++ b/src/client/datascience/commands/commandRegistry.ts @@ -62,6 +62,7 @@ export class CommandRegistry implements IDisposable { this.registerCommand(Commands.ExecSelectionInInteractiveWindow, this.runSelectionOrLine); this.registerCommand(Commands.RunAllCellsAbove, this.runAllCellsAbove); this.registerCommand(Commands.RunCellAndAllBelow, this.runCellAndAllBelow); + this.registerCommand(Commands.InsertCellBelowPosition, this.insertCellBelowPosition); this.registerCommand(Commands.RunAllCellsAbovePalette, this.runAllCellsAboveFromCursor); this.registerCommand(Commands.RunCellAndAllBelowPalette, this.runCellAndAllBelowFromCursor); this.registerCommand(Commands.RunToLine, this.runToLine); @@ -305,6 +306,15 @@ export class CommandRegistry implements IDisposable { } } + private async insertCellBelowPosition(): Promise { + const activeCodeWatcher = this.getCurrentCodeWatcher(); + if (activeCodeWatcher) { + return activeCodeWatcher.insertCellBelowPosition(); + } else { + return Promise.resolve(); + } + } + private async runAllCellsAboveFromCursor(): Promise { const currentCodeLens = this.getCurrentCodeLens(); if (currentCodeLens) { diff --git a/src/client/datascience/constants.ts b/src/client/datascience/constants.ts index cbe375a301b6..ff43756eb07e 100644 --- a/src/client/datascience/constants.ts +++ b/src/client/datascience/constants.ts @@ -77,6 +77,7 @@ export namespace Commands { export const DebugContinue = 'python.datascience.debugcontinue'; export const DebugStop = 'python.datascience.debugstop'; export const RunCurrentCellAndAddBelow = 'python.datascience.runcurrentcellandaddbelow'; + export const InsertCellBelowPosition = 'python.datascience.insertCellBelowPosition'; export const ScrollToCell = 'python.datascience.scrolltocell'; export const CreateNewNotebook = 'python.datascience.createnewnotebook'; export const ViewJupyterOutput = 'python.datascience.viewJupyterOutput'; diff --git a/src/client/datascience/editor-integration/codewatcher.ts b/src/client/datascience/editor-integration/codewatcher.ts index 7cf4bfe30067..d461e1e84469 100644 --- a/src/client/datascience/editor-integration/codewatcher.ts +++ b/src/client/datascience/editor-integration/codewatcher.ts @@ -23,7 +23,13 @@ import { captureTelemetry, sendTelemetryEvent } from '../../telemetry'; import { ICodeExecutionHelper } from '../../terminals/types'; import { CellMatcher } from '../cellMatcher'; import { Commands, Identifiers, Telemetry } from '../constants'; -import { ICodeLensFactory, ICodeWatcher, IDataScienceErrorHandler, IInteractiveWindowProvider } from '../types'; +import { + ICellRange, + ICodeLensFactory, + ICodeWatcher, + IDataScienceErrorHandler, + IInteractiveWindowProvider +} from '../types'; @injectable() export class CodeWatcher implements ICodeWatcher { @@ -359,6 +365,56 @@ export class CodeWatcher implements ICodeWatcher { ); } + public async insertCellBelowPosition(): Promise { + const editor = this.documentManager.activeTextEditor; + if (editor && editor.selection.end) { + return this.insertCell(editor, editor.selection.end.line + 1); + } + } + + public async insertCellBelowCurrent(): Promise { + const editor = this.documentManager.activeTextEditor; + const cell = this.getCellFromPosition(); + if (editor && cell) { + return this.insertCell(editor, cell.range.end.line + 1); + } + } + + public async insertCellAboveCurrent(): Promise { + const editor = this.documentManager.activeTextEditor; + const cell = this.getCellFromPosition(); + if (editor && cell) { + return this.insertCell(editor, cell.range.start.line); + } + } + + private async insertCell(editor: TextEditor, line: number): Promise { + // insertCell + // + // Inserts a cell at current line defined as two new lines and then + // moves cursor to within the cell. + // ``` + // # %% + // + // ``` + // + const cellDelineator = this.getDefaultCellMarker(editor.document.uri); + let newCell = `${cellDelineator}\n\n`; + if (line >= editor.document.lineCount) { + newCell = `\n${cellDelineator}\n`; + } + + const cellStartPosition = new Position(line, 0); + const newCursorPosition = new Position(line + 1, 0); + + editor.edit((editBuilder) => { + editBuilder.insert(cellStartPosition, newCell); + this.codeLensUpdatedEvent.fire(); + }); + + editor.selection = new Selection(newCursorPosition, newCursorPosition); + } + private getDefaultCellMarker(resource: Resource): string { return ( this.configService.getSettings(resource).datascience.defaultCellMarker || Identifiers.DefaultCodeCellMarker @@ -464,6 +520,25 @@ export class CodeWatcher implements ICodeWatcher { } } + private getCellIndex(position?: Position): number | undefined { + if (!position) { + const editor = this.documentManager.activeTextEditor; + if (editor && editor.selection) { + position = editor.selection.start; + } + } + if (position) { + return this.codeLensFactory.cells.findIndex((cell) => position && cell.range.contains(position)); + } + } + + private getCellFromPosition(position?: Position): ICellRange | undefined { + const index = this.getCellIndex(position); + if (index !== undefined) { + return this.codeLensFactory.cells[index]; + } + } + private getCurrentCellLens(pos: Position): CodeLens | undefined { return this.codeLenses.find( (l) => l.range.contains(pos) && l.command !== undefined && l.command.command === Commands.RunCell diff --git a/src/client/datascience/types.ts b/src/client/datascience/types.ts index 62afb3e686ec..64144cc09d3a 100644 --- a/src/client/datascience/types.ts +++ b/src/client/datascience/types.ts @@ -627,6 +627,7 @@ export interface ICodeWatcher { debugFileInteractive(): Promise; addEmptyCellToBottom(): Promise; runCurrentCellAndAddBelow(): Promise; + insertCellBelowPosition(): Promise; debugCurrentCell(): Promise; } diff --git a/src/test/datascience/editor-integration/codewatcher.unit.test.ts b/src/test/datascience/editor-integration/codewatcher.unit.test.ts index f7124d23bfea..4789b731acd0 100644 --- a/src/test/datascience/editor-integration/codewatcher.unit.test.ts +++ b/src/test/datascience/editor-integration/codewatcher.unit.test.ts @@ -5,7 +5,7 @@ // Disable whitespace / multiline as we use that to pass in our fake file strings import { expect } from 'chai'; import * as TypeMoq from 'typemoq'; -import { CancellationTokenSource, CodeLens, Disposable, Range, Selection, TextEditor, Uri } from 'vscode'; +import { CancellationTokenSource, CodeLens, Disposable, Range, Selection, TextDocument, TextEditor, Uri } from 'vscode'; import { ICommandManager, @@ -30,7 +30,10 @@ import { import { IServiceContainer } from '../../../client/ioc/types'; import { ICodeExecutionHelper } from '../../../client/terminals/types'; import { MockAutoSelectionService } from '../../mocks/autoSelector'; +import { MockDocument } from '../mockDocument'; +import { MockDocumentManager } from '../mockDocumentManager'; import { MockPythonSettings } from '../mockPythonSettings'; +import { MockEditor } from '../mockTextEditor'; import { createDocument } from './helpers'; //tslint:disable:no-any @@ -41,6 +44,7 @@ suite('DataScience Code Watcher Unit Tests', () => { let notebookProvider: TypeMoq.IMock; let activeInteractiveWindow: TypeMoq.IMock; let documentManager: TypeMoq.IMock; + let mockDocumentManager: MockDocumentManager; let commandManager: TypeMoq.IMock; let textEditor: TypeMoq.IMock; let fileSystem: TypeMoq.IMock; @@ -70,6 +74,7 @@ suite('DataScience Code Watcher Unit Tests', () => { commandManager = TypeMoq.Mock.ofType(); debugService = TypeMoq.Mock.ofType(); vscodeNotebook = TypeMoq.Mock.ofType(); + mockDocumentManager = new MockDocumentManager(); // Setup default settings pythonSettings.datascience = { @@ -1025,4 +1030,74 @@ testing2`; // Command tests override getText, so just need the ranges here activeInteractiveWindow.verifyAll(); document.verifyAll(); }); + + test('Test insert cell below position', async () => { + const fileName = Uri.file('test.py').fsPath; + const version = 1; + const inputText = `testing0 +#%% +testing1 +#%% +testing2`; + const document = createDocument(inputText, fileName, version, TypeMoq.Times.atLeastOnce(), true); + + codeWatcher.setDocument(document.object); + + // For this test we need to set up a document selection point + // TypeMoq does not play well with setting properties on editor + const mockDocument = mockDocumentManager.addDocument(inputText, fileName); + const mockTextEditor = new MockEditor(mockDocumentManager, mockDocument); + documentManager.reset(); + documentManager.setup((dm) => dm.activeTextEditor).returns(() => mockTextEditor); + mockTextEditor.selection = new Selection(0, 4, 0, 4); + + await codeWatcher.insertCellBelowPosition(); + + expect(mockTextEditor.document.getText()).to.equal(`testing0 +# %% + +#%% +testing1 +#%% +testing2`); + expect(mockTextEditor.selection.start.line).to.equal(2); + expect(mockTextEditor.selection.start.character).to.equal(0); + expect(mockTextEditor.selection.end.line).to.equal(2); + expect(mockTextEditor.selection.end.character).to.equal(0); + }); + + test('Test insert cell below position at end', async () => { + const fileName = Uri.file('test.py').fsPath; + const version = 1; + const inputText = `testing0 +#%% +testing1 +#%% +testing2`; + const document = createDocument(inputText, fileName, version, TypeMoq.Times.atLeastOnce(), true); + + codeWatcher.setDocument(document.object); + + // For this test we need to set up a document selection point + // TypeMoq does not play well with setting properties on editor + const mockDocument = mockDocumentManager.addDocument(inputText, fileName); + const mockTextEditor = new MockEditor(mockDocumentManager, mockDocument); + documentManager.reset(); + documentManager.setup((dm) => dm.activeTextEditor).returns(() => mockTextEditor); + mockTextEditor.selection = new Selection(4, 0, 5, 8); + + await codeWatcher.insertCellBelowPosition(); + + expect(mockTextEditor.document.getText()).to.equal(`testing0 +#%% +testing1 +#%% +testing2 +# %% +`); + expect(mockTextEditor.selection.start.line).to.equal(7); + expect(mockTextEditor.selection.start.character).to.equal(0); + expect(mockTextEditor.selection.end.line).to.equal(7); + expect(mockTextEditor.selection.end.character).to.equal(0); + }); }); From 127e2dfb829dd7f3b753976db02ff785d3aa371b Mon Sep 17 00:00:00 2001 From: earthastronaut <> Date: Sat, 20 Jun 2020 11:44:07 -0600 Subject: [PATCH 03/26] add command insertCellBelow * add method insertCellBelow to the CodeWatcher * link insertCellBelow to python.datascience.insertCellBelow This will insert a cell below the last cell of the current selection --- package.json | 16 ++ package.nls.json | 1 + src/client/common/application/commands.ts | 1 + .../datascience/commands/commandRegistry.ts | 10 ++ src/client/datascience/constants.ts | 1 + .../editor-integration/codewatcher.ts | 24 ++- src/client/datascience/types.ts | 1 + .../codewatcher.unit.test.ts | 160 ++++++++++++++---- 8 files changed, 175 insertions(+), 39 deletions(-) diff --git a/package.json b/package.json index c1655fa8a242..883bc9c040e3 100644 --- a/package.json +++ b/package.json @@ -508,6 +508,11 @@ "title": "%python.command.python.datascience.insertCellBelowPosition.title%", "category": "Python" }, + { + "command": "python.datascience.insertCellBelow", + "title": "%python.command.python.datascience.insertCellBelow.title%", + "category": "Python" + }, { "command": "python.datascience.runcurrentcelladvance", "title": "%python.command.python.datascience.runcurrentcelladvance.title%", @@ -787,6 +792,11 @@ "command": "python.datascience.insertCellBelowPosition", "group": "Python2" }, + { + "when": "editorFocus && editorLangId == python && python.datascience.hascodecells && python.datascience.featureenabled && !notebookEditorFocused", + "command": "python.datascience.insertCellBelow", + "group": "Python2" + }, { "when": "editorFocus && editorLangId == python && python.datascience.hascodecells && python.datascience.featureenabled && !notebookEditorFocused", "command": "python.datascience.runcurrentcell", @@ -958,6 +968,12 @@ "category": "Python", "when": "python.datascience.hascodecells && python.datascience.featureenabled && python.datascience.ispythonornativeactive" }, + { + "command": "python.datascience.insertCellBelow", + "title": "%python.command.python.datascience.insertCellBelow.title%", + "category": "Python", + "when": "python.datascience.hascodecells && python.datascience.featureenabled && python.datascience.ispythonornativeactive" + }, { "command": "python.datascience.runcurrentcell", "title": "%python.command.python.datascience.runcurrentcell.title%", diff --git a/package.nls.json b/package.nls.json index a7732bc8b7c8..27ea2fe68b9b 100644 --- a/package.nls.json +++ b/package.nls.json @@ -65,6 +65,7 @@ "python.command.python.datascience.execSelectionInteractive.title": "Run Selection/Line in Python Interactive Window", "python.command.python.datascience.runcell.title": "Run Cell", "python.command.python.datascience.insertCellBelowPosition.title": "Insert Cell Below Position", + "python.command.python.datascience.insertCellBelow.title": "Insert Cell Below", "python.command.python.datascience.showhistorypane.title": "Show Python Interactive Window", "python.command.python.datascience.selectjupyteruri.title": "Specify local or remote Jupyter server for connections", "python.command.python.datascience.selectjupytercommandline.title": "Specify Jupyter command line arguments", diff --git a/src/client/common/application/commands.ts b/src/client/common/application/commands.ts index 63faf999322b..500f61876a1e 100644 --- a/src/client/common/application/commands.ts +++ b/src/client/common/application/commands.ts @@ -171,6 +171,7 @@ export interface ICommandNameArgumentTypeMapping extends ICommandNameWithoutArgu [DSCommands.DebugContinue]: []; [DSCommands.RunCurrentCellAndAddBelow]: [string]; [DSCommands.InsertCellBelowPosition]: []; + [DSCommands.InsertCellBelow]: []; [DSCommands.ScrollToCell]: [string, string]; [DSCommands.ViewJupyterOutput]: []; [DSCommands.ExportAsPythonScript]: [INotebookModel]; diff --git a/src/client/datascience/commands/commandRegistry.ts b/src/client/datascience/commands/commandRegistry.ts index 659f6ff841b3..73f6f2d6e66a 100644 --- a/src/client/datascience/commands/commandRegistry.ts +++ b/src/client/datascience/commands/commandRegistry.ts @@ -63,6 +63,7 @@ export class CommandRegistry implements IDisposable { this.registerCommand(Commands.RunAllCellsAbove, this.runAllCellsAbove); this.registerCommand(Commands.RunCellAndAllBelow, this.runCellAndAllBelow); this.registerCommand(Commands.InsertCellBelowPosition, this.insertCellBelowPosition); + this.registerCommand(Commands.InsertCellBelow, this.insertCellBelow); this.registerCommand(Commands.RunAllCellsAbovePalette, this.runAllCellsAboveFromCursor); this.registerCommand(Commands.RunCellAndAllBelowPalette, this.runCellAndAllBelowFromCursor); this.registerCommand(Commands.RunToLine, this.runToLine); @@ -315,6 +316,15 @@ export class CommandRegistry implements IDisposable { } } + private async insertCellBelow(): Promise { + const activeCodeWatcher = this.getCurrentCodeWatcher(); + if (activeCodeWatcher) { + return activeCodeWatcher.insertCellBelow(); + } else { + return Promise.resolve(); + } + } + private async runAllCellsAboveFromCursor(): Promise { const currentCodeLens = this.getCurrentCodeLens(); if (currentCodeLens) { diff --git a/src/client/datascience/constants.ts b/src/client/datascience/constants.ts index ff43756eb07e..773ef08f4421 100644 --- a/src/client/datascience/constants.ts +++ b/src/client/datascience/constants.ts @@ -78,6 +78,7 @@ export namespace Commands { export const DebugStop = 'python.datascience.debugstop'; export const RunCurrentCellAndAddBelow = 'python.datascience.runcurrentcellandaddbelow'; export const InsertCellBelowPosition = 'python.datascience.insertCellBelowPosition'; + export const InsertCellBelow = 'python.datascience.insertCellBelow'; export const ScrollToCell = 'python.datascience.scrolltocell'; export const CreateNewNotebook = 'python.datascience.createnewnotebook'; export const ViewJupyterOutput = 'python.datascience.viewJupyterOutput'; diff --git a/src/client/datascience/editor-integration/codewatcher.ts b/src/client/datascience/editor-integration/codewatcher.ts index d461e1e84469..00bb50113a62 100644 --- a/src/client/datascience/editor-integration/codewatcher.ts +++ b/src/client/datascience/editor-integration/codewatcher.ts @@ -372,19 +372,27 @@ export class CodeWatcher implements ICodeWatcher { } } - public async insertCellBelowCurrent(): Promise { + public async insertCellBelow(): Promise { const editor = this.documentManager.activeTextEditor; - const cell = this.getCellFromPosition(); - if (editor && cell) { - return this.insertCell(editor, cell.range.end.line + 1); + if (editor && editor.selection) { + const cell = this.getCellFromPosition(editor.selection.end); + if (cell) { + return this.insertCell(editor, cell.range.end.line + 1); + } else { + return this.insertCell(editor, editor.selection.end.line + 1); + } } } - public async insertCellAboveCurrent(): Promise { + public async insertCellAbove(): Promise { const editor = this.documentManager.activeTextEditor; - const cell = this.getCellFromPosition(); - if (editor && cell) { - return this.insertCell(editor, cell.range.start.line); + if (editor && editor.selection) { + const cell = this.getCellFromPosition(editor.selection.start); + if (cell) { + return this.insertCell(editor, cell.range.start.line); + } else { + return this.insertCell(editor, editor.selection.start.line); + } } } diff --git a/src/client/datascience/types.ts b/src/client/datascience/types.ts index 64144cc09d3a..3ac8b0c17206 100644 --- a/src/client/datascience/types.ts +++ b/src/client/datascience/types.ts @@ -628,6 +628,7 @@ export interface ICodeWatcher { addEmptyCellToBottom(): Promise; runCurrentCellAndAddBelow(): Promise; insertCellBelowPosition(): Promise; + insertCellBelow(): Promise; debugCurrentCell(): Promise; } diff --git a/src/test/datascience/editor-integration/codewatcher.unit.test.ts b/src/test/datascience/editor-integration/codewatcher.unit.test.ts index 4789b731acd0..5527efd39ae6 100644 --- a/src/test/datascience/editor-integration/codewatcher.unit.test.ts +++ b/src/test/datascience/editor-integration/codewatcher.unit.test.ts @@ -5,7 +5,7 @@ // Disable whitespace / multiline as we use that to pass in our fake file strings import { expect } from 'chai'; import * as TypeMoq from 'typemoq'; -import { CancellationTokenSource, CodeLens, Disposable, Range, Selection, TextDocument, TextEditor, Uri } from 'vscode'; +import { CancellationTokenSource, CodeLens, Disposable, Range, Selection, TextEditor, Uri } from 'vscode'; import { ICommandManager, @@ -30,7 +30,6 @@ import { import { IServiceContainer } from '../../../client/ioc/types'; import { ICodeExecutionHelper } from '../../../client/terminals/types'; import { MockAutoSelectionService } from '../../mocks/autoSelector'; -import { MockDocument } from '../mockDocument'; import { MockDocumentManager } from '../mockDocumentManager'; import { MockPythonSettings } from '../mockPythonSettings'; import { MockEditor } from '../mockTextEditor'; @@ -38,13 +37,33 @@ import { createDocument } from './helpers'; //tslint:disable:no-any +function initializeMockTextEditor( + codeWatcher: CodeWatcher, + documentManager: TypeMoq.IMock, + inputText: string +): MockEditor { + const fileName = Uri.file('test.py').fsPath; + const version = 1; + const document = createDocument(inputText, fileName, version, TypeMoq.Times.atLeastOnce(), true); + codeWatcher.setDocument(document.object); + + // For this test we need to set up a document selection point + // TypeMoq does not play well with setting properties on editor + const mockDocumentManager = new MockDocumentManager(); + const mockDocument = mockDocumentManager.addDocument(inputText, fileName); + const mockTextEditor = new MockEditor(mockDocumentManager, mockDocument); + documentManager.reset(); + documentManager.setup((dm) => dm.activeTextEditor).returns(() => mockTextEditor); + mockTextEditor.selection = new Selection(0, 0, 0, 0); + return mockTextEditor; +} + suite('DataScience Code Watcher Unit Tests', () => { let codeWatcher: CodeWatcher; let interactiveWindowProvider: TypeMoq.IMock; let notebookProvider: TypeMoq.IMock; let activeInteractiveWindow: TypeMoq.IMock; let documentManager: TypeMoq.IMock; - let mockDocumentManager: MockDocumentManager; let commandManager: TypeMoq.IMock; let textEditor: TypeMoq.IMock; let fileSystem: TypeMoq.IMock; @@ -74,7 +93,6 @@ suite('DataScience Code Watcher Unit Tests', () => { commandManager = TypeMoq.Mock.ofType(); debugService = TypeMoq.Mock.ofType(); vscodeNotebook = TypeMoq.Mock.ofType(); - mockDocumentManager = new MockDocumentManager(); // Setup default settings pythonSettings.datascience = { @@ -1032,23 +1050,16 @@ testing2`; // Command tests override getText, so just need the ranges here }); test('Test insert cell below position', async () => { - const fileName = Uri.file('test.py').fsPath; - const version = 1; - const inputText = `testing0 + const mockTextEditor = initializeMockTextEditor( + codeWatcher, + documentManager, + `testing0 #%% testing1 #%% -testing2`; - const document = createDocument(inputText, fileName, version, TypeMoq.Times.atLeastOnce(), true); - - codeWatcher.setDocument(document.object); +testing2` + ); - // For this test we need to set up a document selection point - // TypeMoq does not play well with setting properties on editor - const mockDocument = mockDocumentManager.addDocument(inputText, fileName); - const mockTextEditor = new MockEditor(mockDocumentManager, mockDocument); - documentManager.reset(); - documentManager.setup((dm) => dm.activeTextEditor).returns(() => mockTextEditor); mockTextEditor.selection = new Selection(0, 4, 0, 4); await codeWatcher.insertCellBelowPosition(); @@ -1067,24 +1078,18 @@ testing2`); }); test('Test insert cell below position at end', async () => { - const fileName = Uri.file('test.py').fsPath; - const version = 1; - const inputText = `testing0 + const mockTextEditor = initializeMockTextEditor( + codeWatcher, + documentManager, + `testing0 #%% testing1 #%% -testing2`; - const document = createDocument(inputText, fileName, version, TypeMoq.Times.atLeastOnce(), true); - - codeWatcher.setDocument(document.object); +testing2` + ); - // For this test we need to set up a document selection point - // TypeMoq does not play well with setting properties on editor - const mockDocument = mockDocumentManager.addDocument(inputText, fileName); - const mockTextEditor = new MockEditor(mockDocumentManager, mockDocument); - documentManager.reset(); - documentManager.setup((dm) => dm.activeTextEditor).returns(() => mockTextEditor); - mockTextEditor.selection = new Selection(4, 0, 5, 8); + // end selection at bottom of document + mockTextEditor.selection = new Selection(1, 4, 5, 8); await codeWatcher.insertCellBelowPosition(); @@ -1100,4 +1105,97 @@ testing2 expect(mockTextEditor.selection.end.line).to.equal(7); expect(mockTextEditor.selection.end.character).to.equal(0); }); + + test('Test insert cell below', async () => { + const mockTextEditor = initializeMockTextEditor( + codeWatcher, + documentManager, + `testing0 +#%% +testing1 +testing1a +#%% +testing2` + ); + + mockTextEditor.selection = new Selection(2, 4, 2, 4); + + await codeWatcher.insertCellBelow(); + + expect(mockTextEditor.document.getText()).to.equal( + `testing0 +#%% +testing1 +testing1a +# %% + +#%% +testing2` + ); + expect(mockTextEditor.selection.start.line).to.equal(5); + expect(mockTextEditor.selection.start.character).to.equal(0); + expect(mockTextEditor.selection.end.line).to.equal(5); + expect(mockTextEditor.selection.end.character).to.equal(0); + }); + + test('Test insert cell below but above any cell', async () => { + const mockTextEditor = initializeMockTextEditor( + codeWatcher, + documentManager, + `testing0 +#%% +testing1 +#%% +testing2` + ); + + mockTextEditor.selection = new Selection(0, 4, 0, 4); + + await codeWatcher.insertCellBelow(); + + expect(mockTextEditor.document.getText()).to.equal(`testing0 +# %% + +#%% +testing1 +#%% +testing2`); + expect(mockTextEditor.selection.start.line).to.equal(2); + expect(mockTextEditor.selection.start.character).to.equal(0); + expect(mockTextEditor.selection.end.line).to.equal(2); + expect(mockTextEditor.selection.end.character).to.equal(0); + }); + + test('Test insert cell below selection range', async () => { + const mockTextEditor = initializeMockTextEditor( + codeWatcher, + documentManager, + `testing0 +#%% +testing1 +testing1a +#%% +testing2` + ); + + // range crossing multiple cells.Insert below bottom of range. + mockTextEditor.selection = new Selection(0, 4, 2, 4); + + await codeWatcher.insertCellBelow(); + + expect(mockTextEditor.document.getText()).to.equal( + `testing0 +#%% +testing1 +testing1a +# %% + +#%% +testing2` + ); + expect(mockTextEditor.selection.start.line).to.equal(5); + expect(mockTextEditor.selection.start.character).to.equal(0); + expect(mockTextEditor.selection.end.line).to.equal(5); + expect(mockTextEditor.selection.end.character).to.equal(0); + }); }); From 38fad86dd9cf8f932e54cb237f5b199dfff53cde Mon Sep 17 00:00:00 2001 From: earthastronaut <> Date: Sat, 20 Jun 2020 11:57:20 -0600 Subject: [PATCH 04/26] add command insertCellAbove * add method insertCellAbove to the CodeWatcher * link insertCellAbove to python.datascience.insertCellAbove * will insert cell above the top cell of the current selection --- package.json | 16 +++++ package.nls.json | 1 + src/client/common/application/commands.ts | 1 + .../datascience/commands/commandRegistry.ts | 10 +++ src/client/datascience/constants.ts | 1 + src/client/datascience/types.ts | 1 + .../codewatcher.unit.test.ts | 65 +++++++++++++++++++ 7 files changed, 95 insertions(+) diff --git a/package.json b/package.json index 883bc9c040e3..a0fac68ff28a 100644 --- a/package.json +++ b/package.json @@ -513,6 +513,11 @@ "title": "%python.command.python.datascience.insertCellBelow.title%", "category": "Python" }, + { + "command": "python.datascience.insertCellAbove", + "title": "%python.command.python.datascience.insertCellAbove.title%", + "category": "Python" + }, { "command": "python.datascience.runcurrentcelladvance", "title": "%python.command.python.datascience.runcurrentcelladvance.title%", @@ -797,6 +802,11 @@ "command": "python.datascience.insertCellBelow", "group": "Python2" }, + { + "when": "editorFocus && editorLangId == python && python.datascience.hascodecells && python.datascience.featureenabled && !notebookEditorFocused", + "command": "python.datascience.insertCellAbove", + "group": "Python2" + }, { "when": "editorFocus && editorLangId == python && python.datascience.hascodecells && python.datascience.featureenabled && !notebookEditorFocused", "command": "python.datascience.runcurrentcell", @@ -974,6 +984,12 @@ "category": "Python", "when": "python.datascience.hascodecells && python.datascience.featureenabled && python.datascience.ispythonornativeactive" }, + { + "command": "python.datascience.insertCellAbove", + "title": "%python.command.python.datascience.insertCellAbove.title%", + "category": "Python", + "when": "python.datascience.hascodecells && python.datascience.featureenabled && python.datascience.ispythonornativeactive" + }, { "command": "python.datascience.runcurrentcell", "title": "%python.command.python.datascience.runcurrentcell.title%", diff --git a/package.nls.json b/package.nls.json index 27ea2fe68b9b..46ebd83208ed 100644 --- a/package.nls.json +++ b/package.nls.json @@ -66,6 +66,7 @@ "python.command.python.datascience.runcell.title": "Run Cell", "python.command.python.datascience.insertCellBelowPosition.title": "Insert Cell Below Position", "python.command.python.datascience.insertCellBelow.title": "Insert Cell Below", + "python.command.python.datascience.insertCellAbove.title": "Insert Cell Above", "python.command.python.datascience.showhistorypane.title": "Show Python Interactive Window", "python.command.python.datascience.selectjupyteruri.title": "Specify local or remote Jupyter server for connections", "python.command.python.datascience.selectjupytercommandline.title": "Specify Jupyter command line arguments", diff --git a/src/client/common/application/commands.ts b/src/client/common/application/commands.ts index 500f61876a1e..2421b3e3bfdf 100644 --- a/src/client/common/application/commands.ts +++ b/src/client/common/application/commands.ts @@ -172,6 +172,7 @@ export interface ICommandNameArgumentTypeMapping extends ICommandNameWithoutArgu [DSCommands.RunCurrentCellAndAddBelow]: [string]; [DSCommands.InsertCellBelowPosition]: []; [DSCommands.InsertCellBelow]: []; + [DSCommands.InsertCellAbove]: []; [DSCommands.ScrollToCell]: [string, string]; [DSCommands.ViewJupyterOutput]: []; [DSCommands.ExportAsPythonScript]: [INotebookModel]; diff --git a/src/client/datascience/commands/commandRegistry.ts b/src/client/datascience/commands/commandRegistry.ts index 73f6f2d6e66a..7215c3433e62 100644 --- a/src/client/datascience/commands/commandRegistry.ts +++ b/src/client/datascience/commands/commandRegistry.ts @@ -64,6 +64,7 @@ export class CommandRegistry implements IDisposable { this.registerCommand(Commands.RunCellAndAllBelow, this.runCellAndAllBelow); this.registerCommand(Commands.InsertCellBelowPosition, this.insertCellBelowPosition); this.registerCommand(Commands.InsertCellBelow, this.insertCellBelow); + this.registerCommand(Commands.InsertCellAbove, this.insertCellAbove); this.registerCommand(Commands.RunAllCellsAbovePalette, this.runAllCellsAboveFromCursor); this.registerCommand(Commands.RunCellAndAllBelowPalette, this.runCellAndAllBelowFromCursor); this.registerCommand(Commands.RunToLine, this.runToLine); @@ -325,6 +326,15 @@ export class CommandRegistry implements IDisposable { } } + private async insertCellAbove(): Promise { + const activeCodeWatcher = this.getCurrentCodeWatcher(); + if (activeCodeWatcher) { + return activeCodeWatcher.insertCellAbove(); + } else { + return Promise.resolve(); + } + } + private async runAllCellsAboveFromCursor(): Promise { const currentCodeLens = this.getCurrentCodeLens(); if (currentCodeLens) { diff --git a/src/client/datascience/constants.ts b/src/client/datascience/constants.ts index 773ef08f4421..dfed1d393603 100644 --- a/src/client/datascience/constants.ts +++ b/src/client/datascience/constants.ts @@ -79,6 +79,7 @@ export namespace Commands { export const RunCurrentCellAndAddBelow = 'python.datascience.runcurrentcellandaddbelow'; export const InsertCellBelowPosition = 'python.datascience.insertCellBelowPosition'; export const InsertCellBelow = 'python.datascience.insertCellBelow'; + export const InsertCellAbove = 'python.datascience.insertCellAbove'; export const ScrollToCell = 'python.datascience.scrolltocell'; export const CreateNewNotebook = 'python.datascience.createnewnotebook'; export const ViewJupyterOutput = 'python.datascience.viewJupyterOutput'; diff --git a/src/client/datascience/types.ts b/src/client/datascience/types.ts index 3ac8b0c17206..55c4fcfa3d0e 100644 --- a/src/client/datascience/types.ts +++ b/src/client/datascience/types.ts @@ -629,6 +629,7 @@ export interface ICodeWatcher { runCurrentCellAndAddBelow(): Promise; insertCellBelowPosition(): Promise; insertCellBelow(): Promise; + insertCellAbove(): Promise; debugCurrentCell(): Promise; } diff --git a/src/test/datascience/editor-integration/codewatcher.unit.test.ts b/src/test/datascience/editor-integration/codewatcher.unit.test.ts index 5527efd39ae6..b3cfe9ef8bc4 100644 --- a/src/test/datascience/editor-integration/codewatcher.unit.test.ts +++ b/src/test/datascience/editor-integration/codewatcher.unit.test.ts @@ -1198,4 +1198,69 @@ testing2` expect(mockTextEditor.selection.end.line).to.equal(5); expect(mockTextEditor.selection.end.character).to.equal(0); }); + + test('Test insert cell above first cell of range', async () => { + const mockTextEditor = initializeMockTextEditor( + codeWatcher, + documentManager, + `testing0 +#%% +testing1 +testing1a +#%% +testing2` + ); + + // above the first cell of the range + mockTextEditor.selection = new Selection(3, 4, 5, 4); + + await codeWatcher.insertCellAbove(); + + expect(mockTextEditor.document.getText()).to.equal( + `testing0 +# %% + +#%% +testing1 +testing1a +#%% +testing2` + ); + expect(mockTextEditor.selection.start.line).to.equal(2); + expect(mockTextEditor.selection.start.character).to.equal(0); + expect(mockTextEditor.selection.end.line).to.equal(2); + expect(mockTextEditor.selection.end.character).to.equal(0); + }); + + test('Test insert cell above and above cells', async () => { + const mockTextEditor = initializeMockTextEditor( + codeWatcher, + documentManager, + `testing0 +#%% +testing1 +testing1a +#%% +testing2` + ); + + mockTextEditor.selection = new Selection(0, 3, 0, 4); + + await codeWatcher.insertCellAbove(); + + expect(mockTextEditor.document.getText()).to.equal( + `# %% + +testing0 +#%% +testing1 +testing1a +#%% +testing2` + ); + expect(mockTextEditor.selection.start.line).to.equal(1); + expect(mockTextEditor.selection.start.character).to.equal(0); + expect(mockTextEditor.selection.end.line).to.equal(1); + expect(mockTextEditor.selection.end.character).to.equal(0); + }); }); From 5afe4cc691c492da03d05578bd6060624aa4146e Mon Sep 17 00:00:00 2001 From: earthastronaut <> Date: Sun, 21 Jun 2020 10:11:14 -0600 Subject: [PATCH 05/26] add deleteCells command * add deleteCells method to CodeWatcher * link deleteCells to python.datascience.deleteCells --- package.json | 16 +++ package.nls.json | 1 + src/client/common/application/commands.ts | 1 + .../datascience/commands/commandRegistry.ts | 10 ++ src/client/datascience/constants.ts | 1 + .../editor-integration/codewatcher.ts | 119 ++++++++++++++++-- src/client/datascience/types.ts | 1 + .../codewatcher.unit.test.ts | 66 ++++++++++ 8 files changed, 202 insertions(+), 13 deletions(-) diff --git a/package.json b/package.json index a0fac68ff28a..1d48bdc417a6 100644 --- a/package.json +++ b/package.json @@ -518,6 +518,11 @@ "title": "%python.command.python.datascience.insertCellAbove.title%", "category": "Python" }, + { + "command": "python.datascience.deleteCells", + "title": "%python.command.python.datascience.deleteCells.title%", + "category": "Python" + }, { "command": "python.datascience.runcurrentcelladvance", "title": "%python.command.python.datascience.runcurrentcelladvance.title%", @@ -807,6 +812,11 @@ "command": "python.datascience.insertCellAbove", "group": "Python2" }, + { + "when": "editorFocus && editorLangId == python && python.datascience.hascodecells && python.datascience.featureenabled && !notebookEditorFocused", + "command": "python.datascience.deleteCells", + "group": "Python2" + }, { "when": "editorFocus && editorLangId == python && python.datascience.hascodecells && python.datascience.featureenabled && !notebookEditorFocused", "command": "python.datascience.runcurrentcell", @@ -990,6 +1000,12 @@ "category": "Python", "when": "python.datascience.hascodecells && python.datascience.featureenabled && python.datascience.ispythonornativeactive" }, + { + "command": "python.datascience.deleteCells", + "title": "%python.command.python.datascience.deleteCells.title%", + "category": "Python", + "when": "python.datascience.hascodecells && python.datascience.featureenabled && python.datascience.ispythonornativeactive" + }, { "command": "python.datascience.runcurrentcell", "title": "%python.command.python.datascience.runcurrentcell.title%", diff --git a/package.nls.json b/package.nls.json index 46ebd83208ed..c42f3b71a68f 100644 --- a/package.nls.json +++ b/package.nls.json @@ -67,6 +67,7 @@ "python.command.python.datascience.insertCellBelowPosition.title": "Insert Cell Below Position", "python.command.python.datascience.insertCellBelow.title": "Insert Cell Below", "python.command.python.datascience.insertCellAbove.title": "Insert Cell Above", + "python.command.python.datascience.deleteCells.title": "Delete Selected Cells", "python.command.python.datascience.showhistorypane.title": "Show Python Interactive Window", "python.command.python.datascience.selectjupyteruri.title": "Specify local or remote Jupyter server for connections", "python.command.python.datascience.selectjupytercommandline.title": "Specify Jupyter command line arguments", diff --git a/src/client/common/application/commands.ts b/src/client/common/application/commands.ts index 2421b3e3bfdf..9c0c2fdff85f 100644 --- a/src/client/common/application/commands.ts +++ b/src/client/common/application/commands.ts @@ -173,6 +173,7 @@ export interface ICommandNameArgumentTypeMapping extends ICommandNameWithoutArgu [DSCommands.InsertCellBelowPosition]: []; [DSCommands.InsertCellBelow]: []; [DSCommands.InsertCellAbove]: []; + [DSCommands.DeleteCells]: []; [DSCommands.ScrollToCell]: [string, string]; [DSCommands.ViewJupyterOutput]: []; [DSCommands.ExportAsPythonScript]: [INotebookModel]; diff --git a/src/client/datascience/commands/commandRegistry.ts b/src/client/datascience/commands/commandRegistry.ts index 7215c3433e62..34cb4dd63fd4 100644 --- a/src/client/datascience/commands/commandRegistry.ts +++ b/src/client/datascience/commands/commandRegistry.ts @@ -65,6 +65,7 @@ export class CommandRegistry implements IDisposable { this.registerCommand(Commands.InsertCellBelowPosition, this.insertCellBelowPosition); this.registerCommand(Commands.InsertCellBelow, this.insertCellBelow); this.registerCommand(Commands.InsertCellAbove, this.insertCellAbove); + this.registerCommand(Commands.DeleteCells, this.deleteCells); this.registerCommand(Commands.RunAllCellsAbovePalette, this.runAllCellsAboveFromCursor); this.registerCommand(Commands.RunCellAndAllBelowPalette, this.runCellAndAllBelowFromCursor); this.registerCommand(Commands.RunToLine, this.runToLine); @@ -335,6 +336,15 @@ export class CommandRegistry implements IDisposable { } } + private async deleteCells(): Promise { + const activeCodeWatcher = this.getCurrentCodeWatcher(); + if (activeCodeWatcher) { + return activeCodeWatcher.deleteCells(); + } else { + return Promise.resolve(); + } + } + private async runAllCellsAboveFromCursor(): Promise { const currentCodeLens = this.getCurrentCodeLens(); if (currentCodeLens) { diff --git a/src/client/datascience/constants.ts b/src/client/datascience/constants.ts index dfed1d393603..5bb47bd2940e 100644 --- a/src/client/datascience/constants.ts +++ b/src/client/datascience/constants.ts @@ -80,6 +80,7 @@ export namespace Commands { export const InsertCellBelowPosition = 'python.datascience.insertCellBelowPosition'; export const InsertCellBelow = 'python.datascience.insertCellBelow'; export const InsertCellAbove = 'python.datascience.insertCellAbove'; + export const DeleteCells = 'python.datascience.deleteCells'; export const ScrollToCell = 'python.datascience.scrolltocell'; export const CreateNewNotebook = 'python.datascience.createnewnotebook'; export const ViewJupyterOutput = 'python.datascience.viewJupyterOutput'; diff --git a/src/client/datascience/editor-integration/codewatcher.ts b/src/client/datascience/editor-integration/codewatcher.ts index 00bb50113a62..0434cdd8bb59 100644 --- a/src/client/datascience/editor-integration/codewatcher.ts +++ b/src/client/datascience/editor-integration/codewatcher.ts @@ -31,8 +31,30 @@ import { IInteractiveWindowProvider } from '../types'; +function getIndex(index: number, length: number): number { + // return index within the length range with negative indexing + if (length <= 0) { + throw new RangeError(`Length must be > 0 not ${length}`); + } + // negative index count back from length + if (index < 0) { + index += length; + } + // bounded index + if (index < 0) { + return 0; + } else if (index >= length) { + return length - 1; + } else { + return index; + } +} + @injectable() export class CodeWatcher implements ICodeWatcher { + public get codeLensUpdated(): Event { + return this.codeLensUpdatedEvent.event; + } private static sentExecuteCellTelemetry: boolean = false; private document?: TextDocument; private version: number = -1; @@ -73,10 +95,6 @@ export class CodeWatcher implements ICodeWatcher { this.closeDocumentDisposable = this.documentManager.onDidCloseTextDocument(this.onDocumentClosed.bind(this)); } - public get codeLensUpdated(): Event { - return this.codeLensUpdatedEvent.event; - } - public getFileName() { return this.fileName; } @@ -396,6 +414,75 @@ export class CodeWatcher implements ICodeWatcher { } } + public async deleteCells(): Promise { + const editor = this.documentManager.activeTextEditor; + if (!editor || !editor.selection) { + return Promise.resolve(); + } + + const firstLastCells = this.getFirstLastCells(editor.selection); + if (!firstLastCells) { + return Promise.resolve(); + } + const startCell = firstLastCells[0]; + const endCell = firstLastCells[1]; + + // Start of the document should start at position 0, 0 and end one line ahead. + let startLineNumber = 0; + let startCharacterNumber = 0; + let endLineNumber = endCell.range.end.line + 1; + let endCharacterNumber = 0; + // Anywhere else in the document should start at the end of line before the + // cell and end at the last character of the cell. + if (startCell.range.start.line > 0) { + startLineNumber = startCell.range.start.line - 1; + startCharacterNumber = editor.document.lineAt(startLineNumber).range.end.character; + endLineNumber = endCell.range.end.line; + endCharacterNumber = endCell.range.end.character; + } + const cellExtendedRange = new Range( + new Position(startLineNumber, startCharacterNumber), + new Position(endLineNumber, endCharacterNumber) + ); + return editor + .edit((editBuilder) => { + editBuilder.replace(cellExtendedRange, ''); + this.codeLensUpdatedEvent.fire(); + }) + .then(() => { + return Promise.resolve(); + }); + } + + private getFirstLastCells(selection: Selection): ICellRange[] | undefined { + let startCellIndex = this.getCellIndex(selection.start); + let endCellIndex = startCellIndex; + // handle if the selection is the same line, hence same cell + if (selection.start.line !== selection.end.line) { + endCellIndex = this.getCellIndex(selection.end); + } + // handle when selection is above the top most cell + if (startCellIndex === -1) { + if (endCellIndex === -1) { + return undefined; + } else { + // selected a range above the first cell. + startCellIndex = 0; + const startCell = this.getCellFromIndex(0); + if (selection.start.line > startCell.range.start.line) { + throw RangeError( + `Should not be able to pick a range with an end in a cell and start after a cell. ${selection.start.line} > ${startCell.range.end.line}` + ); + } + } + } + if (startCellIndex >= 0 && endCellIndex >= 0) { + const startCell = this.getCellFromIndex(startCellIndex); + const endCell = this.getCellFromIndex(endCellIndex); + return [startCell, endCell]; + } + } + private async insertCell(editor: TextEditor, line: number): Promise { // insertCell // @@ -528,7 +615,17 @@ export class CodeWatcher implements ICodeWatcher { } } - private getCellIndex(position?: Position): number | undefined { + private getCellIndex(position: Position): number { + return this.codeLensFactory.cells.findIndex((cell) => position && cell.range.contains(position)); + } + + private getCellFromIndex(index: number): ICellRange { + const cells = this.codeLensFactory.cells; + const indexBounded = getIndex(index, cells.length); + return cells[indexBounded]; + } + + private getCellFromPosition(position?: Position): ICellRange | undefined { if (!position) { const editor = this.documentManager.activeTextEditor; if (editor && editor.selection) { @@ -536,14 +633,10 @@ export class CodeWatcher implements ICodeWatcher { } } if (position) { - return this.codeLensFactory.cells.findIndex((cell) => position && cell.range.contains(position)); - } - } - - private getCellFromPosition(position?: Position): ICellRange | undefined { - const index = this.getCellIndex(position); - if (index !== undefined) { - return this.codeLensFactory.cells[index]; + const index = this.getCellIndex(position); + if (index >= 0) { + return this.codeLensFactory.cells[index]; + } } } diff --git a/src/client/datascience/types.ts b/src/client/datascience/types.ts index 55c4fcfa3d0e..fbcbe74cdc59 100644 --- a/src/client/datascience/types.ts +++ b/src/client/datascience/types.ts @@ -630,6 +630,7 @@ export interface ICodeWatcher { insertCellBelowPosition(): Promise; insertCellBelow(): Promise; insertCellAbove(): Promise; + deleteCells(): Promise; debugCurrentCell(): Promise; } diff --git a/src/test/datascience/editor-integration/codewatcher.unit.test.ts b/src/test/datascience/editor-integration/codewatcher.unit.test.ts index b3cfe9ef8bc4..1958ef40d25e 100644 --- a/src/test/datascience/editor-integration/codewatcher.unit.test.ts +++ b/src/test/datascience/editor-integration/codewatcher.unit.test.ts @@ -1263,4 +1263,70 @@ testing2` expect(mockTextEditor.selection.end.line).to.equal(1); expect(mockTextEditor.selection.end.character).to.equal(0); }); + + test('Delete single cell', async () => { + const mockTextEditor = initializeMockTextEditor( + codeWatcher, + documentManager, + `testing0 +#%% +testing1 +testing1a +#%% +testing2` + ); + + mockTextEditor.selection = new Selection(3, 4, 3, 4); + + await codeWatcher.deleteCells(); + + expect(mockTextEditor.document.getText()).to.equal( + `testing0 +#%% +testing2` + ); + }); + + test('Delete multiple cell', async () => { + const mockTextEditor = initializeMockTextEditor( + codeWatcher, + documentManager, + `testing0 +#%% +testing1 +testing1a +#%% +testing2` + ); + + mockTextEditor.selection = new Selection(3, 4, 5, 4); + + await codeWatcher.deleteCells(); + + expect(mockTextEditor.document.getText()).to.equal(`testing0`); + }); + + test('Delete cell no cells in selection', async () => { + const mockTextEditor = initializeMockTextEditor( + codeWatcher, + documentManager, + `testing0 +#%% +testing1 +testing1a +#%% +testing2` + ); + + mockTextEditor.selection = new Selection(0, 1, 0, 4); + + await codeWatcher.deleteCells(); + + expect(mockTextEditor.document.getText()).to.equal(`testing0 +#%% +testing1 +testing1a +#%% +testing2`); + }); }); From dd846c1fc397b05d3a92d0fbcd7f55e51d101942 Mon Sep 17 00:00:00 2001 From: earthastronaut <> Date: Sun, 21 Jun 2020 11:08:19 -0600 Subject: [PATCH 06/26] add selectCell command * add selectCell method to CodeWatcher * link selectCell to python.datascience.selectCell --- package.json | 16 +++++ package.nls.json | 1 + src/client/common/application/commands.ts | 1 + .../datascience/commands/commandRegistry.ts | 10 +++ src/client/datascience/constants.ts | 1 + .../editor-integration/codewatcher.ts | 12 ++++ src/client/datascience/types.ts | 1 + .../codewatcher.unit.test.ts | 66 +++++++++++++++++++ 8 files changed, 108 insertions(+) diff --git a/package.json b/package.json index 1d48bdc417a6..2fa240dc080c 100644 --- a/package.json +++ b/package.json @@ -523,6 +523,11 @@ "title": "%python.command.python.datascience.deleteCells.title%", "category": "Python" }, + { + "command": "python.datascience.selectCell", + "title": "%python.command.python.datascience.selectCell.title%", + "category": "Python" + }, { "command": "python.datascience.runcurrentcelladvance", "title": "%python.command.python.datascience.runcurrentcelladvance.title%", @@ -817,6 +822,11 @@ "command": "python.datascience.deleteCells", "group": "Python2" }, + { + "when": "editorFocus && editorLangId == python && python.datascience.hascodecells && python.datascience.featureenabled && !notebookEditorFocused", + "command": "python.datascience.selectCell", + "group": "Python2" + }, { "when": "editorFocus && editorLangId == python && python.datascience.hascodecells && python.datascience.featureenabled && !notebookEditorFocused", "command": "python.datascience.runcurrentcell", @@ -1006,6 +1016,12 @@ "category": "Python", "when": "python.datascience.hascodecells && python.datascience.featureenabled && python.datascience.ispythonornativeactive" }, + { + "command": "python.datascience.selectCell", + "title": "%python.command.python.datascience.selectCell.title%", + "category": "Python", + "when": "python.datascience.hascodecells && python.datascience.featureenabled && python.datascience.ispythonornativeactive" + }, { "command": "python.datascience.runcurrentcell", "title": "%python.command.python.datascience.runcurrentcell.title%", diff --git a/package.nls.json b/package.nls.json index c42f3b71a68f..920f647c68cf 100644 --- a/package.nls.json +++ b/package.nls.json @@ -68,6 +68,7 @@ "python.command.python.datascience.insertCellBelow.title": "Insert Cell Below", "python.command.python.datascience.insertCellAbove.title": "Insert Cell Above", "python.command.python.datascience.deleteCells.title": "Delete Selected Cells", + "python.command.python.datascience.selectCell.title": "Select Cell", "python.command.python.datascience.showhistorypane.title": "Show Python Interactive Window", "python.command.python.datascience.selectjupyteruri.title": "Specify local or remote Jupyter server for connections", "python.command.python.datascience.selectjupytercommandline.title": "Specify Jupyter command line arguments", diff --git a/src/client/common/application/commands.ts b/src/client/common/application/commands.ts index 9c0c2fdff85f..a31f2425cf95 100644 --- a/src/client/common/application/commands.ts +++ b/src/client/common/application/commands.ts @@ -174,6 +174,7 @@ export interface ICommandNameArgumentTypeMapping extends ICommandNameWithoutArgu [DSCommands.InsertCellBelow]: []; [DSCommands.InsertCellAbove]: []; [DSCommands.DeleteCells]: []; + [DSCommands.SelectCell]: []; [DSCommands.ScrollToCell]: [string, string]; [DSCommands.ViewJupyterOutput]: []; [DSCommands.ExportAsPythonScript]: [INotebookModel]; diff --git a/src/client/datascience/commands/commandRegistry.ts b/src/client/datascience/commands/commandRegistry.ts index 34cb4dd63fd4..06d2e9423e18 100644 --- a/src/client/datascience/commands/commandRegistry.ts +++ b/src/client/datascience/commands/commandRegistry.ts @@ -66,6 +66,7 @@ export class CommandRegistry implements IDisposable { this.registerCommand(Commands.InsertCellBelow, this.insertCellBelow); this.registerCommand(Commands.InsertCellAbove, this.insertCellAbove); this.registerCommand(Commands.DeleteCells, this.deleteCells); + this.registerCommand(Commands.SelectCell, this.selectCell); this.registerCommand(Commands.RunAllCellsAbovePalette, this.runAllCellsAboveFromCursor); this.registerCommand(Commands.RunCellAndAllBelowPalette, this.runCellAndAllBelowFromCursor); this.registerCommand(Commands.RunToLine, this.runToLine); @@ -345,6 +346,15 @@ export class CommandRegistry implements IDisposable { } } + private async selectCell(): Promise { + const activeCodeWatcher = this.getCurrentCodeWatcher(); + if (activeCodeWatcher) { + return activeCodeWatcher.selectCell(); + } else { + return Promise.resolve(); + } + } + private async runAllCellsAboveFromCursor(): Promise { const currentCodeLens = this.getCurrentCodeLens(); if (currentCodeLens) { diff --git a/src/client/datascience/constants.ts b/src/client/datascience/constants.ts index 5bb47bd2940e..630ce64a329a 100644 --- a/src/client/datascience/constants.ts +++ b/src/client/datascience/constants.ts @@ -81,6 +81,7 @@ export namespace Commands { export const InsertCellBelow = 'python.datascience.insertCellBelow'; export const InsertCellAbove = 'python.datascience.insertCellAbove'; export const DeleteCells = 'python.datascience.deleteCells'; + export const SelectCell = 'python.datascience.selectCell'; export const ScrollToCell = 'python.datascience.scrolltocell'; export const CreateNewNotebook = 'python.datascience.createnewnotebook'; export const ViewJupyterOutput = 'python.datascience.viewJupyterOutput'; diff --git a/src/client/datascience/editor-integration/codewatcher.ts b/src/client/datascience/editor-integration/codewatcher.ts index 0434cdd8bb59..0b080af6dd1f 100644 --- a/src/client/datascience/editor-integration/codewatcher.ts +++ b/src/client/datascience/editor-integration/codewatcher.ts @@ -454,6 +454,18 @@ export class CodeWatcher implements ICodeWatcher { }); } + public async selectCell(): Promise { + const editor = this.documentManager.activeTextEditor; + if (editor && editor.selection) { + const firstLastCells = this.getFirstLastCells(editor.selection); + if (firstLastCells) { + const startCell = firstLastCells[0]; + const endCell = firstLastCells[1]; + editor.selection = new Selection(startCell.range.start, endCell.range.end); + } + } + } + private getFirstLastCells(selection: Selection): ICellRange[] | undefined { let startCellIndex = this.getCellIndex(selection.start); let endCellIndex = startCellIndex; diff --git a/src/client/datascience/types.ts b/src/client/datascience/types.ts index fbcbe74cdc59..a9ff9caad29f 100644 --- a/src/client/datascience/types.ts +++ b/src/client/datascience/types.ts @@ -631,6 +631,7 @@ export interface ICodeWatcher { insertCellBelow(): Promise; insertCellAbove(): Promise; deleteCells(): Promise; + selectCell(): Promise; debugCurrentCell(): Promise; } diff --git a/src/test/datascience/editor-integration/codewatcher.unit.test.ts b/src/test/datascience/editor-integration/codewatcher.unit.test.ts index 1958ef40d25e..eae7987325fe 100644 --- a/src/test/datascience/editor-integration/codewatcher.unit.test.ts +++ b/src/test/datascience/editor-integration/codewatcher.unit.test.ts @@ -1329,4 +1329,70 @@ testing1a #%% testing2`); }); + + test('Select cell single', async () => { + const mockTextEditor = initializeMockTextEditor( + codeWatcher, + documentManager, + `testing0 +#%% +testing1 +testing1a +#%% +testing2` + ); + + mockTextEditor.selection = new Selection(2, 1, 2, 1); + + await codeWatcher.selectCell(); + + expect(mockTextEditor.selection.start.line).to.equal(1); + expect(mockTextEditor.selection.start.character).to.equal(0); + expect(mockTextEditor.selection.end.line).to.equal(3); + expect(mockTextEditor.selection.end.character).to.equal(9); + }); + + test('Select cell multiple', async () => { + const mockTextEditor = initializeMockTextEditor( + codeWatcher, + documentManager, + `testing0 +#%% +testing1 +testing1a +#%% +testing2` + ); + + mockTextEditor.selection = new Selection(2, 1, 4, 1); + + await codeWatcher.selectCell(); + + expect(mockTextEditor.selection.start.line).to.equal(1); + expect(mockTextEditor.selection.start.character).to.equal(0); + expect(mockTextEditor.selection.end.line).to.equal(5); + expect(mockTextEditor.selection.end.character).to.equal(8); + }); + + test('Select cell unchanged above cells', async () => { + const mockTextEditor = initializeMockTextEditor( + codeWatcher, + documentManager, + `testing0 +#%% +testing1 +testing1a +#%% +testing2` + ); + + mockTextEditor.selection = new Selection(0, 1, 0, 4); + + await codeWatcher.selectCell(); + + expect(mockTextEditor.selection.start.line).to.equal(0); + expect(mockTextEditor.selection.start.character).to.equal(1); + expect(mockTextEditor.selection.end.line).to.equal(0); + expect(mockTextEditor.selection.end.character).to.equal(4); + }); }); From cfeccbb7072b95c6a5fa48b418ff5723a6ad8975 Mon Sep 17 00:00:00 2001 From: earthastronaut <> Date: Sun, 21 Jun 2020 12:48:15 -0600 Subject: [PATCH 07/26] add selectCellContents command * add selectCellContents method to CodeWatcher * link selectCellContents to python.datascience.selectCellContents --- package.json | 16 ++++ package.nls.json | 1 + src/client/common/application/commands.ts | 1 + .../datascience/commands/commandRegistry.ts | 10 +++ src/client/datascience/constants.ts | 1 + .../editor-integration/codewatcher.ts | 64 +++++++++++-- src/client/datascience/types.ts | 1 + .../codewatcher.unit.test.ts | 89 +++++++++++++++++++ 8 files changed, 174 insertions(+), 9 deletions(-) diff --git a/package.json b/package.json index 2fa240dc080c..4dee081dc42a 100644 --- a/package.json +++ b/package.json @@ -528,6 +528,11 @@ "title": "%python.command.python.datascience.selectCell.title%", "category": "Python" }, + { + "command": "python.datascience.selectCellContents", + "title": "%python.command.python.datascience.selectCellContents.title%", + "category": "Python" + }, { "command": "python.datascience.runcurrentcelladvance", "title": "%python.command.python.datascience.runcurrentcelladvance.title%", @@ -827,6 +832,11 @@ "command": "python.datascience.selectCell", "group": "Python2" }, + { + "when": "editorFocus && editorLangId == python && python.datascience.hascodecells && python.datascience.featureenabled && !notebookEditorFocused", + "command": "python.datascience.selectCellContents", + "group": "Python2" + }, { "when": "editorFocus && editorLangId == python && python.datascience.hascodecells && python.datascience.featureenabled && !notebookEditorFocused", "command": "python.datascience.runcurrentcell", @@ -1022,6 +1032,12 @@ "category": "Python", "when": "python.datascience.hascodecells && python.datascience.featureenabled && python.datascience.ispythonornativeactive" }, + { + "command": "python.datascience.selectCellContents", + "title": "%python.command.python.datascience.selectCellContents.title%", + "category": "Python", + "when": "python.datascience.hascodecells && python.datascience.featureenabled && python.datascience.ispythonornativeactive" + }, { "command": "python.datascience.runcurrentcell", "title": "%python.command.python.datascience.runcurrentcell.title%", diff --git a/package.nls.json b/package.nls.json index 920f647c68cf..477ec991b49f 100644 --- a/package.nls.json +++ b/package.nls.json @@ -69,6 +69,7 @@ "python.command.python.datascience.insertCellAbove.title": "Insert Cell Above", "python.command.python.datascience.deleteCells.title": "Delete Selected Cells", "python.command.python.datascience.selectCell.title": "Select Cell", + "python.command.python.datascience.selectCellContents.title": "Select Cell Contents", "python.command.python.datascience.showhistorypane.title": "Show Python Interactive Window", "python.command.python.datascience.selectjupyteruri.title": "Specify local or remote Jupyter server for connections", "python.command.python.datascience.selectjupytercommandline.title": "Specify Jupyter command line arguments", diff --git a/src/client/common/application/commands.ts b/src/client/common/application/commands.ts index a31f2425cf95..cc7943159a5a 100644 --- a/src/client/common/application/commands.ts +++ b/src/client/common/application/commands.ts @@ -175,6 +175,7 @@ export interface ICommandNameArgumentTypeMapping extends ICommandNameWithoutArgu [DSCommands.InsertCellAbove]: []; [DSCommands.DeleteCells]: []; [DSCommands.SelectCell]: []; + [DSCommands.SelectCellContents]: []; [DSCommands.ScrollToCell]: [string, string]; [DSCommands.ViewJupyterOutput]: []; [DSCommands.ExportAsPythonScript]: [INotebookModel]; diff --git a/src/client/datascience/commands/commandRegistry.ts b/src/client/datascience/commands/commandRegistry.ts index 06d2e9423e18..9af78ca6a8b4 100644 --- a/src/client/datascience/commands/commandRegistry.ts +++ b/src/client/datascience/commands/commandRegistry.ts @@ -67,6 +67,7 @@ export class CommandRegistry implements IDisposable { this.registerCommand(Commands.InsertCellAbove, this.insertCellAbove); this.registerCommand(Commands.DeleteCells, this.deleteCells); this.registerCommand(Commands.SelectCell, this.selectCell); + this.registerCommand(Commands.SelectCellContents, this.selectCellContents); this.registerCommand(Commands.RunAllCellsAbovePalette, this.runAllCellsAboveFromCursor); this.registerCommand(Commands.RunCellAndAllBelowPalette, this.runCellAndAllBelowFromCursor); this.registerCommand(Commands.RunToLine, this.runToLine); @@ -355,6 +356,15 @@ export class CommandRegistry implements IDisposable { } } + private async selectCellContents(): Promise { + const activeCodeWatcher = this.getCurrentCodeWatcher(); + if (activeCodeWatcher) { + return activeCodeWatcher.selectCellContents(); + } else { + return Promise.resolve(); + } + } + private async runAllCellsAboveFromCursor(): Promise { const currentCodeLens = this.getCurrentCodeLens(); if (currentCodeLens) { diff --git a/src/client/datascience/constants.ts b/src/client/datascience/constants.ts index 630ce64a329a..3fb5765e3baf 100644 --- a/src/client/datascience/constants.ts +++ b/src/client/datascience/constants.ts @@ -82,6 +82,7 @@ export namespace Commands { export const InsertCellAbove = 'python.datascience.insertCellAbove'; export const DeleteCells = 'python.datascience.deleteCells'; export const SelectCell = 'python.datascience.selectCell'; + export const SelectCellContents = 'python.datascience.selectCellContents'; export const ScrollToCell = 'python.datascience.scrolltocell'; export const CreateNewNotebook = 'python.datascience.createnewnotebook'; export const ViewJupyterOutput = 'python.datascience.viewJupyterOutput'; diff --git a/src/client/datascience/editor-integration/codewatcher.ts b/src/client/datascience/editor-integration/codewatcher.ts index 0b080af6dd1f..b262e3a9acfd 100644 --- a/src/client/datascience/editor-integration/codewatcher.ts +++ b/src/client/datascience/editor-integration/codewatcher.ts @@ -420,7 +420,7 @@ export class CodeWatcher implements ICodeWatcher { return Promise.resolve(); } - const firstLastCells = this.getFirstLastCells(editor.selection); + const firstLastCells = this.getStartEndCells(editor.selection); if (!firstLastCells) { return Promise.resolve(); } @@ -457,16 +457,64 @@ export class CodeWatcher implements ICodeWatcher { public async selectCell(): Promise { const editor = this.documentManager.activeTextEditor; if (editor && editor.selection) { - const firstLastCells = this.getFirstLastCells(editor.selection); - if (firstLastCells) { - const startCell = firstLastCells[0]; - const endCell = firstLastCells[1]; + const startEndCells = this.getStartEndCells(editor.selection); + if (startEndCells) { + const startCell = startEndCells[0]; + const endCell = startEndCells[1]; editor.selection = new Selection(startCell.range.start, endCell.range.end); } } } - private getFirstLastCells(selection: Selection): ICellRange[] | undefined { + public async selectCellContents(): Promise { + const editor = this.documentManager.activeTextEditor; + if (!editor || !editor.selection) { + return Promise.resolve(); + } + const startEndCellIndex = this.getStartEndCellIndex(editor.selection); + if (!startEndCellIndex) { + return Promise.resolve(); + } + const startCellIndex = startEndCellIndex[0]; + const endCellIndex = startEndCellIndex[1]; + const isAnchorLessEqualActive = + editor.selection.anchor.line <= editor.selection.active.line && + editor.selection.anchor.character <= editor.selection.anchor.character; + + const cells = this.codeLensFactory.cells; + const selections: Selection[] = []; + for (let i = startCellIndex; i <= endCellIndex; i += 1) { + const cell = cells[i]; + let anchorLine = cell.range.start.line + 1; + let achorCharacter = 0; + let activeLine = cell.range.end.line; + let activeCharacter = cell.range.end.character; + // if cell is only one line long, select the end of that line + if (cell.range.start.line === cell.range.end.line) { + anchorLine = cell.range.start.line; + achorCharacter = editor.document.lineAt(anchorLine).range.end.character; + activeLine = anchorLine; + activeCharacter = achorCharacter; + } + if (isAnchorLessEqualActive) { + selections.push(new Selection(anchorLine, achorCharacter, activeLine, activeCharacter)); + } else { + selections.push(new Selection(activeLine, activeCharacter, anchorLine, achorCharacter)); + } + } + editor.selections = selections; + } + + private getStartEndCells(selection: Selection): ICellRange[] | undefined { + const startEndCellIndex = this.getStartEndCellIndex(selection); + if (startEndCellIndex) { + const startCell = this.getCellFromIndex(startEndCellIndex[0]); + const endCell = this.getCellFromIndex(startEndCellIndex[1]); + return [startCell, endCell]; + } + } + + private getStartEndCellIndex(selection: Selection): number[] | undefined { let startCellIndex = this.getCellIndex(selection.start); let endCellIndex = startCellIndex; // handle if the selection is the same line, hence same cell @@ -489,9 +537,7 @@ export class CodeWatcher implements ICodeWatcher { } } if (startCellIndex >= 0 && endCellIndex >= 0) { - const startCell = this.getCellFromIndex(startCellIndex); - const endCell = this.getCellFromIndex(endCellIndex); - return [startCell, endCell]; + return [startCellIndex, endCellIndex]; } } diff --git a/src/client/datascience/types.ts b/src/client/datascience/types.ts index a9ff9caad29f..53d7e9c329d1 100644 --- a/src/client/datascience/types.ts +++ b/src/client/datascience/types.ts @@ -632,6 +632,7 @@ export interface ICodeWatcher { insertCellAbove(): Promise; deleteCells(): Promise; selectCell(): Promise; + selectCellContents(): Promise; debugCurrentCell(): Promise; } diff --git a/src/test/datascience/editor-integration/codewatcher.unit.test.ts b/src/test/datascience/editor-integration/codewatcher.unit.test.ts index eae7987325fe..a57f7a606c93 100644 --- a/src/test/datascience/editor-integration/codewatcher.unit.test.ts +++ b/src/test/datascience/editor-integration/codewatcher.unit.test.ts @@ -1395,4 +1395,93 @@ testing2` expect(mockTextEditor.selection.end.line).to.equal(0); expect(mockTextEditor.selection.end.character).to.equal(4); }); + + test('Select cell contents', async () => { + const mockTextEditor = initializeMockTextEditor( + codeWatcher, + documentManager, + `testing0 +#%% +testing1 +testing1a +#%% +testing2` + ); + + mockTextEditor.selection = new Selection(3, 4, 3, 4); + + await codeWatcher.selectCellContents(); + + expect(mockTextEditor.selections.length).to.equal(1); + + const selection = mockTextEditor.selections[0]; + expect(selection.anchor.line).to.equal(2); + expect(selection.anchor.character).to.equal(0); + expect(selection.active.line).to.equal(3); + expect(selection.active.character).to.equal(9); + }); + + test('Select cell contents multi cell', async () => { + const mockTextEditor = initializeMockTextEditor( + codeWatcher, + documentManager, + `testing0 +#%% +testing1 +testing1a +#%% +testing2` + ); + + mockTextEditor.selection = new Selection(3, 4, 5, 4); + + await codeWatcher.selectCellContents(); + + expect(mockTextEditor.selections.length).to.equal(2); + + let selection: Selection; + selection = mockTextEditor.selections[0]; + expect(selection.anchor.line).to.equal(2); + expect(selection.anchor.character).to.equal(0); + expect(selection.active.line).to.equal(3); + expect(selection.active.character).to.equal(9); + + selection = mockTextEditor.selections[1]; + expect(selection.anchor.line).to.equal(5); + expect(selection.anchor.character).to.equal(0); + expect(selection.active.line).to.equal(5); + expect(selection.active.character).to.equal(8); + }); + + test('Select cell contents multi cell reversed', async () => { + const mockTextEditor = initializeMockTextEditor( + codeWatcher, + documentManager, + `testing0 +#%% +testing1 +testing1a +#%% +testing2` + ); + + mockTextEditor.selection = new Selection(5, 4, 3, 4); + + await codeWatcher.selectCellContents(); + + expect(mockTextEditor.selections.length).to.equal(2); + + let selection: Selection; + selection = mockTextEditor.selections[0]; + expect(selection.active.line).to.equal(2); + expect(selection.active.character).to.equal(0); + expect(selection.anchor.line).to.equal(3); + expect(selection.anchor.character).to.equal(9); + + selection = mockTextEditor.selections[1]; + expect(selection.active.line).to.equal(5); + expect(selection.active.character).to.equal(0); + expect(selection.anchor.line).to.equal(5); + expect(selection.anchor.character).to.equal(8); + }); }); From 500fe5a078f2d3e4d35d9fa719b9f4497aa0f7f1 Mon Sep 17 00:00:00 2001 From: earthastronaut <> Date: Sun, 21 Jun 2020 16:02:36 -0600 Subject: [PATCH 08/26] modify selectCell to maintain previous orientation of selection anchor and active --- .../editor-integration/codewatcher.ts | 9 ++++- .../codewatcher.unit.test.ts | 40 ++++++++++++++----- 2 files changed, 39 insertions(+), 10 deletions(-) diff --git a/src/client/datascience/editor-integration/codewatcher.ts b/src/client/datascience/editor-integration/codewatcher.ts index b262e3a9acfd..9aa1544e832d 100644 --- a/src/client/datascience/editor-integration/codewatcher.ts +++ b/src/client/datascience/editor-integration/codewatcher.ts @@ -461,7 +461,14 @@ export class CodeWatcher implements ICodeWatcher { if (startEndCells) { const startCell = startEndCells[0]; const endCell = startEndCells[1]; - editor.selection = new Selection(startCell.range.start, endCell.range.end); + if ( + editor.selection.anchor.line <= editor.selection.active.line && + editor.selection.anchor.character <= editor.selection.active.character + ) { + editor.selection = new Selection(startCell.range.start, endCell.range.end); + } else { + editor.selection = new Selection(endCell.range.end, startCell.range.start); + } } } } diff --git a/src/test/datascience/editor-integration/codewatcher.unit.test.ts b/src/test/datascience/editor-integration/codewatcher.unit.test.ts index a57f7a606c93..283e0d4d8b3d 100644 --- a/src/test/datascience/editor-integration/codewatcher.unit.test.ts +++ b/src/test/datascience/editor-integration/codewatcher.unit.test.ts @@ -1346,10 +1346,10 @@ testing2` await codeWatcher.selectCell(); - expect(mockTextEditor.selection.start.line).to.equal(1); - expect(mockTextEditor.selection.start.character).to.equal(0); - expect(mockTextEditor.selection.end.line).to.equal(3); - expect(mockTextEditor.selection.end.character).to.equal(9); + expect(mockTextEditor.selection.anchor.line).to.equal(1); + expect(mockTextEditor.selection.anchor.character).to.equal(0); + expect(mockTextEditor.selection.active.line).to.equal(3); + expect(mockTextEditor.selection.active.character).to.equal(9); }); test('Select cell multiple', async () => { @@ -1368,13 +1368,35 @@ testing2` await codeWatcher.selectCell(); - expect(mockTextEditor.selection.start.line).to.equal(1); - expect(mockTextEditor.selection.start.character).to.equal(0); - expect(mockTextEditor.selection.end.line).to.equal(5); - expect(mockTextEditor.selection.end.character).to.equal(8); + expect(mockTextEditor.selection.anchor.line).to.equal(1); + expect(mockTextEditor.selection.anchor.character).to.equal(0); + expect(mockTextEditor.selection.active.line).to.equal(5); + expect(mockTextEditor.selection.active.character).to.equal(8); + }); + + test('Select cell multiple reversed', async () => { + const mockTextEditor = initializeMockTextEditor( + codeWatcher, + documentManager, + `testing0 +#%% +testing1 +testing1a +#%% +testing2` + ); + + mockTextEditor.selection = new Selection(4, 1, 2, 1); + + await codeWatcher.selectCell(); + + expect(mockTextEditor.selection.active.line).to.equal(1); + expect(mockTextEditor.selection.active.character).to.equal(0); + expect(mockTextEditor.selection.anchor.line).to.equal(5); + expect(mockTextEditor.selection.anchor.character).to.equal(8); }); - test('Select cell unchanged above cells', async () => { + test('Select cell above cells unchanged', async () => { const mockTextEditor = initializeMockTextEditor( codeWatcher, documentManager, From cf5e404ad29c7d3e5cfa5ac6982219850bb92c5a Mon Sep 17 00:00:00 2001 From: earthastronaut <> Date: Sun, 21 Jun 2020 17:35:33 -0600 Subject: [PATCH 09/26] fix detection of anchor and active position direction --- src/client/datascience/editor-integration/codewatcher.ts | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/src/client/datascience/editor-integration/codewatcher.ts b/src/client/datascience/editor-integration/codewatcher.ts index 9aa1544e832d..80ff087f04e2 100644 --- a/src/client/datascience/editor-integration/codewatcher.ts +++ b/src/client/datascience/editor-integration/codewatcher.ts @@ -461,10 +461,7 @@ export class CodeWatcher implements ICodeWatcher { if (startEndCells) { const startCell = startEndCells[0]; const endCell = startEndCells[1]; - if ( - editor.selection.anchor.line <= editor.selection.active.line && - editor.selection.anchor.character <= editor.selection.active.character - ) { + if (editor.selection.anchor.isBeforeOrEqual(editor.selection.active)) { editor.selection = new Selection(startCell.range.start, endCell.range.end); } else { editor.selection = new Selection(endCell.range.end, startCell.range.start); @@ -484,9 +481,7 @@ export class CodeWatcher implements ICodeWatcher { } const startCellIndex = startEndCellIndex[0]; const endCellIndex = startEndCellIndex[1]; - const isAnchorLessEqualActive = - editor.selection.anchor.line <= editor.selection.active.line && - editor.selection.anchor.character <= editor.selection.anchor.character; + const isAnchorLessEqualActive = editor.selection.anchor.isBeforeOrEqual(editor.selection.active); const cells = this.codeLensFactory.cells; const selections: Selection[] = []; From aa3944499695e0ab8d83cb644a383eddc0e31cfd Mon Sep 17 00:00:00 2001 From: earthastronaut <> Date: Sun, 21 Jun 2020 17:37:07 -0600 Subject: [PATCH 10/26] add extendSelectionByCellAbove * Works similar to excell extend selection by one cell above * Linked command to python.datascience.extendSelectionByCellAbove --- package.json | 16 ++ package.nls.json | 1 + src/client/common/application/commands.ts | 1 + .../datascience/commands/commandRegistry.ts | 10 + src/client/datascience/constants.ts | 1 + .../editor-integration/codewatcher.ts | 60 ++++++ src/client/datascience/types.ts | 1 + .../codewatcher.unit.test.ts | 175 ++++++++++++++++++ 8 files changed, 265 insertions(+) diff --git a/package.json b/package.json index 4dee081dc42a..9fd269c58b61 100644 --- a/package.json +++ b/package.json @@ -533,6 +533,11 @@ "title": "%python.command.python.datascience.selectCellContents.title%", "category": "Python" }, + { + "command": "python.datascience.extendSelectionByCellAbove", + "title": "%python.command.python.datascience.extendSelectionByCellAbove.title%", + "category": "Python" + }, { "command": "python.datascience.runcurrentcelladvance", "title": "%python.command.python.datascience.runcurrentcelladvance.title%", @@ -837,6 +842,11 @@ "command": "python.datascience.selectCellContents", "group": "Python2" }, + { + "when": "editorFocus && editorLangId == python && python.datascience.hascodecells && python.datascience.featureenabled && !notebookEditorFocused", + "command": "python.datascience.extendSelectionByCellAbove", + "group": "Python2" + }, { "when": "editorFocus && editorLangId == python && python.datascience.hascodecells && python.datascience.featureenabled && !notebookEditorFocused", "command": "python.datascience.runcurrentcell", @@ -1038,6 +1048,12 @@ "category": "Python", "when": "python.datascience.hascodecells && python.datascience.featureenabled && python.datascience.ispythonornativeactive" }, + { + "command": "python.datascience.extendSelectionByCellAbove", + "title": "%python.command.python.datascience.extendSelectionByCellAbove.title%", + "category": "Python", + "when": "python.datascience.hascodecells && python.datascience.featureenabled && python.datascience.ispythonornativeactive" + }, { "command": "python.datascience.runcurrentcell", "title": "%python.command.python.datascience.runcurrentcell.title%", diff --git a/package.nls.json b/package.nls.json index 477ec991b49f..ac137d5ec94a 100644 --- a/package.nls.json +++ b/package.nls.json @@ -70,6 +70,7 @@ "python.command.python.datascience.deleteCells.title": "Delete Selected Cells", "python.command.python.datascience.selectCell.title": "Select Cell", "python.command.python.datascience.selectCellContents.title": "Select Cell Contents", + "python.command.python.datascience.extendSelectionByCellAbove.title": "Extend Selection By Cell Above", "python.command.python.datascience.showhistorypane.title": "Show Python Interactive Window", "python.command.python.datascience.selectjupyteruri.title": "Specify local or remote Jupyter server for connections", "python.command.python.datascience.selectjupytercommandline.title": "Specify Jupyter command line arguments", diff --git a/src/client/common/application/commands.ts b/src/client/common/application/commands.ts index cc7943159a5a..9d32ee8ca587 100644 --- a/src/client/common/application/commands.ts +++ b/src/client/common/application/commands.ts @@ -176,6 +176,7 @@ export interface ICommandNameArgumentTypeMapping extends ICommandNameWithoutArgu [DSCommands.DeleteCells]: []; [DSCommands.SelectCell]: []; [DSCommands.SelectCellContents]: []; + [DSCommands.ExtendSelectionByCellAbove]: []; [DSCommands.ScrollToCell]: [string, string]; [DSCommands.ViewJupyterOutput]: []; [DSCommands.ExportAsPythonScript]: [INotebookModel]; diff --git a/src/client/datascience/commands/commandRegistry.ts b/src/client/datascience/commands/commandRegistry.ts index 9af78ca6a8b4..1bf35b727a6b 100644 --- a/src/client/datascience/commands/commandRegistry.ts +++ b/src/client/datascience/commands/commandRegistry.ts @@ -68,6 +68,7 @@ export class CommandRegistry implements IDisposable { this.registerCommand(Commands.DeleteCells, this.deleteCells); this.registerCommand(Commands.SelectCell, this.selectCell); this.registerCommand(Commands.SelectCellContents, this.selectCellContents); + this.registerCommand(Commands.ExtendSelectionByCellAbove, this.extendSelectionByCellAbove); this.registerCommand(Commands.RunAllCellsAbovePalette, this.runAllCellsAboveFromCursor); this.registerCommand(Commands.RunCellAndAllBelowPalette, this.runCellAndAllBelowFromCursor); this.registerCommand(Commands.RunToLine, this.runToLine); @@ -365,6 +366,15 @@ export class CommandRegistry implements IDisposable { } } + private async extendSelectionByCellAbove(): Promise { + const activeCodeWatcher = this.getCurrentCodeWatcher(); + if (activeCodeWatcher) { + return activeCodeWatcher.extendSelectionByCellAbove(); + } else { + return Promise.resolve(); + } + } + private async runAllCellsAboveFromCursor(): Promise { const currentCodeLens = this.getCurrentCodeLens(); if (currentCodeLens) { diff --git a/src/client/datascience/constants.ts b/src/client/datascience/constants.ts index 3fb5765e3baf..b424ecf2a07b 100644 --- a/src/client/datascience/constants.ts +++ b/src/client/datascience/constants.ts @@ -83,6 +83,7 @@ export namespace Commands { export const DeleteCells = 'python.datascience.deleteCells'; export const SelectCell = 'python.datascience.selectCell'; export const SelectCellContents = 'python.datascience.selectCellContents'; + export const ExtendSelectionByCellAbove = 'python.datascience.extendSelectionByCellAbove'; export const ScrollToCell = 'python.datascience.scrolltocell'; export const CreateNewNotebook = 'python.datascience.createnewnotebook'; export const ViewJupyterOutput = 'python.datascience.viewJupyterOutput'; diff --git a/src/client/datascience/editor-integration/codewatcher.ts b/src/client/datascience/editor-integration/codewatcher.ts index 80ff087f04e2..c2929c57d614 100644 --- a/src/client/datascience/editor-integration/codewatcher.ts +++ b/src/client/datascience/editor-integration/codewatcher.ts @@ -507,6 +507,66 @@ export class CodeWatcher implements ICodeWatcher { editor.selections = selections; } + public async extendSelectionByCellAbove(): Promise { + // This behaves similarly to excel "Extend Selection by One Cell Above". + // The direction of the selection matters (i.e. where the active cursor) + // position is. First, it ensures that complete cells are selection. + // If so, then if active cursor is in cells below it contracts the + // selection range. If the active cursor is above, it expands the + // selection range. + const editor = this.documentManager.activeTextEditor; + if (!editor || !editor.selection) { + return Promise.resolve(); + } + const currentSelection = editor.selection; + const startEndCellIndex = this.getStartEndCellIndex(editor.selection); + if (!startEndCellIndex) { + return Promise.resolve(); + } + + const isAnchorLessThanActive = editor.selection.anchor.isBefore(editor.selection.active); + + const cells = this.codeLensFactory.cells; + const startCellIndex = startEndCellIndex[0]; + const endCellIndex = startEndCellIndex[1]; + const startCell = cells[startCellIndex]; + const endCell = cells[endCellIndex]; + + if ( + !startCell.range.start.isEqual(currentSelection.start) || + !endCell.range.end.isEqual(currentSelection.end) + ) { + // full cell range not selected, first select a full cell range. + let selection: Selection; + if (isAnchorLessThanActive) { + if (startCellIndex < endCellIndex) { + // active at end of cell before endCell + selection = new Selection(startCell.range.start, cells[endCellIndex - 1].range.end); + } else { + // active at end of startCell + selection = new Selection(startCell.range.end, startCell.range.start); + } + } else { + // active at start of start cell. + selection = new Selection(endCell.range.end, startCell.range.start); + } + editor.selection = selection; + } else { + // full cell range is selected now decide if expanding or contracting? + if (isAnchorLessThanActive && startCellIndex < endCellIndex) { + // anchor is above active, contract selection by cell below. + const newEndCell = cells[endCellIndex - 1]; + editor.selection = new Selection(startCell.range.start, newEndCell.range.end); + } else { + // anchor is below active, expand selection by cell above. + if (startCellIndex > 0) { + const aboveCell = cells[startCellIndex - 1]; + editor.selection = new Selection(endCell.range.end, aboveCell.range.start); + } + } + } + } + private getStartEndCells(selection: Selection): ICellRange[] | undefined { const startEndCellIndex = this.getStartEndCellIndex(selection); if (startEndCellIndex) { diff --git a/src/client/datascience/types.ts b/src/client/datascience/types.ts index 53d7e9c329d1..cc13484a160e 100644 --- a/src/client/datascience/types.ts +++ b/src/client/datascience/types.ts @@ -633,6 +633,7 @@ export interface ICodeWatcher { deleteCells(): Promise; selectCell(): Promise; selectCellContents(): Promise; + extendSelectionByCellAbove(): Promise; debugCurrentCell(): Promise; } diff --git a/src/test/datascience/editor-integration/codewatcher.unit.test.ts b/src/test/datascience/editor-integration/codewatcher.unit.test.ts index 283e0d4d8b3d..927c46451493 100644 --- a/src/test/datascience/editor-integration/codewatcher.unit.test.ts +++ b/src/test/datascience/editor-integration/codewatcher.unit.test.ts @@ -1506,4 +1506,179 @@ testing2` expect(selection.anchor.line).to.equal(5); expect(selection.anchor.character).to.equal(8); }); + + test('Extend selection by cell above initial select', async () => { + const mockTextEditor = initializeMockTextEditor( + codeWatcher, + documentManager, + `testing_L0 +# %% +testing_L2 +testing_L3 +# %% +testing_L5 +testing_L6 +# %% +testing_L8` + ); + + mockTextEditor.selection = new Selection(5, 2, 5, 2); + + await codeWatcher.extendSelectionByCellAbove(); + + expect(mockTextEditor.selection.anchor.line).to.equal(6); + expect(mockTextEditor.selection.anchor.character).to.equal(10); + expect(mockTextEditor.selection.active.line).to.equal(4); + expect(mockTextEditor.selection.active.character).to.equal(0); + }); + + test('Extend selection by cell above initial range in cell', async () => { + const mockTextEditor = initializeMockTextEditor( + codeWatcher, + documentManager, + `testing_L0 +# %% +testing_L2 +testing_L3 +# %% +testing_L5 +testing_L6 +# %% +testing_L8` + ); + + mockTextEditor.selection = new Selection(5, 2, 6, 4); + + await codeWatcher.extendSelectionByCellAbove(); + + expect(mockTextEditor.selection.anchor.line).to.equal(6); + expect(mockTextEditor.selection.anchor.character).to.equal(10); + expect(mockTextEditor.selection.active.line).to.equal(4); + expect(mockTextEditor.selection.active.character).to.equal(0); + }); + + test('Extend selection by cell above initial range in cell opposite direction', async () => { + const mockTextEditor = initializeMockTextEditor( + codeWatcher, + documentManager, + `testing_L0 +# %% +testing_L2 +testing_L3 +# %% +testing_L5 +testing_L6 +# %% +testing_L8` + ); + + mockTextEditor.selection = new Selection(6, 4, 5, 2); + + await codeWatcher.extendSelectionByCellAbove(); + + expect(mockTextEditor.selection.anchor.line).to.equal(6); + expect(mockTextEditor.selection.anchor.character).to.equal(10); + expect(mockTextEditor.selection.active.line).to.equal(4); + expect(mockTextEditor.selection.active.character).to.equal(0); + }); + + test('Extend selection by cell above initial range below cell', async () => { + const mockTextEditor = initializeMockTextEditor( + codeWatcher, + documentManager, + `testing_L0 +# %% +testing_L2 +testing_L3 +# %% +testing_L5 +testing_L6 +# %% +testing_L8` + ); + + mockTextEditor.selection = new Selection(5, 2, 8, 2); + + await codeWatcher.extendSelectionByCellAbove(); + + expect(mockTextEditor.selection.anchor.line).to.equal(4); + expect(mockTextEditor.selection.anchor.character).to.equal(0); + expect(mockTextEditor.selection.active.line).to.equal(6); + expect(mockTextEditor.selection.active.character).to.equal(10); + }); + + test('Extend selection by cell above initial range above cell', async () => { + const mockTextEditor = initializeMockTextEditor( + codeWatcher, + documentManager, + `testing_L0 +# %% +testing_L2 +testing_L3 +# %% +testing_L5 +testing_L6 +# %% +testing_L8` + ); + + mockTextEditor.selection = new Selection(8, 2, 5, 2); + + await codeWatcher.extendSelectionByCellAbove(); + + expect(mockTextEditor.selection.anchor.line).to.equal(8); + expect(mockTextEditor.selection.anchor.character).to.equal(10); + expect(mockTextEditor.selection.active.line).to.equal(4); + expect(mockTextEditor.selection.active.character).to.equal(0); + }); + + test('Extend selection by cell above expand above', async () => { + const mockTextEditor = initializeMockTextEditor( + codeWatcher, + documentManager, + `testing_L0 +# %% +testing_L2 +testing_L3 +# %% +testing_L5 +testing_L6 +# %% +testing_L8` + ); + + mockTextEditor.selection = new Selection(6, 10, 4, 0); + + await codeWatcher.extendSelectionByCellAbove(); + + expect(mockTextEditor.selection.anchor.line).to.equal(6); + expect(mockTextEditor.selection.anchor.character).to.equal(10); + expect(mockTextEditor.selection.active.line).to.equal(1); + expect(mockTextEditor.selection.active.character).to.equal(0); + }); + + test('Extend selection by cell above contract below', async () => { + const mockTextEditor = initializeMockTextEditor( + codeWatcher, + documentManager, + `testing_L0 +# %% +testing_L2 +testing_L3 +# %% +testing_L5 +testing_L6 +# %% +testing_L8` + ); + + mockTextEditor.selection = new Selection(1, 0, 6, 10); + + await codeWatcher.extendSelectionByCellAbove(); + + expect(mockTextEditor.selection.anchor.line).to.equal(1); + expect(mockTextEditor.selection.anchor.character).to.equal(0); + expect(mockTextEditor.selection.active.line).to.equal(3); + expect(mockTextEditor.selection.active.character).to.equal(10); + }); }); From 9a073aa194f234725733417fe1cccf5af3e4de0e Mon Sep 17 00:00:00 2001 From: earthastronaut <> Date: Sun, 21 Jun 2020 23:57:46 -0600 Subject: [PATCH 11/26] add extendSelectionByCellBelow * Works similar to excell extend selection by one cell below * Linked command to python.datascience.extendSelectionByCellBelow --- package.json | 16 ++ package.nls.json | 1 + src/client/common/application/commands.ts | 1 + .../datascience/commands/commandRegistry.ts | 10 + src/client/datascience/constants.ts | 1 + .../editor-integration/codewatcher.ts | 62 ++++++ src/client/datascience/types.ts | 1 + .../codewatcher.unit.test.ts | 207 ++++++++++++++++++ 8 files changed, 299 insertions(+) diff --git a/package.json b/package.json index 9fd269c58b61..1eb43a8b88ac 100644 --- a/package.json +++ b/package.json @@ -538,6 +538,11 @@ "title": "%python.command.python.datascience.extendSelectionByCellAbove.title%", "category": "Python" }, + { + "command": "python.datascience.extendSelectionByCellBelow", + "title": "%python.command.python.datascience.extendSelectionByCellBelow.title%", + "category": "Python" + }, { "command": "python.datascience.runcurrentcelladvance", "title": "%python.command.python.datascience.runcurrentcelladvance.title%", @@ -847,6 +852,11 @@ "command": "python.datascience.extendSelectionByCellAbove", "group": "Python2" }, + { + "when": "editorFocus && editorLangId == python && python.datascience.hascodecells && python.datascience.featureenabled && !notebookEditorFocused", + "command": "python.datascience.extendSelectionByCellBelow", + "group": "Python2" + }, { "when": "editorFocus && editorLangId == python && python.datascience.hascodecells && python.datascience.featureenabled && !notebookEditorFocused", "command": "python.datascience.runcurrentcell", @@ -1054,6 +1064,12 @@ "category": "Python", "when": "python.datascience.hascodecells && python.datascience.featureenabled && python.datascience.ispythonornativeactive" }, + { + "command": "python.datascience.extendSelectionByCellBelow", + "title": "%python.command.python.datascience.extendSelectionByCellBelow.title%", + "category": "Python", + "when": "python.datascience.hascodecells && python.datascience.featureenabled && python.datascience.ispythonornativeactive" + }, { "command": "python.datascience.runcurrentcell", "title": "%python.command.python.datascience.runcurrentcell.title%", diff --git a/package.nls.json b/package.nls.json index ac137d5ec94a..5b9778779bb6 100644 --- a/package.nls.json +++ b/package.nls.json @@ -71,6 +71,7 @@ "python.command.python.datascience.selectCell.title": "Select Cell", "python.command.python.datascience.selectCellContents.title": "Select Cell Contents", "python.command.python.datascience.extendSelectionByCellAbove.title": "Extend Selection By Cell Above", + "python.command.python.datascience.extendSelectionByCellBelow.title": "Extend Selection By Cell Below", "python.command.python.datascience.showhistorypane.title": "Show Python Interactive Window", "python.command.python.datascience.selectjupyteruri.title": "Specify local or remote Jupyter server for connections", "python.command.python.datascience.selectjupytercommandline.title": "Specify Jupyter command line arguments", diff --git a/src/client/common/application/commands.ts b/src/client/common/application/commands.ts index 9d32ee8ca587..619270e5fd14 100644 --- a/src/client/common/application/commands.ts +++ b/src/client/common/application/commands.ts @@ -177,6 +177,7 @@ export interface ICommandNameArgumentTypeMapping extends ICommandNameWithoutArgu [DSCommands.SelectCell]: []; [DSCommands.SelectCellContents]: []; [DSCommands.ExtendSelectionByCellAbove]: []; + [DSCommands.ExtendSelectionByCellBelow]: []; [DSCommands.ScrollToCell]: [string, string]; [DSCommands.ViewJupyterOutput]: []; [DSCommands.ExportAsPythonScript]: [INotebookModel]; diff --git a/src/client/datascience/commands/commandRegistry.ts b/src/client/datascience/commands/commandRegistry.ts index 1bf35b727a6b..98c271e52952 100644 --- a/src/client/datascience/commands/commandRegistry.ts +++ b/src/client/datascience/commands/commandRegistry.ts @@ -69,6 +69,7 @@ export class CommandRegistry implements IDisposable { this.registerCommand(Commands.SelectCell, this.selectCell); this.registerCommand(Commands.SelectCellContents, this.selectCellContents); this.registerCommand(Commands.ExtendSelectionByCellAbove, this.extendSelectionByCellAbove); + this.registerCommand(Commands.ExtendSelectionByCellBelow, this.extendSelectionByCellBelow); this.registerCommand(Commands.RunAllCellsAbovePalette, this.runAllCellsAboveFromCursor); this.registerCommand(Commands.RunCellAndAllBelowPalette, this.runCellAndAllBelowFromCursor); this.registerCommand(Commands.RunToLine, this.runToLine); @@ -375,6 +376,15 @@ export class CommandRegistry implements IDisposable { } } + private async extendSelectionByCellBelow(): Promise { + const activeCodeWatcher = this.getCurrentCodeWatcher(); + if (activeCodeWatcher) { + return activeCodeWatcher.extendSelectionByCellBelow(); + } else { + return Promise.resolve(); + } + } + private async runAllCellsAboveFromCursor(): Promise { const currentCodeLens = this.getCurrentCodeLens(); if (currentCodeLens) { diff --git a/src/client/datascience/constants.ts b/src/client/datascience/constants.ts index b424ecf2a07b..04fd861a76e7 100644 --- a/src/client/datascience/constants.ts +++ b/src/client/datascience/constants.ts @@ -84,6 +84,7 @@ export namespace Commands { export const SelectCell = 'python.datascience.selectCell'; export const SelectCellContents = 'python.datascience.selectCellContents'; export const ExtendSelectionByCellAbove = 'python.datascience.extendSelectionByCellAbove'; + export const ExtendSelectionByCellBelow = 'python.datascience.extendSelectionByCellBelow'; export const ScrollToCell = 'python.datascience.scrolltocell'; export const CreateNewNotebook = 'python.datascience.createnewnotebook'; export const ViewJupyterOutput = 'python.datascience.viewJupyterOutput'; diff --git a/src/client/datascience/editor-integration/codewatcher.ts b/src/client/datascience/editor-integration/codewatcher.ts index c2929c57d614..db03efef4882 100644 --- a/src/client/datascience/editor-integration/codewatcher.ts +++ b/src/client/datascience/editor-integration/codewatcher.ts @@ -567,6 +567,68 @@ export class CodeWatcher implements ICodeWatcher { } } + public async extendSelectionByCellBelow(): Promise { + // This behaves similarly to excel "Extend Selection by One Cell Above". + // The direction of the selection matters (i.e. where the active cursor) + // position is. First, it ensures that complete cells are selection. + // If so, then if active cursor is in cells below it expands the + // selection range. If the active cursor is above, it contracts the + // selection range. + const editor = this.documentManager.activeTextEditor; + if (!editor || !editor.selection) { + return Promise.resolve(); + } + const currentSelection = editor.selection; + const startEndCellIndex = this.getStartEndCellIndex(editor.selection); + if (!startEndCellIndex) { + return Promise.resolve(); + } + + const isAnchorLessEqualActive = editor.selection.anchor.isBeforeOrEqual(editor.selection.active); + + const cells = this.codeLensFactory.cells; + const startCellIndex = startEndCellIndex[0]; + const endCellIndex = startEndCellIndex[1]; + const startCell = cells[startCellIndex]; + const endCell = cells[endCellIndex]; + + if ( + !startCell.range.start.isEqual(currentSelection.start) || + !endCell.range.end.isEqual(currentSelection.end) + ) { + // full cell range not selected, first select a full cell range. + let selection: Selection; + if (isAnchorLessEqualActive) { + // active at start of start cell. + selection = new Selection(startCell.range.start, endCell.range.end); + } else { + if (startCellIndex < endCellIndex) { + // active at end of cell before endCell + selection = new Selection(cells[startCellIndex + 1].range.start, endCell.range.end); + } else { + // active at end of startCell + selection = new Selection(endCell.range.start, endCell.range.end); + } + } + editor.selection = selection; + } else { + // full cell range is selected now decide if expanding or contracting? + if (isAnchorLessEqualActive || startCellIndex === endCellIndex) { + // anchor is above active, expand selection by cell below. + if (endCellIndex < cells.length - 1) { + const extendCell = cells[endCellIndex + 1]; + editor.selection = new Selection(startCell.range.start, extendCell.range.end); + } + } else { + // anchor is below active, contract selection by cell above. + if (startCellIndex < endCellIndex) { + const contractCell = cells[startCellIndex + 1]; + editor.selection = new Selection(endCell.range.end, contractCell.range.start); + } + } + } + } + private getStartEndCells(selection: Selection): ICellRange[] | undefined { const startEndCellIndex = this.getStartEndCellIndex(selection); if (startEndCellIndex) { diff --git a/src/client/datascience/types.ts b/src/client/datascience/types.ts index cc13484a160e..bd5ce9ef0a01 100644 --- a/src/client/datascience/types.ts +++ b/src/client/datascience/types.ts @@ -634,6 +634,7 @@ export interface ICodeWatcher { selectCell(): Promise; selectCellContents(): Promise; extendSelectionByCellAbove(): Promise; + extendSelectionByCellBelow(): Promise; debugCurrentCell(): Promise; } diff --git a/src/test/datascience/editor-integration/codewatcher.unit.test.ts b/src/test/datascience/editor-integration/codewatcher.unit.test.ts index 927c46451493..7d1e02d62121 100644 --- a/src/test/datascience/editor-integration/codewatcher.unit.test.ts +++ b/src/test/datascience/editor-integration/codewatcher.unit.test.ts @@ -1681,4 +1681,211 @@ testing_L8` expect(mockTextEditor.selection.active.line).to.equal(3); expect(mockTextEditor.selection.active.character).to.equal(10); }); + + test('Extend selection by cell below initial select', async () => { + const mockTextEditor = initializeMockTextEditor( + codeWatcher, + documentManager, + `testing_L0 +# %% +testing_L2 +testing_L3 +# %% +testing_L5 +testing_L6 +# %% +testing_L8` + ); + + mockTextEditor.selection = new Selection(5, 2, 5, 2); + + await codeWatcher.extendSelectionByCellBelow(); + + expect(mockTextEditor.selection.anchor.line).to.equal(4); + expect(mockTextEditor.selection.anchor.character).to.equal(0); + expect(mockTextEditor.selection.active.line).to.equal(6); + expect(mockTextEditor.selection.active.character).to.equal(10); + }); + + test('Extend selection by cell below initial range in cell', async () => { + const mockTextEditor = initializeMockTextEditor( + codeWatcher, + documentManager, + `testing_L0 +# %% +testing_L2 +testing_L3 +# %% +testing_L5 +testing_L6 +# %% +testing_L8` + ); + + mockTextEditor.selection = new Selection(5, 2, 6, 4); + + await codeWatcher.extendSelectionByCellBelow(); + + expect(mockTextEditor.selection.anchor.line).to.equal(4); + expect(mockTextEditor.selection.anchor.character).to.equal(0); + expect(mockTextEditor.selection.active.line).to.equal(6); + expect(mockTextEditor.selection.active.character).to.equal(10); + }); + + test('Extend selection by cell below initial range in cell opposite direction', async () => { + const mockTextEditor = initializeMockTextEditor( + codeWatcher, + documentManager, + `testing_L0 +# %% +testing_L2 +testing_L3 +# %% +testing_L5 +testing_L6 +# %% +testing_L8` + ); + + mockTextEditor.selection = new Selection(6, 4, 5, 2); + + await codeWatcher.extendSelectionByCellBelow(); + + expect(mockTextEditor.selection.anchor.line).to.equal(4); + expect(mockTextEditor.selection.anchor.character).to.equal(0); + expect(mockTextEditor.selection.active.line).to.equal(6); + expect(mockTextEditor.selection.active.character).to.equal(10); + }); + + test('Extend selection by cell below initial range below cell', async () => { + const mockTextEditor = initializeMockTextEditor( + codeWatcher, + documentManager, + `testing_L0 +# %% +testing_L2 +testing_L3 +# %% +testing_L5 +testing_L6 +# %% +testing_L8` + ); + + mockTextEditor.selection = new Selection(3, 2, 6, 2); + + await codeWatcher.extendSelectionByCellBelow(); + + expect(mockTextEditor.selection.anchor.line).to.equal(1); + expect(mockTextEditor.selection.anchor.character).to.equal(0); + expect(mockTextEditor.selection.active.line).to.equal(6); + expect(mockTextEditor.selection.active.character).to.equal(10); + }); + + test('Extend selection by cell below initial range above cell', async () => { + const mockTextEditor = initializeMockTextEditor( + codeWatcher, + documentManager, + `testing_L0 +# %% +testing_L2 +testing_L3 +# %% +testing_L5 +testing_L6 +# %% +testing_L8` + ); + + mockTextEditor.selection = new Selection(6, 2, 3, 2); + + await codeWatcher.extendSelectionByCellBelow(); + + expect(mockTextEditor.selection.anchor.line).to.equal(4); + expect(mockTextEditor.selection.anchor.character).to.equal(0); + expect(mockTextEditor.selection.active.line).to.equal(6); + expect(mockTextEditor.selection.active.character).to.equal(10); + }); + + test('Extend selection by cell below expand below', async () => { + const mockTextEditor = initializeMockTextEditor( + codeWatcher, + documentManager, + `testing_L0 +# %% +testing_L2 +testing_L3 +# %% +testing_L5 +testing_L6 +# %% +testing_L8` + ); + + mockTextEditor.selection = new Selection(6, 10, 4, 0); + + await codeWatcher.extendSelectionByCellBelow(); + + expect(mockTextEditor.selection.anchor.line).to.equal(4); + expect(mockTextEditor.selection.anchor.character).to.equal(0); + expect(mockTextEditor.selection.active.line).to.equal(8); + expect(mockTextEditor.selection.active.character).to.equal(10); + }); + + test('Extend selection by cell below contract above', async () => { + const mockTextEditor = initializeMockTextEditor( + codeWatcher, + documentManager, + `testing_L0 +# %% +testing_L2 +testing_L3 +# %% +testing_L5 +testing_L6 +# %% +testing_L8` + ); + + mockTextEditor.selection = new Selection(6, 10, 1, 0); + + await codeWatcher.extendSelectionByCellBelow(); + + expect(mockTextEditor.selection.anchor.line).to.equal(6); + expect(mockTextEditor.selection.anchor.character).to.equal(10); + expect(mockTextEditor.selection.active.line).to.equal(4); + expect(mockTextEditor.selection.active.character).to.equal(0); + }); + + test('Extend selection by cell above and below', async () => { + const mockTextEditor = initializeMockTextEditor( + codeWatcher, + documentManager, + `testing_L0 +# %% +testing_L2 +testing_L3 +# %% +testing_L5 +testing_L6 +# %% +testing_L8` + ); + + mockTextEditor.selection = new Selection(5, 2, 6, 2); + + await codeWatcher.extendSelectionByCellAbove(); // select full cell + await codeWatcher.extendSelectionByCellAbove(); // select cell above + await codeWatcher.extendSelectionByCellAbove(); // top cell no change + await codeWatcher.extendSelectionByCellAbove(); // top cell no change + await codeWatcher.extendSelectionByCellBelow(); // contract by cell + await codeWatcher.extendSelectionByCellBelow(); // expand by cell below + await codeWatcher.extendSelectionByCellBelow(); // last cell no change + await codeWatcher.extendSelectionByCellAbove(); // Original cell + + expect(mockTextEditor.selection.anchor.line).to.equal(4); + expect(mockTextEditor.selection.anchor.character).to.equal(0); + expect(mockTextEditor.selection.active.line).to.equal(6); + expect(mockTextEditor.selection.active.character).to.equal(10); + }); }); From 4a95409f50f74aaf855a4cf6c0981e515a3cdf13 Mon Sep 17 00:00:00 2001 From: earthastronaut <> Date: Tue, 23 Jun 2020 16:43:03 -0600 Subject: [PATCH 12/26] add moveCellsUp * Command to take selected cells and move them one cell up * linked command to python.datascience.moveCellsUp --- package.json | 16 +++ package.nls.json | 1 + src/client/common/application/commands.ts | 1 + .../datascience/commands/commandRegistry.ts | 10 ++ src/client/datascience/constants.ts | 1 + .../editor-integration/codewatcher.ts | 125 ++++++++++++++++++ src/client/datascience/types.ts | 1 + .../codewatcher.unit.test.ts | 108 +++++++++++++++ 8 files changed, 263 insertions(+) diff --git a/package.json b/package.json index 1eb43a8b88ac..d508b701b8ee 100644 --- a/package.json +++ b/package.json @@ -543,6 +543,11 @@ "title": "%python.command.python.datascience.extendSelectionByCellBelow.title%", "category": "Python" }, + { + "command": "python.datascience.moveCellsUp", + "title": "%python.command.python.datascience.moveCellsUp.title%", + "category": "Python" + }, { "command": "python.datascience.runcurrentcelladvance", "title": "%python.command.python.datascience.runcurrentcelladvance.title%", @@ -857,6 +862,11 @@ "command": "python.datascience.extendSelectionByCellBelow", "group": "Python2" }, + { + "when": "editorFocus && editorLangId == python && python.datascience.hascodecells && python.datascience.featureenabled && !notebookEditorFocused", + "command": "python.datascience.moveCellsUp", + "group": "Python2" + }, { "when": "editorFocus && editorLangId == python && python.datascience.hascodecells && python.datascience.featureenabled && !notebookEditorFocused", "command": "python.datascience.runcurrentcell", @@ -1070,6 +1080,12 @@ "category": "Python", "when": "python.datascience.hascodecells && python.datascience.featureenabled && python.datascience.ispythonornativeactive" }, + { + "command": "python.datascience.moveCellsUp", + "title": "%python.command.python.datascience.moveCellsUp.title%", + "category": "Python", + "when": "python.datascience.hascodecells && python.datascience.featureenabled && python.datascience.ispythonornativeactive" + }, { "command": "python.datascience.runcurrentcell", "title": "%python.command.python.datascience.runcurrentcell.title%", diff --git a/package.nls.json b/package.nls.json index 5b9778779bb6..48cdbc0804ae 100644 --- a/package.nls.json +++ b/package.nls.json @@ -72,6 +72,7 @@ "python.command.python.datascience.selectCellContents.title": "Select Cell Contents", "python.command.python.datascience.extendSelectionByCellAbove.title": "Extend Selection By Cell Above", "python.command.python.datascience.extendSelectionByCellBelow.title": "Extend Selection By Cell Below", + "python.command.python.datascience.moveCellsUp.title": "Move Selected Cells Up", "python.command.python.datascience.showhistorypane.title": "Show Python Interactive Window", "python.command.python.datascience.selectjupyteruri.title": "Specify local or remote Jupyter server for connections", "python.command.python.datascience.selectjupytercommandline.title": "Specify Jupyter command line arguments", diff --git a/src/client/common/application/commands.ts b/src/client/common/application/commands.ts index 619270e5fd14..d9feb1390425 100644 --- a/src/client/common/application/commands.ts +++ b/src/client/common/application/commands.ts @@ -178,6 +178,7 @@ export interface ICommandNameArgumentTypeMapping extends ICommandNameWithoutArgu [DSCommands.SelectCellContents]: []; [DSCommands.ExtendSelectionByCellAbove]: []; [DSCommands.ExtendSelectionByCellBelow]: []; + [DSCommands.MoveCellsUp]: []; [DSCommands.ScrollToCell]: [string, string]; [DSCommands.ViewJupyterOutput]: []; [DSCommands.ExportAsPythonScript]: [INotebookModel]; diff --git a/src/client/datascience/commands/commandRegistry.ts b/src/client/datascience/commands/commandRegistry.ts index 98c271e52952..d45aa5011be2 100644 --- a/src/client/datascience/commands/commandRegistry.ts +++ b/src/client/datascience/commands/commandRegistry.ts @@ -70,6 +70,7 @@ export class CommandRegistry implements IDisposable { this.registerCommand(Commands.SelectCellContents, this.selectCellContents); this.registerCommand(Commands.ExtendSelectionByCellAbove, this.extendSelectionByCellAbove); this.registerCommand(Commands.ExtendSelectionByCellBelow, this.extendSelectionByCellBelow); + this.registerCommand(Commands.MoveCellsUp, this.moveCellsUp); this.registerCommand(Commands.RunAllCellsAbovePalette, this.runAllCellsAboveFromCursor); this.registerCommand(Commands.RunCellAndAllBelowPalette, this.runCellAndAllBelowFromCursor); this.registerCommand(Commands.RunToLine, this.runToLine); @@ -385,6 +386,15 @@ export class CommandRegistry implements IDisposable { } } + private async moveCellsUp(): Promise { + const activeCodeWatcher = this.getCurrentCodeWatcher(); + if (activeCodeWatcher) { + return activeCodeWatcher.moveCellsUp(); + } else { + return Promise.resolve(); + } + } + private async runAllCellsAboveFromCursor(): Promise { const currentCodeLens = this.getCurrentCodeLens(); if (currentCodeLens) { diff --git a/src/client/datascience/constants.ts b/src/client/datascience/constants.ts index 04fd861a76e7..736418f2b0c1 100644 --- a/src/client/datascience/constants.ts +++ b/src/client/datascience/constants.ts @@ -85,6 +85,7 @@ export namespace Commands { export const SelectCellContents = 'python.datascience.selectCellContents'; export const ExtendSelectionByCellAbove = 'python.datascience.extendSelectionByCellAbove'; export const ExtendSelectionByCellBelow = 'python.datascience.extendSelectionByCellBelow'; + export const MoveCellsUp = 'python.datascience.moveCellsUp'; export const ScrollToCell = 'python.datascience.scrolltocell'; export const CreateNewNotebook = 'python.datascience.createnewnotebook'; export const ViewJupyterOutput = 'python.datascience.viewJupyterOutput'; diff --git a/src/client/datascience/editor-integration/codewatcher.ts b/src/client/datascience/editor-integration/codewatcher.ts index db03efef4882..500c939f85ad 100644 --- a/src/client/datascience/editor-integration/codewatcher.ts +++ b/src/client/datascience/editor-integration/codewatcher.ts @@ -629,6 +629,131 @@ export class CodeWatcher implements ICodeWatcher { } } + public async moveCellsUp(): Promise { + return this.moveCellsDirection(true); + } + + private async moveCellsDirection(directionUp: boolean): Promise { + const editor = this.documentManager.activeTextEditor; + if (!editor || !editor.selection) { + return Promise.resolve(); + } + const startEndCellIndex = this.getStartEndCellIndex(editor.selection); + if (!startEndCellIndex) { + return Promise.resolve(); + } + const startCellIndex = startEndCellIndex[0]; + const endCellIndex = startEndCellIndex[1]; + const cells = this.codeLensFactory.cells; + const startCell = cells[startCellIndex]; + const endCell = cells[endCellIndex]; + if (!startCell || !endCell) { + return Promise.resolve(); + } + const currentRange = new Range(startCell.range.start, endCell.range.end); + const relativeSelectionRange = new Range( + editor.selection.start.line - currentRange.start.line, + editor.selection.start.character, + editor.selection.end.line - currentRange.start.line, + editor.selection.end.character + ); + const isActiveBeforeAnchor = editor.selection.active.isBefore(editor.selection.anchor); + let thenSetSelection: Thenable; + if (directionUp) { + if (startCellIndex === 0) { + return Promise.resolve(); + } else { + const aboveCell = cells[startCellIndex - 1]; + const thenExchangeTextLines = this.exchangeTextLines(editor, aboveCell.range, currentRange); + thenSetSelection = thenExchangeTextLines.then((isEditSuccessful) => { + if (isEditSuccessful) { + editor.selection = new Selection( + aboveCell.range.start.line + relativeSelectionRange.start.line, + relativeSelectionRange.start.character, + aboveCell.range.start.line + relativeSelectionRange.end.line, + relativeSelectionRange.end.character + ); + } + return isEditSuccessful; + }); + } + } else { + if (endCellIndex === cells.length - 1) { + return Promise.resolve(); + } else { + const belowCell = cells[endCellIndex + 1]; + const thenExchangeTextLines = this.exchangeTextLines(editor, currentRange, belowCell.range); + const belowCellLineLength = belowCell.range.end.line - belowCell.range.start.line; + const aboveCellLineLength = currentRange.end.line - currentRange.start.line; + const diffCellLineLength = belowCellLineLength - aboveCellLineLength; + thenSetSelection = thenExchangeTextLines.then((isEditSuccessful) => { + if (isEditSuccessful) { + editor.selection = new Selection( + belowCell.range.start.line + diffCellLineLength + relativeSelectionRange.start.line, + relativeSelectionRange.start.character, + belowCell.range.start.line + diffCellLineLength + relativeSelectionRange.end.line, + relativeSelectionRange.end.character + ); + } + return isEditSuccessful; + }); + } + } + return thenSetSelection.then((isEditSuccessful) => { + if (isEditSuccessful && isActiveBeforeAnchor) { + editor.selection = new Selection(editor.selection.active, editor.selection.anchor); + } + return Promise.resolve(); + }); + } + + private exchangeTextLines(editor: TextEditor, aboveRange: Range, belowRange: Range): Thenable { + const aboveStartLine = aboveRange.start.line; + const aboveEndLine = aboveRange.end.line; + const belowStartLine = belowRange.start.line; + const belowEndLine = belowRange.end.line; + + if (aboveEndLine >= belowStartLine) { + throw RangeError(`Above lines must be fully above not ${aboveEndLine} <= ${belowStartLine}`); + } + + const above = new Range( + aboveStartLine, + 0, + aboveEndLine, + editor.document.lineAt(aboveEndLine).range.end.character + ); + const aboveText = editor.document.getText(above); + + const below = new Range( + belowStartLine, + 0, + belowEndLine, + editor.document.lineAt(belowEndLine).range.end.character + ); + const belowText = editor.document.getText(below); + + let betweenText = ''; + if (aboveEndLine + 1 < belowStartLine) { + const betweenStatLine = aboveEndLine + 1; + const betweenEndLine = belowStartLine - 1; + const between = new Range( + betweenStatLine, + 0, + betweenEndLine, + editor.document.lineAt(betweenEndLine).range.end.character + ); + betweenText = `${editor.document.getText(between)}\n`; + } + + const newText = `${belowText}\n${betweenText}${aboveText}`; + const newRange = new Range(above.start, below.end); + return editor.edit((editBuilder) => { + editBuilder.replace(newRange, newText); + this.codeLensUpdatedEvent.fire(); + }); + } + private getStartEndCells(selection: Selection): ICellRange[] | undefined { const startEndCellIndex = this.getStartEndCellIndex(selection); if (startEndCellIndex) { diff --git a/src/client/datascience/types.ts b/src/client/datascience/types.ts index bd5ce9ef0a01..874344b05089 100644 --- a/src/client/datascience/types.ts +++ b/src/client/datascience/types.ts @@ -635,6 +635,7 @@ export interface ICodeWatcher { selectCellContents(): Promise; extendSelectionByCellAbove(): Promise; extendSelectionByCellBelow(): Promise; + moveCellsUp(): Promise; debugCurrentCell(): Promise; } diff --git a/src/test/datascience/editor-integration/codewatcher.unit.test.ts b/src/test/datascience/editor-integration/codewatcher.unit.test.ts index 7d1e02d62121..6a970a38f005 100644 --- a/src/test/datascience/editor-integration/codewatcher.unit.test.ts +++ b/src/test/datascience/editor-integration/codewatcher.unit.test.ts @@ -1888,4 +1888,112 @@ testing_L8` expect(mockTextEditor.selection.active.line).to.equal(6); expect(mockTextEditor.selection.active.character).to.equal(10); }); + + test('Move cells up', async () => { + const mockTextEditor = initializeMockTextEditor( + codeWatcher, + documentManager, + `testing_L0 +# %% +testing_L2 +testing_L3 +# %% +testing_L5 +testing_L6 +# %% +testing_L8` + ); + + mockTextEditor.selection = new Selection(5, 5, 5, 5); + + await codeWatcher.moveCellsUp(); + + expect(mockTextEditor.document.getText()).to.equal( + `testing_L0 +# %% +testing_L5 +testing_L6 +# %% +testing_L2 +testing_L3 +# %% +testing_L8` + ); + expect(mockTextEditor.selection.anchor.line).to.equal(2); + expect(mockTextEditor.selection.anchor.character).to.equal(5); + expect(mockTextEditor.selection.active.line).to.equal(2); + expect(mockTextEditor.selection.active.character).to.equal(5); + }); + + test('Move cells up multiple cells', async () => { + const mockTextEditor = initializeMockTextEditor( + codeWatcher, + documentManager, + `testing_L0 +# %% +testing_L2 +testing_L3 +# %% +testing_L5 +testing_L6 +# %% +testing_L8` + ); + + mockTextEditor.selection = new Selection(8, 8, 5, 5); + + await codeWatcher.moveCellsUp(); + + expect(mockTextEditor.document.getText()).to.equal( + `testing_L0 +# %% +testing_L5 +testing_L6 +# %% +testing_L8 +# %% +testing_L2 +testing_L3` + ); + expect(mockTextEditor.selection.anchor.line).to.equal(5); + expect(mockTextEditor.selection.anchor.character).to.equal(8); + expect(mockTextEditor.selection.active.line).to.equal(2); + expect(mockTextEditor.selection.active.character).to.equal(5); + }); + + test('Move cells up first cell no change', async () => { + const mockTextEditor = initializeMockTextEditor( + codeWatcher, + documentManager, + `testing_L0 +# %% +testing_L2 +testing_L3 +# %% +testing_L5 +testing_L6 +# %% +testing_L8` + ); + + mockTextEditor.selection = new Selection(1, 2, 5, 5); + + await codeWatcher.moveCellsUp(); + + expect(mockTextEditor.document.getText()).to.equal( + `testing_L0 +# %% +testing_L2 +testing_L3 +# %% +testing_L5 +testing_L6 +# %% +testing_L8` + ); + expect(mockTextEditor.selection.anchor.line).to.equal(1); + expect(mockTextEditor.selection.anchor.character).to.equal(2); + expect(mockTextEditor.selection.active.line).to.equal(5); + expect(mockTextEditor.selection.active.character).to.equal(5); + }); }); From 95fc70d0b36b1b88ef1f9055b7ea9a9ef30053b0 Mon Sep 17 00:00:00 2001 From: earthastronaut <> Date: Tue, 23 Jun 2020 16:53:42 -0600 Subject: [PATCH 13/26] add moveCellsDown * Moves selected cells one cell down * Linked to python.datascience.moveCellsDown --- package.json | 16 ++++++++++++++++ package.nls.json | 1 + src/client/common/application/commands.ts | 1 + .../datascience/commands/commandRegistry.ts | 10 ++++++++++ src/client/datascience/constants.ts | 1 + .../editor-integration/codewatcher.ts | 4 ++++ src/client/datascience/types.ts | 1 + 7 files changed, 34 insertions(+) diff --git a/package.json b/package.json index d508b701b8ee..d5a832688888 100644 --- a/package.json +++ b/package.json @@ -548,6 +548,11 @@ "title": "%python.command.python.datascience.moveCellsUp.title%", "category": "Python" }, + { + "command": "python.datascience.moveCellsDown", + "title": "%python.command.python.datascience.moveCellsDown.title%", + "category": "Python" + }, { "command": "python.datascience.runcurrentcelladvance", "title": "%python.command.python.datascience.runcurrentcelladvance.title%", @@ -867,6 +872,11 @@ "command": "python.datascience.moveCellsUp", "group": "Python2" }, + { + "when": "editorFocus && editorLangId == python && python.datascience.hascodecells && python.datascience.featureenabled && !notebookEditorFocused", + "command": "python.datascience.moveCellsDown", + "group": "Python2" + }, { "when": "editorFocus && editorLangId == python && python.datascience.hascodecells && python.datascience.featureenabled && !notebookEditorFocused", "command": "python.datascience.runcurrentcell", @@ -1086,6 +1096,12 @@ "category": "Python", "when": "python.datascience.hascodecells && python.datascience.featureenabled && python.datascience.ispythonornativeactive" }, + { + "command": "python.datascience.moveCellsDown", + "title": "%python.command.python.datascience.moveCellsDown.title%", + "category": "Python", + "when": "python.datascience.hascodecells && python.datascience.featureenabled && python.datascience.ispythonornativeactive" + }, { "command": "python.datascience.runcurrentcell", "title": "%python.command.python.datascience.runcurrentcell.title%", diff --git a/package.nls.json b/package.nls.json index 48cdbc0804ae..091435db1c45 100644 --- a/package.nls.json +++ b/package.nls.json @@ -73,6 +73,7 @@ "python.command.python.datascience.extendSelectionByCellAbove.title": "Extend Selection By Cell Above", "python.command.python.datascience.extendSelectionByCellBelow.title": "Extend Selection By Cell Below", "python.command.python.datascience.moveCellsUp.title": "Move Selected Cells Up", + "python.command.python.datascience.moveCellsDown.title": "Move Selected Cells Down", "python.command.python.datascience.showhistorypane.title": "Show Python Interactive Window", "python.command.python.datascience.selectjupyteruri.title": "Specify local or remote Jupyter server for connections", "python.command.python.datascience.selectjupytercommandline.title": "Specify Jupyter command line arguments", diff --git a/src/client/common/application/commands.ts b/src/client/common/application/commands.ts index d9feb1390425..1f694db510fb 100644 --- a/src/client/common/application/commands.ts +++ b/src/client/common/application/commands.ts @@ -179,6 +179,7 @@ export interface ICommandNameArgumentTypeMapping extends ICommandNameWithoutArgu [DSCommands.ExtendSelectionByCellAbove]: []; [DSCommands.ExtendSelectionByCellBelow]: []; [DSCommands.MoveCellsUp]: []; + [DSCommands.MoveCellsDown]: []; [DSCommands.ScrollToCell]: [string, string]; [DSCommands.ViewJupyterOutput]: []; [DSCommands.ExportAsPythonScript]: [INotebookModel]; diff --git a/src/client/datascience/commands/commandRegistry.ts b/src/client/datascience/commands/commandRegistry.ts index d45aa5011be2..333b168a345d 100644 --- a/src/client/datascience/commands/commandRegistry.ts +++ b/src/client/datascience/commands/commandRegistry.ts @@ -71,6 +71,7 @@ export class CommandRegistry implements IDisposable { this.registerCommand(Commands.ExtendSelectionByCellAbove, this.extendSelectionByCellAbove); this.registerCommand(Commands.ExtendSelectionByCellBelow, this.extendSelectionByCellBelow); this.registerCommand(Commands.MoveCellsUp, this.moveCellsUp); + this.registerCommand(Commands.MoveCellsDown, this.moveCellsDown); this.registerCommand(Commands.RunAllCellsAbovePalette, this.runAllCellsAboveFromCursor); this.registerCommand(Commands.RunCellAndAllBelowPalette, this.runCellAndAllBelowFromCursor); this.registerCommand(Commands.RunToLine, this.runToLine); @@ -395,6 +396,15 @@ export class CommandRegistry implements IDisposable { } } + private async moveCellsDown(): Promise { + const activeCodeWatcher = this.getCurrentCodeWatcher(); + if (activeCodeWatcher) { + return activeCodeWatcher.moveCellsDown(); + } else { + return Promise.resolve(); + } + } + private async runAllCellsAboveFromCursor(): Promise { const currentCodeLens = this.getCurrentCodeLens(); if (currentCodeLens) { diff --git a/src/client/datascience/constants.ts b/src/client/datascience/constants.ts index 736418f2b0c1..2d24bf26a29d 100644 --- a/src/client/datascience/constants.ts +++ b/src/client/datascience/constants.ts @@ -86,6 +86,7 @@ export namespace Commands { export const ExtendSelectionByCellAbove = 'python.datascience.extendSelectionByCellAbove'; export const ExtendSelectionByCellBelow = 'python.datascience.extendSelectionByCellBelow'; export const MoveCellsUp = 'python.datascience.moveCellsUp'; + export const MoveCellsDown = 'python.datascience.moveCellsDown'; export const ScrollToCell = 'python.datascience.scrolltocell'; export const CreateNewNotebook = 'python.datascience.createnewnotebook'; export const ViewJupyterOutput = 'python.datascience.viewJupyterOutput'; diff --git a/src/client/datascience/editor-integration/codewatcher.ts b/src/client/datascience/editor-integration/codewatcher.ts index 500c939f85ad..c1cdc149b5ea 100644 --- a/src/client/datascience/editor-integration/codewatcher.ts +++ b/src/client/datascience/editor-integration/codewatcher.ts @@ -633,6 +633,10 @@ export class CodeWatcher implements ICodeWatcher { return this.moveCellsDirection(true); } + public async moveCellsDown(): Promise { + return this.moveCellsDirection(false); + } + private async moveCellsDirection(directionUp: boolean): Promise { const editor = this.documentManager.activeTextEditor; if (!editor || !editor.selection) { diff --git a/src/client/datascience/types.ts b/src/client/datascience/types.ts index 874344b05089..841665fb5a6f 100644 --- a/src/client/datascience/types.ts +++ b/src/client/datascience/types.ts @@ -636,6 +636,7 @@ export interface ICodeWatcher { extendSelectionByCellAbove(): Promise; extendSelectionByCellBelow(): Promise; moveCellsUp(): Promise; + moveCellsDown(): Promise; debugCurrentCell(): Promise; } From cc3749424af73c8145d1ffca512d409409b877c4 Mon Sep 17 00:00:00 2001 From: earthastronaut <> Date: Tue, 23 Jun 2020 21:21:12 -0600 Subject: [PATCH 14/26] added news file for enhancement changes being added --- news/1 Enhancements/12414.md | 1 + 1 file changed, 1 insertion(+) create mode 100644 news/1 Enhancements/12414.md diff --git a/news/1 Enhancements/12414.md b/news/1 Enhancements/12414.md new file mode 100644 index 000000000000..da6a9071c367 --- /dev/null +++ b/news/1 Enhancements/12414.md @@ -0,0 +1 @@ +Add cell editing shortcuts for python interactive cells. Thanks [@earthastronaut](https://github.com/earthastronaut/)! From 1f735554101d171005b3e518780d60782f6e8516 Mon Sep 17 00:00:00 2001 From: earthastronaut <> Date: Sat, 27 Jun 2020 21:52:12 -0600 Subject: [PATCH 15/26] add changeCellToMarkdown * command will use selected cell and if cell_type is not markdown will convert to markdown * linked to python.datascience.changeCellToMarkdown --- package.json | 16 ++++ package.nls.json | 1 + src/client/common/application/commands.ts | 1 + src/client/datascience/cellMatcher.ts | 5 +- .../datascience/commands/commandRegistry.ts | 10 +++ src/client/datascience/constants.ts | 1 + .../editor-integration/codewatcher.ts | 42 +++++++++- src/client/datascience/types.ts | 1 + .../codewatcher.unit.test.ts | 78 +++++++++++++++++++ 9 files changed, 152 insertions(+), 3 deletions(-) diff --git a/package.json b/package.json index d5a832688888..da0cb8cbe229 100644 --- a/package.json +++ b/package.json @@ -553,6 +553,11 @@ "title": "%python.command.python.datascience.moveCellsDown.title%", "category": "Python" }, + { + "command": "python.datascience.changeCellToMarkdown", + "title": "%python.command.python.datascience.changeCellToMarkdown.title%", + "category": "Python" + }, { "command": "python.datascience.runcurrentcelladvance", "title": "%python.command.python.datascience.runcurrentcelladvance.title%", @@ -877,6 +882,11 @@ "command": "python.datascience.moveCellsDown", "group": "Python2" }, + { + "when": "editorFocus && editorLangId == python && python.datascience.hascodecells && python.datascience.featureenabled && !notebookEditorFocused", + "command": "python.datascience.changeCellToMarkdown", + "group": "Python2" + }, { "when": "editorFocus && editorLangId == python && python.datascience.hascodecells && python.datascience.featureenabled && !notebookEditorFocused", "command": "python.datascience.runcurrentcell", @@ -1102,6 +1112,12 @@ "category": "Python", "when": "python.datascience.hascodecells && python.datascience.featureenabled && python.datascience.ispythonornativeactive" }, + { + "command": "python.datascience.changeCellToMarkdown", + "title": "%python.command.python.datascience.changeCellToMarkdown.title%", + "category": "Python", + "when": "python.datascience.hascodecells && python.datascience.featureenabled && python.datascience.ispythonornativeactive" + }, { "command": "python.datascience.runcurrentcell", "title": "%python.command.python.datascience.runcurrentcell.title%", diff --git a/package.nls.json b/package.nls.json index 091435db1c45..e6186091696e 100644 --- a/package.nls.json +++ b/package.nls.json @@ -74,6 +74,7 @@ "python.command.python.datascience.extendSelectionByCellBelow.title": "Extend Selection By Cell Below", "python.command.python.datascience.moveCellsUp.title": "Move Selected Cells Up", "python.command.python.datascience.moveCellsDown.title": "Move Selected Cells Down", + "python.command.python.datascience.changeCellToMarkdown.title": "Change Cell to Markdown", "python.command.python.datascience.showhistorypane.title": "Show Python Interactive Window", "python.command.python.datascience.selectjupyteruri.title": "Specify local or remote Jupyter server for connections", "python.command.python.datascience.selectjupytercommandline.title": "Specify Jupyter command line arguments", diff --git a/src/client/common/application/commands.ts b/src/client/common/application/commands.ts index 1f694db510fb..4e411287ad2f 100644 --- a/src/client/common/application/commands.ts +++ b/src/client/common/application/commands.ts @@ -180,6 +180,7 @@ export interface ICommandNameArgumentTypeMapping extends ICommandNameWithoutArgu [DSCommands.ExtendSelectionByCellBelow]: []; [DSCommands.MoveCellsUp]: []; [DSCommands.MoveCellsDown]: []; + [DSCommands.ChangeCellToMarkdown]: []; [DSCommands.ScrollToCell]: [string, string]; [DSCommands.ViewJupyterOutput]: []; [DSCommands.ExportAsPythonScript]: [INotebookModel]; diff --git a/src/client/datascience/cellMatcher.ts b/src/client/datascience/cellMatcher.ts index 53a89392fd97..1a6a61848983 100644 --- a/src/client/datascience/cellMatcher.ts +++ b/src/client/datascience/cellMatcher.ts @@ -8,10 +8,11 @@ import { noop } from '../common/utils/misc'; import { RegExpValues } from './constants'; export class CellMatcher { + public codeExecRegEx: RegExp; + public markdownExecRegEx: RegExp; + private codeMatchRegEx: RegExp; private markdownMatchRegEx: RegExp; - private codeExecRegEx: RegExp; - private markdownExecRegEx: RegExp; private defaultCellMarker: string; private defaultCellMarkerExec: RegExp; diff --git a/src/client/datascience/commands/commandRegistry.ts b/src/client/datascience/commands/commandRegistry.ts index 333b168a345d..171182f5742b 100644 --- a/src/client/datascience/commands/commandRegistry.ts +++ b/src/client/datascience/commands/commandRegistry.ts @@ -72,6 +72,7 @@ export class CommandRegistry implements IDisposable { this.registerCommand(Commands.ExtendSelectionByCellBelow, this.extendSelectionByCellBelow); this.registerCommand(Commands.MoveCellsUp, this.moveCellsUp); this.registerCommand(Commands.MoveCellsDown, this.moveCellsDown); + this.registerCommand(Commands.ChangeCellToMarkdown, this.changeCellToMarkdown); this.registerCommand(Commands.RunAllCellsAbovePalette, this.runAllCellsAboveFromCursor); this.registerCommand(Commands.RunCellAndAllBelowPalette, this.runCellAndAllBelowFromCursor); this.registerCommand(Commands.RunToLine, this.runToLine); @@ -405,6 +406,15 @@ export class CommandRegistry implements IDisposable { } } + private async changeCellToMarkdown(): Promise { + const activeCodeWatcher = this.getCurrentCodeWatcher(); + if (activeCodeWatcher) { + return activeCodeWatcher.changeCellToMarkdown(); + } else { + return Promise.resolve(); + } + } + private async runAllCellsAboveFromCursor(): Promise { const currentCodeLens = this.getCurrentCodeLens(); if (currentCodeLens) { diff --git a/src/client/datascience/constants.ts b/src/client/datascience/constants.ts index 2d24bf26a29d..7e370ec245f7 100644 --- a/src/client/datascience/constants.ts +++ b/src/client/datascience/constants.ts @@ -87,6 +87,7 @@ export namespace Commands { export const ExtendSelectionByCellBelow = 'python.datascience.extendSelectionByCellBelow'; export const MoveCellsUp = 'python.datascience.moveCellsUp'; export const MoveCellsDown = 'python.datascience.moveCellsDown'; + export const ChangeCellToMarkdown = 'python.datascience.changeCellToMarkdown'; export const ScrollToCell = 'python.datascience.scrolltocell'; export const CreateNewNotebook = 'python.datascience.createnewnotebook'; export const ViewJupyterOutput = 'python.datascience.viewJupyterOutput'; diff --git a/src/client/datascience/editor-integration/codewatcher.ts b/src/client/datascience/editor-integration/codewatcher.ts index c1cdc149b5ea..0315a4248b19 100644 --- a/src/client/datascience/editor-integration/codewatcher.ts +++ b/src/client/datascience/editor-integration/codewatcher.ts @@ -4,6 +4,7 @@ import { inject, injectable } from 'inversify'; import { CodeLens, + commands, Event, EventEmitter, Position, @@ -637,6 +638,45 @@ export class CodeWatcher implements ICodeWatcher { return this.moveCellsDirection(false); } + public async changeCellToMarkdown(): Promise { + const editor = this.documentManager.activeTextEditor; + const cell = this.getCellFromPosition(); + if (editor && cell) { + if (cell.cell_type === 'markdown') { + return Promise.resolve(); + } + + const cellMatcher = new CellMatcher(this.configService.getSettings(editor.document.uri).datascience); + const definitionLine = editor.document.lineAt(cell.range.start.line); + const definitionText = editor.document.getText(definitionLine.range); + const definitionMatch = cellMatcher.codeExecRegEx.exec(definitionText); + if (!definitionMatch) { + return Promise.resolve(); + } + const definitionExtra = definitionMatch[definitionMatch.length - 1]; + + const cellMarker = this.getDefaultCellMarker(editor.document.uri); + const newDefinitionText = `${cellMarker} [markdown]${definitionExtra}`; + await editor.edit(async (editBuilder) => { + editBuilder.replace(definitionLine.range, newDefinitionText); + cell.cell_type = 'markdown'; + if (cell.range.start.line < cell.range.end.line) { + editor.selection = new Selection( + cell.range.start.line + 1, + 0, + cell.range.end.line, + cell.range.end.character + ); + // ensure all lines in markdown cell have a comment. + // these are not included in the test because it's unclear + // how TypeMoq works with them. + await commands.executeCommand('editor.action.removeCommentLine'); + await commands.executeCommand('editor.action.addCommentLine'); + } + }); + } + } + private async moveCellsDirection(directionUp: boolean): Promise { const editor = this.documentManager.activeTextEditor; if (!editor || !editor.selection) { @@ -940,7 +980,7 @@ export class CodeWatcher implements ICodeWatcher { if (!position) { const editor = this.documentManager.activeTextEditor; if (editor && editor.selection) { - position = editor.selection.start; + position = editor.selection.active; } } if (position) { diff --git a/src/client/datascience/types.ts b/src/client/datascience/types.ts index 841665fb5a6f..76e9075fecd1 100644 --- a/src/client/datascience/types.ts +++ b/src/client/datascience/types.ts @@ -637,6 +637,7 @@ export interface ICodeWatcher { extendSelectionByCellBelow(): Promise; moveCellsUp(): Promise; moveCellsDown(): Promise; + changeCellToMarkdown(): Promise; debugCurrentCell(): Promise; } diff --git a/src/test/datascience/editor-integration/codewatcher.unit.test.ts b/src/test/datascience/editor-integration/codewatcher.unit.test.ts index 6a970a38f005..de6c33222852 100644 --- a/src/test/datascience/editor-integration/codewatcher.unit.test.ts +++ b/src/test/datascience/editor-integration/codewatcher.unit.test.ts @@ -1996,4 +1996,82 @@ testing_L8` expect(mockTextEditor.selection.active.line).to.equal(5); expect(mockTextEditor.selection.active.character).to.equal(5); }); + + test('Change cell to markdown', async () => { + const mockTextEditor = initializeMockTextEditor( + codeWatcher, + documentManager, + `testing_L0 +# %% +testing_L2 +testing_L3 +# %% extra +# # testing_L5 +testing_L6 + +` + ); + + mockTextEditor.selection = new Selection(1, 2, 5, 5); + + await codeWatcher.changeCellToMarkdown(); + + // NOTE: When running the function in real environment there + // are comment lines added in addition to the [markdown] definition. + // It is unclear with TypeMoq how to test this particular behavior because + // the external `commands.executeCommmands` is being proxied along with + // all subsequent calls. Essentially, I must rely on those functions + // being unit tested. + /* + actual expected = `testing_L0 +# %% +testing_L2 +testing_L3 +# %% [markdown] extra +# # testing_L5 +# testing_L6 + +` + */ + + expect(mockTextEditor.document.getText()).to.equal( + `testing_L0 +# %% +testing_L2 +testing_L3 +# %% [markdown] extra +# # testing_L5 +testing_L6 + +` + ); + expect(mockTextEditor.selection.anchor.line).to.equal(5); + expect(mockTextEditor.selection.anchor.character).to.equal(0); + expect(mockTextEditor.selection.active.line).to.equal(8); + expect(mockTextEditor.selection.active.character).to.equal(0); + }); + + test('Change cell to markdown no change', async () => { + const text = `testing_L0 +# %% +testing_L2 +testing_L3 +# %% [markdown] extra +# # testing_L5 +testing_L6 + +`; + const mockTextEditor = initializeMockTextEditor(codeWatcher, documentManager, text); + + mockTextEditor.selection = new Selection(1, 2, 5, 5); + + await codeWatcher.changeCellToMarkdown(); + + expect(mockTextEditor.document.getText()).to.equal(text); + + expect(mockTextEditor.selection.anchor.line).to.equal(1); + expect(mockTextEditor.selection.anchor.character).to.equal(2); + expect(mockTextEditor.selection.active.line).to.equal(5); + expect(mockTextEditor.selection.active.character).to.equal(5); + }); }); From e62137a549601a0250ef66df370f33acff730732 Mon Sep 17 00:00:00 2001 From: earthastronaut <> Date: Thu, 9 Jul 2020 19:30:18 -0600 Subject: [PATCH 16/26] add changeCellToCode * command will use selected cell and if cell_type is not code then it'll convert to code * linked to python.datascience.changeCellToCode --- package.json | 16 +++ package.nls.json | 1 + src/client/common/application/commands.ts | 1 + .../datascience/commands/commandRegistry.ts | 10 ++ src/client/datascience/constants.ts | 1 + .../editor-integration/codewatcher.ts | 110 ++++++++++++------ src/client/datascience/types.ts | 1 + .../codewatcher.unit.test.ts | 82 ++++++++++++- 8 files changed, 187 insertions(+), 35 deletions(-) diff --git a/package.json b/package.json index da0cb8cbe229..9aa4938481cf 100644 --- a/package.json +++ b/package.json @@ -558,6 +558,11 @@ "title": "%python.command.python.datascience.changeCellToMarkdown.title%", "category": "Python" }, + { + "command": "python.datascience.changeCellToCode", + "title": "%python.command.python.datascience.changeCellToCode.title%", + "category": "Python" + }, { "command": "python.datascience.runcurrentcelladvance", "title": "%python.command.python.datascience.runcurrentcelladvance.title%", @@ -887,6 +892,11 @@ "command": "python.datascience.changeCellToMarkdown", "group": "Python2" }, + { + "when": "editorFocus && editorLangId == python && python.datascience.hascodecells && python.datascience.featureenabled && !notebookEditorFocused", + "command": "python.datascience.changeCellToCode", + "group": "Python2" + }, { "when": "editorFocus && editorLangId == python && python.datascience.hascodecells && python.datascience.featureenabled && !notebookEditorFocused", "command": "python.datascience.runcurrentcell", @@ -1118,6 +1128,12 @@ "category": "Python", "when": "python.datascience.hascodecells && python.datascience.featureenabled && python.datascience.ispythonornativeactive" }, + { + "command": "python.datascience.changeCellToCode", + "title": "%python.command.python.datascience.changeCellToCode.title%", + "category": "Python", + "when": "python.datascience.hascodecells && python.datascience.featureenabled && python.datascience.ispythonornativeactive" + }, { "command": "python.datascience.runcurrentcell", "title": "%python.command.python.datascience.runcurrentcell.title%", diff --git a/package.nls.json b/package.nls.json index e6186091696e..0a307df90104 100644 --- a/package.nls.json +++ b/package.nls.json @@ -75,6 +75,7 @@ "python.command.python.datascience.moveCellsUp.title": "Move Selected Cells Up", "python.command.python.datascience.moveCellsDown.title": "Move Selected Cells Down", "python.command.python.datascience.changeCellToMarkdown.title": "Change Cell to Markdown", + "python.command.python.datascience.changeCellToCode.title": "Change Cell to Code", "python.command.python.datascience.showhistorypane.title": "Show Python Interactive Window", "python.command.python.datascience.selectjupyteruri.title": "Specify local or remote Jupyter server for connections", "python.command.python.datascience.selectjupytercommandline.title": "Specify Jupyter command line arguments", diff --git a/src/client/common/application/commands.ts b/src/client/common/application/commands.ts index 4e411287ad2f..c084177c2161 100644 --- a/src/client/common/application/commands.ts +++ b/src/client/common/application/commands.ts @@ -181,6 +181,7 @@ export interface ICommandNameArgumentTypeMapping extends ICommandNameWithoutArgu [DSCommands.MoveCellsUp]: []; [DSCommands.MoveCellsDown]: []; [DSCommands.ChangeCellToMarkdown]: []; + [DSCommands.ChangeCellToCode]: []; [DSCommands.ScrollToCell]: [string, string]; [DSCommands.ViewJupyterOutput]: []; [DSCommands.ExportAsPythonScript]: [INotebookModel]; diff --git a/src/client/datascience/commands/commandRegistry.ts b/src/client/datascience/commands/commandRegistry.ts index 171182f5742b..03979bec5683 100644 --- a/src/client/datascience/commands/commandRegistry.ts +++ b/src/client/datascience/commands/commandRegistry.ts @@ -73,6 +73,7 @@ export class CommandRegistry implements IDisposable { this.registerCommand(Commands.MoveCellsUp, this.moveCellsUp); this.registerCommand(Commands.MoveCellsDown, this.moveCellsDown); this.registerCommand(Commands.ChangeCellToMarkdown, this.changeCellToMarkdown); + this.registerCommand(Commands.ChangeCellToCode, this.changeCellToCode); this.registerCommand(Commands.RunAllCellsAbovePalette, this.runAllCellsAboveFromCursor); this.registerCommand(Commands.RunCellAndAllBelowPalette, this.runCellAndAllBelowFromCursor); this.registerCommand(Commands.RunToLine, this.runToLine); @@ -415,6 +416,15 @@ export class CommandRegistry implements IDisposable { } } + private async changeCellToCode(): Promise { + const activeCodeWatcher = this.getCurrentCodeWatcher(); + if (activeCodeWatcher) { + return activeCodeWatcher.changeCellToCode(); + } else { + return Promise.resolve(); + } + } + private async runAllCellsAboveFromCursor(): Promise { const currentCodeLens = this.getCurrentCodeLens(); if (currentCodeLens) { diff --git a/src/client/datascience/constants.ts b/src/client/datascience/constants.ts index 7e370ec245f7..5029da315f48 100644 --- a/src/client/datascience/constants.ts +++ b/src/client/datascience/constants.ts @@ -88,6 +88,7 @@ export namespace Commands { export const MoveCellsUp = 'python.datascience.moveCellsUp'; export const MoveCellsDown = 'python.datascience.moveCellsDown'; export const ChangeCellToMarkdown = 'python.datascience.changeCellToMarkdown'; + export const ChangeCellToCode = 'python.datascience.changeCellToCode'; export const ScrollToCell = 'python.datascience.scrolltocell'; export const CreateNewNotebook = 'python.datascience.createnewnotebook'; export const ViewJupyterOutput = 'python.datascience.viewJupyterOutput'; diff --git a/src/client/datascience/editor-integration/codewatcher.ts b/src/client/datascience/editor-integration/codewatcher.ts index 0315a4248b19..596c248957d6 100644 --- a/src/client/datascience/editor-integration/codewatcher.ts +++ b/src/client/datascience/editor-integration/codewatcher.ts @@ -1,6 +1,7 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. 'use strict'; +import type { nbformat } from '@jupyterlab/coreutils'; import { inject, injectable } from 'inversify'; import { CodeLens, @@ -639,42 +640,82 @@ export class CodeWatcher implements ICodeWatcher { } public async changeCellToMarkdown(): Promise { + return this.applyToCells(async (editor, cell, _) => { + return this.changeCellTo(editor, cell, 'markdown'); + }).then(() => Promise.resolve()); + } + + public async changeCellToCode(): Promise { + return this.applyToCells(async (editor, cell, _) => { + return this.changeCellTo(editor, cell, 'code'); + }).then(() => Promise.resolve()); + } + + private async applyToCells( + callback: (editor: TextEditor, cell: ICellRange, cellIndex: number) => Thenable + ): Promise { const editor = this.documentManager.activeTextEditor; - const cell = this.getCellFromPosition(); - if (editor && cell) { - if (cell.cell_type === 'markdown') { - return Promise.resolve(); - } + const startEndCellIndex = this.getStartEndCellIndex(editor?.selection); + if (!editor || !startEndCellIndex) { + return Promise.resolve(false); + } + const cells = this.codeLensFactory.cells; + const startIndex = startEndCellIndex[0]; + const endIndex = startEndCellIndex[1]; + for (let cellIndex = startIndex; cellIndex <= endIndex; cellIndex += 1) { + await callback(editor, cells[cellIndex], cellIndex); + } + return Promise.resolve(true); + } - const cellMatcher = new CellMatcher(this.configService.getSettings(editor.document.uri).datascience); - const definitionLine = editor.document.lineAt(cell.range.start.line); - const definitionText = editor.document.getText(definitionLine.range); - const definitionMatch = cellMatcher.codeExecRegEx.exec(definitionText); - if (!definitionMatch) { - return Promise.resolve(); - } - const definitionExtra = definitionMatch[definitionMatch.length - 1]; - - const cellMarker = this.getDefaultCellMarker(editor.document.uri); - const newDefinitionText = `${cellMarker} [markdown]${definitionExtra}`; - await editor.edit(async (editBuilder) => { - editBuilder.replace(definitionLine.range, newDefinitionText); - cell.cell_type = 'markdown'; - if (cell.range.start.line < cell.range.end.line) { - editor.selection = new Selection( - cell.range.start.line + 1, - 0, - cell.range.end.line, - cell.range.end.character - ); - // ensure all lines in markdown cell have a comment. - // these are not included in the test because it's unclear - // how TypeMoq works with them. - await commands.executeCommand('editor.action.removeCommentLine'); + private async changeCellTo(editor: TextEditor, cell: ICellRange, toCellType: nbformat.CellType): Promise { + // change cell from code -> markdown or markdown -> code + if (toCellType === 'raw') { + throw Error('Cell Type raw not implemented'); + } + + // don't change cell type if already that type + if (cell.cell_type === toCellType) { + return Promise.resolve(false); + } + const cellMatcher = new CellMatcher(this.configService.getSettings(editor.document.uri).datascience); + const definitionLine = editor.document.lineAt(cell.range.start.line); + const definitionText = editor.document.getText(definitionLine.range); + + // new definition text + const cellMarker = this.getDefaultCellMarker(editor.document.uri); + const definitionMatch = + toCellType === 'markdown' + ? cellMatcher.codeExecRegEx.exec(definitionText) // code -> markdown + : cellMatcher.markdownExecRegEx.exec(definitionText); // markdown -> code + if (!definitionMatch) { + return Promise.resolve(false); + } + const definitionExtra = definitionMatch[definitionMatch.length - 1]; + const newDefinitionText = + toCellType === 'markdown' + ? `${cellMarker} [markdown]${definitionExtra}` // code -> markdown + : `${cellMarker}${definitionExtra}`; // markdown -> code + + return editor.edit(async (editBuilder) => { + editBuilder.replace(definitionLine.range, newDefinitionText); + cell.cell_type = toCellType; + if (cell.range.start.line < cell.range.end.line) { + editor.selection = new Selection( + cell.range.start.line + 1, + 0, + cell.range.end.line, + cell.range.end.character + ); + // ensure all lines in markdown cell have a comment. + // these are not included in the test because it's unclear + // how TypeMoq works with them. + await commands.executeCommand('editor.action.removeCommentLine'); + if (toCellType === 'markdown') { await commands.executeCommand('editor.action.addCommentLine'); } - }); - } + } + }); } private async moveCellsDirection(directionUp: boolean): Promise { @@ -807,7 +848,10 @@ export class CodeWatcher implements ICodeWatcher { } } - private getStartEndCellIndex(selection: Selection): number[] | undefined { + private getStartEndCellIndex(selection?: Selection): number[] | undefined { + if (!selection) { + return undefined; + } let startCellIndex = this.getCellIndex(selection.start); let endCellIndex = startCellIndex; // handle if the selection is the same line, hence same cell diff --git a/src/client/datascience/types.ts b/src/client/datascience/types.ts index 76e9075fecd1..6dab433a4762 100644 --- a/src/client/datascience/types.ts +++ b/src/client/datascience/types.ts @@ -638,6 +638,7 @@ export interface ICodeWatcher { moveCellsUp(): Promise; moveCellsDown(): Promise; changeCellToMarkdown(): Promise; + changeCellToCode(): Promise; debugCurrentCell(): Promise; } diff --git a/src/test/datascience/editor-integration/codewatcher.unit.test.ts b/src/test/datascience/editor-integration/codewatcher.unit.test.ts index de6c33222852..3839fc84518d 100644 --- a/src/test/datascience/editor-integration/codewatcher.unit.test.ts +++ b/src/test/datascience/editor-integration/codewatcher.unit.test.ts @@ -2036,7 +2036,7 @@ testing_L3 expect(mockTextEditor.document.getText()).to.equal( `testing_L0 -# %% +# %% [markdown] testing_L2 testing_L3 # %% [markdown] extra @@ -2053,7 +2053,7 @@ testing_L6 test('Change cell to markdown no change', async () => { const text = `testing_L0 -# %% +# %% [markdown] testing_L2 testing_L3 # %% [markdown] extra @@ -2074,4 +2074,82 @@ testing_L6 expect(mockTextEditor.selection.active.line).to.equal(5); expect(mockTextEditor.selection.active.character).to.equal(5); }); + + test('Change cell to code', async () => { + const mockTextEditor = initializeMockTextEditor( + codeWatcher, + documentManager, + `testing_L0 +# %% [markdown] +# testing_L2 +# testing_L3 +# %% [markdown] extra +# # testing_L5 +# testing_L6 + +` + ); + + mockTextEditor.selection = new Selection(1, 2, 5, 5); + + await codeWatcher.changeCellToCode(); + + // NOTE: When running the function in real environment there + // are comment lines added in addition to the [markdown] definition. + // It is unclear with TypeMoq how to test this particular behavior because + // the external `commands.executeCommmands` is being proxied along with + // all subsequent calls. Essentially, I must rely on those functions + // being unit tested. + /* + actual expected = `testing_L0 +# %% +testing_L2 +testing_L3 +# %% [markdown] extra +# # testing_L5 +# testing_L6 + +` + */ + + expect(mockTextEditor.document.getText()).to.equal( + `testing_L0 +# %% +# testing_L2 +# testing_L3 +# %% extra +# # testing_L5 +# testing_L6 + +` + ); + expect(mockTextEditor.selection.anchor.line).to.equal(5); + expect(mockTextEditor.selection.anchor.character).to.equal(0); + expect(mockTextEditor.selection.active.line).to.equal(8); + expect(mockTextEditor.selection.active.character).to.equal(0); + }); + + test('Change cell to code no change', async () => { + const text = `testing_L0 +# %% +# testing_L2 +# testing_L3 +# %% extra +# # testing_L5 +# testing_L6 + +`; + const mockTextEditor = initializeMockTextEditor(codeWatcher, documentManager, text); + + mockTextEditor.selection = new Selection(1, 2, 5, 5); + + await codeWatcher.changeCellToCode(); + + expect(mockTextEditor.document.getText()).to.equal(text); + + expect(mockTextEditor.selection.anchor.line).to.equal(1); + expect(mockTextEditor.selection.anchor.character).to.equal(2); + expect(mockTextEditor.selection.active.line).to.equal(5); + expect(mockTextEditor.selection.active.character).to.equal(5); + }); }); From f624692a29fef9b42307772acff75fc5ba5317eb Mon Sep 17 00:00:00 2001 From: earthastronaut <> Date: Thu, 9 Jul 2020 19:33:52 -0600 Subject: [PATCH 17/26] Remove cell edit commands from context menu --- package.json | 60 ---------------------------------------------------- 1 file changed, 60 deletions(-) diff --git a/package.json b/package.json index 9aa4938481cf..206640500c63 100644 --- a/package.json +++ b/package.json @@ -837,66 +837,6 @@ "command": "python.datascience.runallcells", "group": "Python2" }, - { - "when": "editorFocus && editorLangId == python && python.datascience.hascodecells && python.datascience.featureenabled && !notebookEditorFocused", - "command": "python.datascience.insertCellBelowPosition", - "group": "Python2" - }, - { - "when": "editorFocus && editorLangId == python && python.datascience.hascodecells && python.datascience.featureenabled && !notebookEditorFocused", - "command": "python.datascience.insertCellBelow", - "group": "Python2" - }, - { - "when": "editorFocus && editorLangId == python && python.datascience.hascodecells && python.datascience.featureenabled && !notebookEditorFocused", - "command": "python.datascience.insertCellAbove", - "group": "Python2" - }, - { - "when": "editorFocus && editorLangId == python && python.datascience.hascodecells && python.datascience.featureenabled && !notebookEditorFocused", - "command": "python.datascience.deleteCells", - "group": "Python2" - }, - { - "when": "editorFocus && editorLangId == python && python.datascience.hascodecells && python.datascience.featureenabled && !notebookEditorFocused", - "command": "python.datascience.selectCell", - "group": "Python2" - }, - { - "when": "editorFocus && editorLangId == python && python.datascience.hascodecells && python.datascience.featureenabled && !notebookEditorFocused", - "command": "python.datascience.selectCellContents", - "group": "Python2" - }, - { - "when": "editorFocus && editorLangId == python && python.datascience.hascodecells && python.datascience.featureenabled && !notebookEditorFocused", - "command": "python.datascience.extendSelectionByCellAbove", - "group": "Python2" - }, - { - "when": "editorFocus && editorLangId == python && python.datascience.hascodecells && python.datascience.featureenabled && !notebookEditorFocused", - "command": "python.datascience.extendSelectionByCellBelow", - "group": "Python2" - }, - { - "when": "editorFocus && editorLangId == python && python.datascience.hascodecells && python.datascience.featureenabled && !notebookEditorFocused", - "command": "python.datascience.moveCellsUp", - "group": "Python2" - }, - { - "when": "editorFocus && editorLangId == python && python.datascience.hascodecells && python.datascience.featureenabled && !notebookEditorFocused", - "command": "python.datascience.moveCellsDown", - "group": "Python2" - }, - { - "when": "editorFocus && editorLangId == python && python.datascience.hascodecells && python.datascience.featureenabled && !notebookEditorFocused", - "command": "python.datascience.changeCellToMarkdown", - "group": "Python2" - }, - { - "when": "editorFocus && editorLangId == python && python.datascience.hascodecells && python.datascience.featureenabled && !notebookEditorFocused", - "command": "python.datascience.changeCellToCode", - "group": "Python2" - }, { "when": "editorFocus && editorLangId == python && python.datascience.hascodecells && python.datascience.featureenabled && !notebookEditorFocused", "command": "python.datascience.runcurrentcell", From 9a17d5f977482aef77f3add2f3b992f54b5a1e8f Mon Sep 17 00:00:00 2001 From: earthastronaut <> Date: Thu, 9 Jul 2020 19:56:31 -0600 Subject: [PATCH 18/26] Use the insertCell method to insert a cell when RunCellAndAdvance reaches the last cell --- .../editor-integration/codewatcher.ts | 34 ++++--------------- 1 file changed, 7 insertions(+), 27 deletions(-) diff --git a/src/client/datascience/editor-integration/codewatcher.ts b/src/client/datascience/editor-integration/codewatcher.ts index 596c248957d6..a5443b40d457 100644 --- a/src/client/datascience/editor-integration/codewatcher.ts +++ b/src/client/datascience/editor-integration/codewatcher.ts @@ -982,16 +982,14 @@ export class CodeWatcher implements ICodeWatcher { if (currentRunCellLens) { // Move the next cell if allowed. if (advance) { - // Either use the next cell that we found, or add a new one into the document - let nextRange: Range; - if (!nextRunCellLens) { - nextRange = this.createNewCell(currentRunCellLens.range); + if (nextRunCellLens) { + this.advanceToRange(nextRunCellLens.range); } else { - nextRange = nextRunCellLens.range; - } - - if (nextRange) { - this.advanceToRange(nextRange); + // insert new cell at bottom after current + const editor = this.documentManager.activeTextEditor; + if (editor) { + await this.insertCell(editor, currentRunCellLens.range.end.line + 1); + } } } @@ -1061,24 +1059,6 @@ export class CodeWatcher implements ICodeWatcher { } } - // User has picked run and advance on the last cell of a document - // Create a new cell at the bottom and put their selection there, ready to type - private createNewCell(currentRange: Range): Range { - const editor = this.documentManager.activeTextEditor; - const newPosition = new Position(currentRange.end.line + 3, 0); // +3 to account for the added spaces and to position after the new mark - - if (editor) { - editor.edit((editBuilder) => { - editBuilder.insert( - new Position(currentRange.end.line + 1, 0), - `\n\n${this.getDefaultCellMarker(editor.document.uri)}\n` - ); - }); - } - - return new Range(newPosition, newPosition); - } - // Advance the cursor to the selected range private advanceToRange(targetRange: Range) { const editor = this.documentManager.activeTextEditor; From 0ae48a68fab254511084363c6a82ba7ad327a706 Mon Sep 17 00:00:00 2001 From: earthastronaut <> Date: Thu, 9 Jul 2020 19:57:40 -0600 Subject: [PATCH 19/26] Use the insertCell method to insert cell within addEmptyCellToBottom --- src/client/datascience/editor-integration/codewatcher.ts | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/src/client/datascience/editor-integration/codewatcher.ts b/src/client/datascience/editor-integration/codewatcher.ts index a5443b40d457..63a4a206efe3 100644 --- a/src/client/datascience/editor-integration/codewatcher.ts +++ b/src/client/datascience/editor-integration/codewatcher.ts @@ -337,14 +337,8 @@ export class CodeWatcher implements ICodeWatcher { public async addEmptyCellToBottom(): Promise { const editor = this.documentManager.activeTextEditor; - const cellDelineator = this.getDefaultCellMarker(editor?.document.uri); if (editor) { - editor.edit((editBuilder) => { - editBuilder.insert(new Position(editor.document.lineCount, 0), `\n\n${cellDelineator}\n`); - }); - - const newPosition = new Position(editor.document.lineCount + 3, 0); // +3 to account for the added spaces and to position after the new mark - return this.advanceToRange(new Range(newPosition, newPosition)); + return this.insertCell(editor, editor.document.lineCount + 1); } } From 8842cdeac249258ab15f2c716d2845405518973a Mon Sep 17 00:00:00 2001 From: earthastronaut <> Date: Thu, 9 Jul 2020 20:20:37 -0600 Subject: [PATCH 20/26] Add telemetry for cell edit commands in interactive python --- src/client/datascience/constants.ts | 14 +++++++++++ .../editor-integration/codewatcher.ts | 14 +++++++++++ src/client/telemetry/index.ts | 23 +++++++++++++++++++ 3 files changed, 51 insertions(+) diff --git a/src/client/datascience/constants.ts b/src/client/datascience/constants.ts index 5029da315f48..5588083eae1a 100644 --- a/src/client/datascience/constants.ts +++ b/src/client/datascience/constants.ts @@ -168,6 +168,20 @@ export enum Telemetry { RunAllCells = 'DATASCIENCE.RUN_ALL_CELLS', RunAllCellsAbove = 'DATASCIENCE.RUN_ALL_CELLS_ABOVE', RunCellAndAllBelow = 'DATASCIENCE.RUN_CELL_AND_ALL_BELOW', + AddEmptyCellToBottom = 'DATASCIENCE.RUN_ADD_EMPTY_CELL_TO_BOTTOM', + RunCurrentCellAndAddBelow = 'DATASCIENCE.RUN_CURRENT_CELL_AND_ADD_BELOW', + InsertCellBelowPosition = 'DATASCIENCE.RUN_INSERT_CELL_BELOW_POSITION', + InsertCellBelow = 'DATASCIENCE.RUN_INSERT_CELL_BELOW', + InsertCellAbove = 'DATASCIENCE.RUN_INSERT_CELL_ABOVE', + DeleteCells = 'DATASCIENCE.RUN_DELETE_CELLS', + SelectCell = 'DATASCIENCE.RUN_SELECT_CELL', + SelectCellContents = 'DATASCIENCE.RUN_SELECT_CELL_CONTENTS', + ExtendSelectionByCellAbove = 'DATASCIENCE.RUN_EXTEND_SELECTION_BY_CELL_ABOVE', + ExtendSelectionByCellBelow = 'DATASCIENCE.RUN_EXTEND_SELECTION_BY_CELL_BELOW', + MoveCellsUp = 'DATASCIENCE.RUN_MOVE_CELLS_UP', + MoveCellsDown = 'DATASCIENCE.RUN_MOVE_CELLS_DOWN', + ChangeCellToMarkdown = 'DATASCIENCE.RUN_CHANGE_CELL_TO_MARKDOWN', + ChangeCellToCode = 'DATASCIENCE.RUN_CHANGE_CELL_TO_CODE', RunSelectionOrLine = 'DATASCIENCE.RUN_SELECTION_OR_LINE', RunToLine = 'DATASCIENCE.RUN_TO_LINE', RunFromLine = 'DATASCIENCE.RUN_FROM_LINE', diff --git a/src/client/datascience/editor-integration/codewatcher.ts b/src/client/datascience/editor-integration/codewatcher.ts index 63a4a206efe3..1bc13f86872d 100644 --- a/src/client/datascience/editor-integration/codewatcher.ts +++ b/src/client/datascience/editor-integration/codewatcher.ts @@ -335,6 +335,7 @@ export class CodeWatcher implements ICodeWatcher { return this.runMatchingCell(this.documentManager.activeTextEditor.selection, true); } + @captureTelemetry(Telemetry.AddEmptyCellToBottom) public async addEmptyCellToBottom(): Promise { const editor = this.documentManager.activeTextEditor; if (editor) { @@ -342,6 +343,7 @@ export class CodeWatcher implements ICodeWatcher { } } + @captureTelemetry(Telemetry.RunCurrentCellAndAddBelow) public async runCurrentCellAndAddBelow(): Promise { if (!this.documentManager.activeTextEditor || !this.documentManager.activeTextEditor.document) { return Promise.resolve(); @@ -379,6 +381,7 @@ export class CodeWatcher implements ICodeWatcher { ); } + @captureTelemetry(Telemetry.InsertCellBelowPosition) public async insertCellBelowPosition(): Promise { const editor = this.documentManager.activeTextEditor; if (editor && editor.selection.end) { @@ -386,6 +389,7 @@ export class CodeWatcher implements ICodeWatcher { } } + @captureTelemetry(Telemetry.InsertCellBelow) public async insertCellBelow(): Promise { const editor = this.documentManager.activeTextEditor; if (editor && editor.selection) { @@ -398,6 +402,7 @@ export class CodeWatcher implements ICodeWatcher { } } + @captureTelemetry(Telemetry.InsertCellAbove) public async insertCellAbove(): Promise { const editor = this.documentManager.activeTextEditor; if (editor && editor.selection) { @@ -410,6 +415,7 @@ export class CodeWatcher implements ICodeWatcher { } } + @captureTelemetry(Telemetry.DeleteCells) public async deleteCells(): Promise { const editor = this.documentManager.activeTextEditor; if (!editor || !editor.selection) { @@ -450,6 +456,7 @@ export class CodeWatcher implements ICodeWatcher { }); } + @captureTelemetry(Telemetry.SelectCell) public async selectCell(): Promise { const editor = this.documentManager.activeTextEditor; if (editor && editor.selection) { @@ -466,6 +473,7 @@ export class CodeWatcher implements ICodeWatcher { } } + @captureTelemetry(Telemetry.SelectCellContents) public async selectCellContents(): Promise { const editor = this.documentManager.activeTextEditor; if (!editor || !editor.selection) { @@ -503,6 +511,7 @@ export class CodeWatcher implements ICodeWatcher { editor.selections = selections; } + @captureTelemetry(Telemetry.ExtendSelectionByCellAbove) public async extendSelectionByCellAbove(): Promise { // This behaves similarly to excel "Extend Selection by One Cell Above". // The direction of the selection matters (i.e. where the active cursor) @@ -563,6 +572,7 @@ export class CodeWatcher implements ICodeWatcher { } } + @captureTelemetry(Telemetry.ExtendSelectionByCellBelow) public async extendSelectionByCellBelow(): Promise { // This behaves similarly to excel "Extend Selection by One Cell Above". // The direction of the selection matters (i.e. where the active cursor) @@ -625,20 +635,24 @@ export class CodeWatcher implements ICodeWatcher { } } + @captureTelemetry(Telemetry.MoveCellsUp) public async moveCellsUp(): Promise { return this.moveCellsDirection(true); } + @captureTelemetry(Telemetry.MoveCellsDown) public async moveCellsDown(): Promise { return this.moveCellsDirection(false); } + @captureTelemetry(Telemetry.ChangeCellToMarkdown) public async changeCellToMarkdown(): Promise { return this.applyToCells(async (editor, cell, _) => { return this.changeCellTo(editor, cell, 'markdown'); }).then(() => Promise.resolve()); } + @captureTelemetry(Telemetry.ChangeCellToCode) public async changeCellToCode(): Promise { return this.applyToCells(async (editor, cell, _) => { return this.changeCellTo(editor, cell, 'code'); diff --git a/src/client/telemetry/index.ts b/src/client/telemetry/index.ts index db0d91296886..2a4093563e95 100644 --- a/src/client/telemetry/index.ts +++ b/src/client/telemetry/index.ts @@ -1674,6 +1674,9 @@ export interface IEventNamePropertyMapping { [Telemetry.RestartJupyterTime]: never | undefined; [Telemetry.RestartKernel]: never | undefined; [Telemetry.RestartKernelCommand]: never | undefined; + /** + * Run Cell Commands in Interactive Python + */ [Telemetry.RunAllCells]: never | undefined; [Telemetry.RunSelectionOrLine]: never | undefined; [Telemetry.RunCell]: never | undefined; @@ -1685,6 +1688,26 @@ export interface IEventNamePropertyMapping { [Telemetry.RunFileInteractive]: never | undefined; [Telemetry.RunFromLine]: never | undefined; [Telemetry.ScrolledToCell]: never | undefined; + /** + * Cell Edit Commands in Interactive Python + */ + [Telemetry.InsertCellBelowPosition]: never | undefined; + [Telemetry.InsertCellBelow]: never | undefined; + [Telemetry.InsertCellAbove]: never | undefined; + [Telemetry.DeleteCells]: never | undefined; + [Telemetry.SelectCell]: never | undefined; + [Telemetry.SelectCellContents]: never | undefined; + [Telemetry.ExtendSelectionByCellAbove]: never | undefined; + [Telemetry.ExtendSelectionByCellBelow]: never | undefined; + [Telemetry.MoveCellsUp]: never | undefined; + [Telemetry.MoveCellsDown]: never | undefined; + [Telemetry.ChangeCellToMarkdown]: never | undefined; + [Telemetry.ChangeCellToCode]: never | undefined; + /** + * Misc + */ + [Telemetry.AddEmptyCellToBottom]: never | undefined; + [Telemetry.RunCurrentCellAndAddBelow]: never | undefined; [Telemetry.CellCount]: { count: number }; [Telemetry.Save]: never | undefined; [Telemetry.SelfCertsMessageClose]: never | undefined; From 8d1ae3fa51b102b8d8596aa9cf6979107bc87210 Mon Sep 17 00:00:00 2001 From: earthastronaut <> Date: Thu, 9 Jul 2020 20:40:17 -0600 Subject: [PATCH 21/26] Add codeLensFactory.getCodeLensCacheData and abstracted the cell range creation Abstracts the cells behind a method on codeLensFactory. Consolidates storage into the single cached data store on the codeLensFactory though it requires updating again on the codeWatcher. --- .../editor-integration/codeLensFactory.ts | 17 +++++++++++------ .../editor-integration/codewatcher.ts | 19 +++++++++++-------- src/client/datascience/types.ts | 2 +- 3 files changed, 23 insertions(+), 15 deletions(-) diff --git a/src/client/datascience/editor-integration/codeLensFactory.ts b/src/client/datascience/editor-integration/codeLensFactory.ts index d6ea1ddc02ac..c24eaae73e22 100644 --- a/src/client/datascience/editor-integration/codeLensFactory.ts +++ b/src/client/datascience/editor-integration/codeLensFactory.ts @@ -42,7 +42,6 @@ type CodeLensCacheData = { */ @injectable() export class CodeLensFactory implements ICodeLensFactory, IInteractiveWindowListener { - public cells: ICellRange[]; private updateEvent: EventEmitter = new EventEmitter(); // tslint:disable-next-line: no-any private postEmitter: EventEmitter<{ message: string; payload: any }> = new EventEmitter<{ @@ -62,7 +61,6 @@ export class CodeLensFactory implements ICodeLensFactory, IInteractiveWindowList @inject(IFileSystem) private fileSystem: IFileSystem, @inject(IDocumentManager) private documentManager: IDocumentManager ) { - this.cells = []; this.documentManager.onDidCloseTextDocument(this.onClosedDocument.bind(this)); this.configService.getSettings(undefined).onDidChange(this.onChangedSettings.bind(this)); } @@ -132,6 +130,16 @@ export class CodeLensFactory implements ICodeLensFactory, IInteractiveWindowList } public createCodeLenses(document: TextDocument): CodeLens[] { + const cache = this.getCodeLensCacheData(document); + return [...cache.documentLenses, ...cache.gotoCellLens]; + } + + public getCellRanges(document: TextDocument): ICellRange[] { + const cache = this.getCodeLensCacheData(document); + return cache.cellRanges; + } + + private getCodeLensCacheData(document: TextDocument): CodeLensCacheData { // See if we have a cached version of the code lenses for this document const key = document.fileName.toLocaleLowerCase(); let cache = this.codeLensCache.get(key); @@ -206,10 +214,7 @@ export class CodeLensFactory implements ICodeLensFactory, IInteractiveWindowList } }); } - - this.cells = cache.cellRanges; - - return [...cache.documentLenses, ...cache.gotoCellLens]; + return cache; } private setIdentity(identity: INotebookIdentity) { diff --git a/src/client/datascience/editor-integration/codewatcher.ts b/src/client/datascience/editor-integration/codewatcher.ts index 1bc13f86872d..02c9595f89cb 100644 --- a/src/client/datascience/editor-integration/codewatcher.ts +++ b/src/client/datascience/editor-integration/codewatcher.ts @@ -62,6 +62,7 @@ export class CodeWatcher implements ICodeWatcher { private version: number = -1; private fileName: string = ''; private codeLenses: CodeLens[] = []; + private cells: ICellRange[] = []; private cachedSettings: IDataScienceSettings | undefined; private codeLensUpdatedEvent: EventEmitter = new EventEmitter(); private updateRequiredDisposable: IDisposable | undefined; @@ -89,6 +90,7 @@ export class CodeWatcher implements ICodeWatcher { // Use the factory to generate our new code lenses. this.codeLenses = this.codeLensFactory.createCodeLenses(document); + this.cells = this.codeLensFactory.getCellRanges(document); // Listen for changes this.updateRequiredDisposable = this.codeLensFactory.updateRequired(this.onCodeLensFactoryUpdated.bind(this)); @@ -487,7 +489,7 @@ export class CodeWatcher implements ICodeWatcher { const endCellIndex = startEndCellIndex[1]; const isAnchorLessEqualActive = editor.selection.anchor.isBeforeOrEqual(editor.selection.active); - const cells = this.codeLensFactory.cells; + const cells = this.cells; const selections: Selection[] = []; for (let i = startCellIndex; i <= endCellIndex; i += 1) { const cell = cells[i]; @@ -531,7 +533,7 @@ export class CodeWatcher implements ICodeWatcher { const isAnchorLessThanActive = editor.selection.anchor.isBefore(editor.selection.active); - const cells = this.codeLensFactory.cells; + const cells = this.cells; const startCellIndex = startEndCellIndex[0]; const endCellIndex = startEndCellIndex[1]; const startCell = cells[startCellIndex]; @@ -592,7 +594,7 @@ export class CodeWatcher implements ICodeWatcher { const isAnchorLessEqualActive = editor.selection.anchor.isBeforeOrEqual(editor.selection.active); - const cells = this.codeLensFactory.cells; + const cells = this.cells; const startCellIndex = startEndCellIndex[0]; const endCellIndex = startEndCellIndex[1]; const startCell = cells[startCellIndex]; @@ -667,7 +669,7 @@ export class CodeWatcher implements ICodeWatcher { if (!editor || !startEndCellIndex) { return Promise.resolve(false); } - const cells = this.codeLensFactory.cells; + const cells = this.cells; const startIndex = startEndCellIndex[0]; const endIndex = startEndCellIndex[1]; for (let cellIndex = startIndex; cellIndex <= endIndex; cellIndex += 1) { @@ -737,7 +739,7 @@ export class CodeWatcher implements ICodeWatcher { } const startCellIndex = startEndCellIndex[0]; const endCellIndex = startEndCellIndex[1]; - const cells = this.codeLensFactory.cells; + const cells = this.cells; const startCell = cells[startCellIndex]; const endCell = cells[endCellIndex]; if (!startCell || !endCell) { @@ -923,6 +925,7 @@ export class CodeWatcher implements ICodeWatcher { // Update our code lenses. if (this.document) { this.codeLenses = this.codeLensFactory.createCodeLenses(this.document); + this.cells = this.codeLensFactory.getCellRanges(this.document); } this.codeLensUpdatedEvent.fire(); } @@ -1017,11 +1020,11 @@ export class CodeWatcher implements ICodeWatcher { } private getCellIndex(position: Position): number { - return this.codeLensFactory.cells.findIndex((cell) => position && cell.range.contains(position)); + return this.cells.findIndex((cell) => position && cell.range.contains(position)); } private getCellFromIndex(index: number): ICellRange { - const cells = this.codeLensFactory.cells; + const cells = this.cells; const indexBounded = getIndex(index, cells.length); return cells[indexBounded]; } @@ -1036,7 +1039,7 @@ export class CodeWatcher implements ICodeWatcher { if (position) { const index = this.getCellIndex(position); if (index >= 0) { - return this.codeLensFactory.cells[index]; + return this.cells[index]; } } } diff --git a/src/client/datascience/types.ts b/src/client/datascience/types.ts index 6dab433a4762..1a8c3dd5eb5e 100644 --- a/src/client/datascience/types.ts +++ b/src/client/datascience/types.ts @@ -644,9 +644,9 @@ export interface ICodeWatcher { export const ICodeLensFactory = Symbol('ICodeLensFactory'); export interface ICodeLensFactory { - cells: ICellRange[]; updateRequired: Event; createCodeLenses(document: TextDocument): CodeLens[]; + getCellRanges(document: TextDocument): ICellRange[]; } export enum CellState { From db47d3aa4e971906e5a748622a17cf30e3717fc2 Mon Sep 17 00:00:00 2001 From: earthastronaut <> Date: Mon, 13 Jul 2020 14:57:57 -0600 Subject: [PATCH 22/26] clean up async and promises within codewatcher.ts --- .../editor-integration/codewatcher.ts | 76 +++++++++---------- 1 file changed, 36 insertions(+), 40 deletions(-) diff --git a/src/client/datascience/editor-integration/codewatcher.ts b/src/client/datascience/editor-integration/codewatcher.ts index 02c9595f89cb..ae2c37c8710d 100644 --- a/src/client/datascience/editor-integration/codewatcher.ts +++ b/src/client/datascience/editor-integration/codewatcher.ts @@ -118,7 +118,7 @@ export class CodeWatcher implements ICodeWatcher { @captureTelemetry(Telemetry.DebugCurrentCell) public async debugCurrentCell() { if (!this.documentManager.activeTextEditor || !this.documentManager.activeTextEditor.document) { - return Promise.resolve(); + return; } // Run the cell that matches the current cursor position. @@ -294,9 +294,9 @@ export class CodeWatcher implements ICodeWatcher { } @captureTelemetry(Telemetry.RunCell) - public runCell(range: Range): Promise { + public async runCell(range: Range): Promise { if (!this.documentManager.activeTextEditor || !this.documentManager.activeTextEditor.document) { - return Promise.resolve(); + return; } // Run the cell clicked. Advance if the cursor is inside this cell and we're allowed to @@ -308,9 +308,9 @@ export class CodeWatcher implements ICodeWatcher { } @captureTelemetry(Telemetry.DebugCurrentCell) - public debugCell(range: Range): Promise { + public async debugCell(range: Range): Promise { if (!this.documentManager.activeTextEditor || !this.documentManager.activeTextEditor.document) { - return Promise.resolve(); + return; } // Debug the cell clicked. @@ -318,9 +318,9 @@ export class CodeWatcher implements ICodeWatcher { } @captureTelemetry(Telemetry.RunCurrentCell) - public runCurrentCell(): Promise { + public async runCurrentCell(): Promise { if (!this.documentManager.activeTextEditor || !this.documentManager.activeTextEditor.document) { - return Promise.resolve(); + return; } // Run the cell that matches the current cursor position. @@ -348,7 +348,7 @@ export class CodeWatcher implements ICodeWatcher { @captureTelemetry(Telemetry.RunCurrentCellAndAddBelow) public async runCurrentCellAndAddBelow(): Promise { if (!this.documentManager.activeTextEditor || !this.documentManager.activeTextEditor.document) { - return Promise.resolve(); + return; } const editor = this.documentManager.activeTextEditor; @@ -421,12 +421,12 @@ export class CodeWatcher implements ICodeWatcher { public async deleteCells(): Promise { const editor = this.documentManager.activeTextEditor; if (!editor || !editor.selection) { - return Promise.resolve(); + return; } const firstLastCells = this.getStartEndCells(editor.selection); if (!firstLastCells) { - return Promise.resolve(); + return; } const startCell = firstLastCells[0]; const endCell = firstLastCells[1]; @@ -448,14 +448,10 @@ export class CodeWatcher implements ICodeWatcher { new Position(startLineNumber, startCharacterNumber), new Position(endLineNumber, endCharacterNumber) ); - return editor - .edit((editBuilder) => { - editBuilder.replace(cellExtendedRange, ''); - this.codeLensUpdatedEvent.fire(); - }) - .then(() => { - return Promise.resolve(); - }); + await editor.edit((editBuilder) => { + editBuilder.replace(cellExtendedRange, ''); + this.codeLensUpdatedEvent.fire(); + }); } @captureTelemetry(Telemetry.SelectCell) @@ -479,11 +475,11 @@ export class CodeWatcher implements ICodeWatcher { public async selectCellContents(): Promise { const editor = this.documentManager.activeTextEditor; if (!editor || !editor.selection) { - return Promise.resolve(); + return; } const startEndCellIndex = this.getStartEndCellIndex(editor.selection); if (!startEndCellIndex) { - return Promise.resolve(); + return; } const startCellIndex = startEndCellIndex[0]; const endCellIndex = startEndCellIndex[1]; @@ -523,12 +519,12 @@ export class CodeWatcher implements ICodeWatcher { // selection range. const editor = this.documentManager.activeTextEditor; if (!editor || !editor.selection) { - return Promise.resolve(); + return; } const currentSelection = editor.selection; const startEndCellIndex = this.getStartEndCellIndex(editor.selection); if (!startEndCellIndex) { - return Promise.resolve(); + return; } const isAnchorLessThanActive = editor.selection.anchor.isBefore(editor.selection.active); @@ -584,12 +580,12 @@ export class CodeWatcher implements ICodeWatcher { // selection range. const editor = this.documentManager.activeTextEditor; if (!editor || !editor.selection) { - return Promise.resolve(); + return; } const currentSelection = editor.selection; const startEndCellIndex = this.getStartEndCellIndex(editor.selection); if (!startEndCellIndex) { - return Promise.resolve(); + return; } const isAnchorLessEqualActive = editor.selection.anchor.isBeforeOrEqual(editor.selection.active); @@ -649,16 +645,16 @@ export class CodeWatcher implements ICodeWatcher { @captureTelemetry(Telemetry.ChangeCellToMarkdown) public async changeCellToMarkdown(): Promise { - return this.applyToCells(async (editor, cell, _) => { + await this.applyToCells(async (editor, cell, _) => { return this.changeCellTo(editor, cell, 'markdown'); - }).then(() => Promise.resolve()); + }); } @captureTelemetry(Telemetry.ChangeCellToCode) public async changeCellToCode(): Promise { - return this.applyToCells(async (editor, cell, _) => { + await this.applyToCells(async (editor, cell, _) => { return this.changeCellTo(editor, cell, 'code'); - }).then(() => Promise.resolve()); + }); } private async applyToCells( @@ -667,7 +663,7 @@ export class CodeWatcher implements ICodeWatcher { const editor = this.documentManager.activeTextEditor; const startEndCellIndex = this.getStartEndCellIndex(editor?.selection); if (!editor || !startEndCellIndex) { - return Promise.resolve(false); + return false; } const cells = this.cells; const startIndex = startEndCellIndex[0]; @@ -675,7 +671,7 @@ export class CodeWatcher implements ICodeWatcher { for (let cellIndex = startIndex; cellIndex <= endIndex; cellIndex += 1) { await callback(editor, cells[cellIndex], cellIndex); } - return Promise.resolve(true); + return true; } private async changeCellTo(editor: TextEditor, cell: ICellRange, toCellType: nbformat.CellType): Promise { @@ -686,7 +682,7 @@ export class CodeWatcher implements ICodeWatcher { // don't change cell type if already that type if (cell.cell_type === toCellType) { - return Promise.resolve(false); + return false; } const cellMatcher = new CellMatcher(this.configService.getSettings(editor.document.uri).datascience); const definitionLine = editor.document.lineAt(cell.range.start.line); @@ -699,7 +695,7 @@ export class CodeWatcher implements ICodeWatcher { ? cellMatcher.codeExecRegEx.exec(definitionText) // code -> markdown : cellMatcher.markdownExecRegEx.exec(definitionText); // markdown -> code if (!definitionMatch) { - return Promise.resolve(false); + return false; } const definitionExtra = definitionMatch[definitionMatch.length - 1]; const newDefinitionText = @@ -731,11 +727,11 @@ export class CodeWatcher implements ICodeWatcher { private async moveCellsDirection(directionUp: boolean): Promise { const editor = this.documentManager.activeTextEditor; if (!editor || !editor.selection) { - return Promise.resolve(); + return; } const startEndCellIndex = this.getStartEndCellIndex(editor.selection); if (!startEndCellIndex) { - return Promise.resolve(); + return; } const startCellIndex = startEndCellIndex[0]; const endCellIndex = startEndCellIndex[1]; @@ -743,7 +739,7 @@ export class CodeWatcher implements ICodeWatcher { const startCell = cells[startCellIndex]; const endCell = cells[endCellIndex]; if (!startCell || !endCell) { - return Promise.resolve(); + return; } const currentRange = new Range(startCell.range.start, endCell.range.end); const relativeSelectionRange = new Range( @@ -756,7 +752,7 @@ export class CodeWatcher implements ICodeWatcher { let thenSetSelection: Thenable; if (directionUp) { if (startCellIndex === 0) { - return Promise.resolve(); + return; } else { const aboveCell = cells[startCellIndex - 1]; const thenExchangeTextLines = this.exchangeTextLines(editor, aboveCell.range, currentRange); @@ -774,7 +770,7 @@ export class CodeWatcher implements ICodeWatcher { } } else { if (endCellIndex === cells.length - 1) { - return Promise.resolve(); + return; } else { const belowCell = cells[endCellIndex + 1]; const thenExchangeTextLines = this.exchangeTextLines(editor, currentRange, belowCell.range); @@ -798,7 +794,7 @@ export class CodeWatcher implements ICodeWatcher { if (isEditSuccessful && isActiveBeforeAnchor) { editor.selection = new Selection(editor.selection.active, editor.selection.anchor); } - return Promise.resolve(); + return; }); } @@ -888,7 +884,7 @@ export class CodeWatcher implements ICodeWatcher { } } - private async insertCell(editor: TextEditor, line: number): Promise { + private insertCell(editor: TextEditor, line: number) { // insertCell // // Inserts a cell at current line defined as two new lines and then @@ -999,7 +995,7 @@ export class CodeWatcher implements ICodeWatcher { // insert new cell at bottom after current const editor = this.documentManager.activeTextEditor; if (editor) { - await this.insertCell(editor, currentRunCellLens.range.end.line + 1); + this.insertCell(editor, currentRunCellLens.range.end.line + 1); } } } From 4fdd64c2937ba763007ee8958fdf3c46ee3297b2 Mon Sep 17 00:00:00 2001 From: earthastronaut <> Date: Tue, 14 Jul 2020 10:37:40 -0600 Subject: [PATCH 23/26] clean up unnecessary Promise.resolve() --- .../datascience/commands/commandRegistry.ts | 44 +++++++++---------- 1 file changed, 22 insertions(+), 22 deletions(-) diff --git a/src/client/datascience/commands/commandRegistry.ts b/src/client/datascience/commands/commandRegistry.ts index 03979bec5683..aaf580f6ff1d 100644 --- a/src/client/datascience/commands/commandRegistry.ts +++ b/src/client/datascience/commands/commandRegistry.ts @@ -153,7 +153,7 @@ export class CommandRegistry implements IDisposable { if (codeWatcher) { return codeWatcher.runAllCells(); } else { - return Promise.resolve(); + return; } } @@ -165,7 +165,7 @@ export class CommandRegistry implements IDisposable { if (codeWatcher) { return codeWatcher.runFileInteractive(); } else { - return Promise.resolve(); + return; } } @@ -177,7 +177,7 @@ export class CommandRegistry implements IDisposable { if (codeWatcher) { return codeWatcher.debugFileInteractive(); } else { - return Promise.resolve(); + return; } } @@ -239,7 +239,7 @@ export class CommandRegistry implements IDisposable { if (activeCodeWatcher) { return activeCodeWatcher.runCurrentCell(); } else { - return Promise.resolve(); + return; } } @@ -248,7 +248,7 @@ export class CommandRegistry implements IDisposable { if (activeCodeWatcher) { return activeCodeWatcher.runCurrentCellAndAdvance(); } else { - return Promise.resolve(); + return; } } @@ -257,7 +257,7 @@ export class CommandRegistry implements IDisposable { if (activeCodeWatcher) { return activeCodeWatcher.runSelectionOrLine(this.documentManager.activeTextEditor); } else { - return Promise.resolve(); + return; } } @@ -313,7 +313,7 @@ export class CommandRegistry implements IDisposable { if (activeCodeWatcher) { return activeCodeWatcher.runCurrentCellAndAddBelow(); } else { - return Promise.resolve(); + return; } } @@ -322,7 +322,7 @@ export class CommandRegistry implements IDisposable { if (activeCodeWatcher) { return activeCodeWatcher.insertCellBelowPosition(); } else { - return Promise.resolve(); + return; } } @@ -331,7 +331,7 @@ export class CommandRegistry implements IDisposable { if (activeCodeWatcher) { return activeCodeWatcher.insertCellBelow(); } else { - return Promise.resolve(); + return; } } @@ -340,7 +340,7 @@ export class CommandRegistry implements IDisposable { if (activeCodeWatcher) { return activeCodeWatcher.insertCellAbove(); } else { - return Promise.resolve(); + return; } } @@ -349,7 +349,7 @@ export class CommandRegistry implements IDisposable { if (activeCodeWatcher) { return activeCodeWatcher.deleteCells(); } else { - return Promise.resolve(); + return; } } @@ -358,7 +358,7 @@ export class CommandRegistry implements IDisposable { if (activeCodeWatcher) { return activeCodeWatcher.selectCell(); } else { - return Promise.resolve(); + return; } } @@ -367,7 +367,7 @@ export class CommandRegistry implements IDisposable { if (activeCodeWatcher) { return activeCodeWatcher.selectCellContents(); } else { - return Promise.resolve(); + return; } } @@ -376,7 +376,7 @@ export class CommandRegistry implements IDisposable { if (activeCodeWatcher) { return activeCodeWatcher.extendSelectionByCellAbove(); } else { - return Promise.resolve(); + return; } } @@ -385,7 +385,7 @@ export class CommandRegistry implements IDisposable { if (activeCodeWatcher) { return activeCodeWatcher.extendSelectionByCellBelow(); } else { - return Promise.resolve(); + return; } } @@ -394,7 +394,7 @@ export class CommandRegistry implements IDisposable { if (activeCodeWatcher) { return activeCodeWatcher.moveCellsUp(); } else { - return Promise.resolve(); + return; } } @@ -403,7 +403,7 @@ export class CommandRegistry implements IDisposable { if (activeCodeWatcher) { return activeCodeWatcher.moveCellsDown(); } else { - return Promise.resolve(); + return; } } @@ -412,7 +412,7 @@ export class CommandRegistry implements IDisposable { if (activeCodeWatcher) { return activeCodeWatcher.changeCellToMarkdown(); } else { - return Promise.resolve(); + return; } } @@ -421,7 +421,7 @@ export class CommandRegistry implements IDisposable { if (activeCodeWatcher) { return activeCodeWatcher.changeCellToCode(); } else { - return Promise.resolve(); + return; } } @@ -436,7 +436,7 @@ export class CommandRegistry implements IDisposable { ); } } else { - return Promise.resolve(); + return; } } @@ -451,7 +451,7 @@ export class CommandRegistry implements IDisposable { ); } } else { - return Promise.resolve(); + return; } } @@ -463,7 +463,7 @@ export class CommandRegistry implements IDisposable { return activeCodeWatcher.debugCurrentCell(); } } else { - return Promise.resolve(); + return; } } From 9876718d48509ea4ad8bc96a21ec25e7ea5df6b8 Mon Sep 17 00:00:00 2001 From: earthastronaut <> Date: Thu, 30 Jul 2020 20:16:55 -0600 Subject: [PATCH 24/26] refactor unnecessary async functions to sync functions --- .../datascience/commands/commandRegistry.ts | 99 +++---------------- .../editor-integration/codewatcher.ts | 81 ++++++++------- src/client/datascience/types.ts | 20 ++-- .../codewatcher.unit.test.ts | 86 ++++++++-------- 4 files changed, 108 insertions(+), 178 deletions(-) diff --git a/src/client/datascience/commands/commandRegistry.ts b/src/client/datascience/commands/commandRegistry.ts index aaf580f6ff1d..bf477df89ab4 100644 --- a/src/client/datascience/commands/commandRegistry.ts +++ b/src/client/datascience/commands/commandRegistry.ts @@ -300,129 +300,62 @@ export class CommandRegistry implements IDisposable { this.commandManager.executeCommand('workbench.action.debug.continue'); } } + @captureTelemetry(Telemetry.AddCellBelow) private async addCellBelow(): Promise { - const activeEditor = this.documentManager.activeTextEditor; - const activeCodeWatcher = this.getCurrentCodeWatcher(); - if (activeEditor && activeCodeWatcher) { - return activeCodeWatcher.addEmptyCellToBottom(); - } + await this.getCurrentCodeWatcher()?.addEmptyCellToBottom(); } + private async runCurrentCellAndAddBelow(): Promise { - const activeCodeWatcher = this.getCurrentCodeWatcher(); - if (activeCodeWatcher) { - return activeCodeWatcher.runCurrentCellAndAddBelow(); - } else { - return; - } + this.getCurrentCodeWatcher()?.runCurrentCellAndAddBelow(); } private async insertCellBelowPosition(): Promise { - const activeCodeWatcher = this.getCurrentCodeWatcher(); - if (activeCodeWatcher) { - return activeCodeWatcher.insertCellBelowPosition(); - } else { - return; - } + this.getCurrentCodeWatcher()?.insertCellBelowPosition(); } private async insertCellBelow(): Promise { - const activeCodeWatcher = this.getCurrentCodeWatcher(); - if (activeCodeWatcher) { - return activeCodeWatcher.insertCellBelow(); - } else { - return; - } + this.getCurrentCodeWatcher()?.insertCellBelow(); } private async insertCellAbove(): Promise { - const activeCodeWatcher = this.getCurrentCodeWatcher(); - if (activeCodeWatcher) { - return activeCodeWatcher.insertCellAbove(); - } else { - return; - } + this.getCurrentCodeWatcher()?.insertCellAbove(); } private async deleteCells(): Promise { - const activeCodeWatcher = this.getCurrentCodeWatcher(); - if (activeCodeWatcher) { - return activeCodeWatcher.deleteCells(); - } else { - return; - } + this.getCurrentCodeWatcher()?.deleteCells(); } private async selectCell(): Promise { - const activeCodeWatcher = this.getCurrentCodeWatcher(); - if (activeCodeWatcher) { - return activeCodeWatcher.selectCell(); - } else { - return; - } + this.getCurrentCodeWatcher()?.selectCell(); } private async selectCellContents(): Promise { - const activeCodeWatcher = this.getCurrentCodeWatcher(); - if (activeCodeWatcher) { - return activeCodeWatcher.selectCellContents(); - } else { - return; - } + this.getCurrentCodeWatcher()?.selectCellContents(); } private async extendSelectionByCellAbove(): Promise { - const activeCodeWatcher = this.getCurrentCodeWatcher(); - if (activeCodeWatcher) { - return activeCodeWatcher.extendSelectionByCellAbove(); - } else { - return; - } + this.getCurrentCodeWatcher()?.extendSelectionByCellAbove(); } private async extendSelectionByCellBelow(): Promise { - const activeCodeWatcher = this.getCurrentCodeWatcher(); - if (activeCodeWatcher) { - return activeCodeWatcher.extendSelectionByCellBelow(); - } else { - return; - } + this.getCurrentCodeWatcher()?.extendSelectionByCellBelow(); } private async moveCellsUp(): Promise { - const activeCodeWatcher = this.getCurrentCodeWatcher(); - if (activeCodeWatcher) { - return activeCodeWatcher.moveCellsUp(); - } else { - return; - } + this.getCurrentCodeWatcher()?.moveCellsUp(); } private async moveCellsDown(): Promise { - const activeCodeWatcher = this.getCurrentCodeWatcher(); - if (activeCodeWatcher) { - return activeCodeWatcher.moveCellsDown(); - } else { - return; - } + this.getCurrentCodeWatcher()?.moveCellsDown(); } private async changeCellToMarkdown(): Promise { - const activeCodeWatcher = this.getCurrentCodeWatcher(); - if (activeCodeWatcher) { - return activeCodeWatcher.changeCellToMarkdown(); - } else { - return; - } + this.getCurrentCodeWatcher()?.changeCellToMarkdown(); } private async changeCellToCode(): Promise { - const activeCodeWatcher = this.getCurrentCodeWatcher(); - if (activeCodeWatcher) { - return activeCodeWatcher.changeCellToCode(); - } else { - return; - } + this.getCurrentCodeWatcher()?.changeCellToCode(); } private async runAllCellsAboveFromCursor(): Promise { diff --git a/src/client/datascience/editor-integration/codewatcher.ts b/src/client/datascience/editor-integration/codewatcher.ts index ae2c37c8710d..4169f880ba5b 100644 --- a/src/client/datascience/editor-integration/codewatcher.ts +++ b/src/client/datascience/editor-integration/codewatcher.ts @@ -337,11 +337,11 @@ export class CodeWatcher implements ICodeWatcher { return this.runMatchingCell(this.documentManager.activeTextEditor.selection, true); } - @captureTelemetry(Telemetry.AddEmptyCellToBottom) + // telemetry captured on CommandRegistry public async addEmptyCellToBottom(): Promise { const editor = this.documentManager.activeTextEditor; if (editor) { - return this.insertCell(editor, editor.document.lineCount + 1); + this.insertCell(editor, editor.document.lineCount + 1); } } @@ -384,41 +384,41 @@ export class CodeWatcher implements ICodeWatcher { } @captureTelemetry(Telemetry.InsertCellBelowPosition) - public async insertCellBelowPosition(): Promise { + public insertCellBelowPosition() { const editor = this.documentManager.activeTextEditor; - if (editor && editor.selection.end) { - return this.insertCell(editor, editor.selection.end.line + 1); + if (editor && editor.selection) { + this.insertCell(editor, editor.selection.end.line + 1); } } @captureTelemetry(Telemetry.InsertCellBelow) - public async insertCellBelow(): Promise { + public insertCellBelow() { const editor = this.documentManager.activeTextEditor; if (editor && editor.selection) { const cell = this.getCellFromPosition(editor.selection.end); if (cell) { - return this.insertCell(editor, cell.range.end.line + 1); + this.insertCell(editor, cell.range.end.line + 1); } else { - return this.insertCell(editor, editor.selection.end.line + 1); + this.insertCell(editor, editor.selection.end.line + 1); } } } @captureTelemetry(Telemetry.InsertCellAbove) - public async insertCellAbove(): Promise { + public insertCellAbove() { const editor = this.documentManager.activeTextEditor; if (editor && editor.selection) { const cell = this.getCellFromPosition(editor.selection.start); if (cell) { - return this.insertCell(editor, cell.range.start.line); + this.insertCell(editor, cell.range.start.line); } else { - return this.insertCell(editor, editor.selection.start.line); + this.insertCell(editor, editor.selection.start.line); } } } @captureTelemetry(Telemetry.DeleteCells) - public async deleteCells(): Promise { + public deleteCells() { const editor = this.documentManager.activeTextEditor; if (!editor || !editor.selection) { return; @@ -448,14 +448,14 @@ export class CodeWatcher implements ICodeWatcher { new Position(startLineNumber, startCharacterNumber), new Position(endLineNumber, endCharacterNumber) ); - await editor.edit((editBuilder) => { + editor.edit((editBuilder) => { editBuilder.replace(cellExtendedRange, ''); this.codeLensUpdatedEvent.fire(); }); } @captureTelemetry(Telemetry.SelectCell) - public async selectCell(): Promise { + public selectCell() { const editor = this.documentManager.activeTextEditor; if (editor && editor.selection) { const startEndCells = this.getStartEndCells(editor.selection); @@ -472,7 +472,7 @@ export class CodeWatcher implements ICodeWatcher { } @captureTelemetry(Telemetry.SelectCellContents) - public async selectCellContents(): Promise { + public selectCellContents() { const editor = this.documentManager.activeTextEditor; if (!editor || !editor.selection) { return; @@ -510,7 +510,7 @@ export class CodeWatcher implements ICodeWatcher { } @captureTelemetry(Telemetry.ExtendSelectionByCellAbove) - public async extendSelectionByCellAbove(): Promise { + public extendSelectionByCellAbove() { // This behaves similarly to excel "Extend Selection by One Cell Above". // The direction of the selection matters (i.e. where the active cursor) // position is. First, it ensures that complete cells are selection. @@ -571,7 +571,7 @@ export class CodeWatcher implements ICodeWatcher { } @captureTelemetry(Telemetry.ExtendSelectionByCellBelow) - public async extendSelectionByCellBelow(): Promise { + public extendSelectionByCellBelow() { // This behaves similarly to excel "Extend Selection by One Cell Above". // The direction of the selection matters (i.e. where the active cursor) // position is. First, it ensures that complete cells are selection. @@ -635,46 +635,43 @@ export class CodeWatcher implements ICodeWatcher { @captureTelemetry(Telemetry.MoveCellsUp) public async moveCellsUp(): Promise { - return this.moveCellsDirection(true); + await this.moveCellsDirection(true); } @captureTelemetry(Telemetry.MoveCellsDown) public async moveCellsDown(): Promise { - return this.moveCellsDirection(false); + await this.moveCellsDirection(false); } @captureTelemetry(Telemetry.ChangeCellToMarkdown) - public async changeCellToMarkdown(): Promise { - await this.applyToCells(async (editor, cell, _) => { + public changeCellToMarkdown() { + this.applyToCells((editor, cell, _) => { return this.changeCellTo(editor, cell, 'markdown'); }); } @captureTelemetry(Telemetry.ChangeCellToCode) - public async changeCellToCode(): Promise { - await this.applyToCells(async (editor, cell, _) => { + public changeCellToCode() { + this.applyToCells((editor, cell, _) => { return this.changeCellTo(editor, cell, 'code'); }); } - private async applyToCells( - callback: (editor: TextEditor, cell: ICellRange, cellIndex: number) => Thenable - ): Promise { + private applyToCells(callback: (editor: TextEditor, cell: ICellRange, cellIndex: number) => void) { const editor = this.documentManager.activeTextEditor; const startEndCellIndex = this.getStartEndCellIndex(editor?.selection); if (!editor || !startEndCellIndex) { - return false; + return; } const cells = this.cells; const startIndex = startEndCellIndex[0]; const endIndex = startEndCellIndex[1]; for (let cellIndex = startIndex; cellIndex <= endIndex; cellIndex += 1) { - await callback(editor, cells[cellIndex], cellIndex); + callback(editor, cells[cellIndex], cellIndex); } - return true; } - private async changeCellTo(editor: TextEditor, cell: ICellRange, toCellType: nbformat.CellType): Promise { + private changeCellTo(editor: TextEditor, cell: ICellRange, toCellType: nbformat.CellType) { // change cell from code -> markdown or markdown -> code if (toCellType === 'raw') { throw Error('Cell Type raw not implemented'); @@ -682,7 +679,7 @@ export class CodeWatcher implements ICodeWatcher { // don't change cell type if already that type if (cell.cell_type === toCellType) { - return false; + return; } const cellMatcher = new CellMatcher(this.configService.getSettings(editor.document.uri).datascience); const definitionLine = editor.document.lineAt(cell.range.start.line); @@ -695,7 +692,7 @@ export class CodeWatcher implements ICodeWatcher { ? cellMatcher.codeExecRegEx.exec(definitionText) // code -> markdown : cellMatcher.markdownExecRegEx.exec(definitionText); // markdown -> code if (!definitionMatch) { - return false; + return; } const definitionExtra = definitionMatch[definitionMatch.length - 1]; const newDefinitionText = @@ -703,7 +700,7 @@ export class CodeWatcher implements ICodeWatcher { ? `${cellMarker} [markdown]${definitionExtra}` // code -> markdown : `${cellMarker}${definitionExtra}`; // markdown -> code - return editor.edit(async (editBuilder) => { + editor.edit(async (editBuilder) => { editBuilder.replace(definitionLine.range, newDefinitionText); cell.cell_type = toCellType; if (cell.range.start.line < cell.range.end.line) { @@ -716,22 +713,22 @@ export class CodeWatcher implements ICodeWatcher { // ensure all lines in markdown cell have a comment. // these are not included in the test because it's unclear // how TypeMoq works with them. - await commands.executeCommand('editor.action.removeCommentLine'); + commands.executeCommand('editor.action.removeCommentLine'); if (toCellType === 'markdown') { - await commands.executeCommand('editor.action.addCommentLine'); + commands.executeCommand('editor.action.addCommentLine'); } } }); } - private async moveCellsDirection(directionUp: boolean): Promise { + private async moveCellsDirection(directionUp: boolean): Promise { const editor = this.documentManager.activeTextEditor; if (!editor || !editor.selection) { - return; + return false; } const startEndCellIndex = this.getStartEndCellIndex(editor.selection); if (!startEndCellIndex) { - return; + return false; } const startCellIndex = startEndCellIndex[0]; const endCellIndex = startEndCellIndex[1]; @@ -739,7 +736,7 @@ export class CodeWatcher implements ICodeWatcher { const startCell = cells[startCellIndex]; const endCell = cells[endCellIndex]; if (!startCell || !endCell) { - return; + return false; } const currentRange = new Range(startCell.range.start, endCell.range.end); const relativeSelectionRange = new Range( @@ -752,7 +749,7 @@ export class CodeWatcher implements ICodeWatcher { let thenSetSelection: Thenable; if (directionUp) { if (startCellIndex === 0) { - return; + return false; } else { const aboveCell = cells[startCellIndex - 1]; const thenExchangeTextLines = this.exchangeTextLines(editor, aboveCell.range, currentRange); @@ -770,7 +767,7 @@ export class CodeWatcher implements ICodeWatcher { } } else { if (endCellIndex === cells.length - 1) { - return; + return false; } else { const belowCell = cells[endCellIndex + 1]; const thenExchangeTextLines = this.exchangeTextLines(editor, currentRange, belowCell.range); @@ -794,7 +791,7 @@ export class CodeWatcher implements ICodeWatcher { if (isEditSuccessful && isActiveBeforeAnchor) { editor.selection = new Selection(editor.selection.active, editor.selection.anchor); } - return; + return true; }); } diff --git a/src/client/datascience/types.ts b/src/client/datascience/types.ts index 1a8c3dd5eb5e..06f3601d4f7e 100644 --- a/src/client/datascience/types.ts +++ b/src/client/datascience/types.ts @@ -627,18 +627,18 @@ export interface ICodeWatcher { debugFileInteractive(): Promise; addEmptyCellToBottom(): Promise; runCurrentCellAndAddBelow(): Promise; - insertCellBelowPosition(): Promise; - insertCellBelow(): Promise; - insertCellAbove(): Promise; - deleteCells(): Promise; - selectCell(): Promise; - selectCellContents(): Promise; - extendSelectionByCellAbove(): Promise; - extendSelectionByCellBelow(): Promise; + insertCellBelowPosition(): void; + insertCellBelow(): void; + insertCellAbove(): void; + deleteCells(): void; + selectCell(): void; + selectCellContents(): void; + extendSelectionByCellAbove(): void; + extendSelectionByCellBelow(): void; moveCellsUp(): Promise; moveCellsDown(): Promise; - changeCellToMarkdown(): Promise; - changeCellToCode(): Promise; + changeCellToMarkdown(): void; + changeCellToCode(): void; debugCurrentCell(): Promise; } diff --git a/src/test/datascience/editor-integration/codewatcher.unit.test.ts b/src/test/datascience/editor-integration/codewatcher.unit.test.ts index 3839fc84518d..3d7f414ab9c4 100644 --- a/src/test/datascience/editor-integration/codewatcher.unit.test.ts +++ b/src/test/datascience/editor-integration/codewatcher.unit.test.ts @@ -1062,7 +1062,7 @@ testing2` mockTextEditor.selection = new Selection(0, 4, 0, 4); - await codeWatcher.insertCellBelowPosition(); + codeWatcher.insertCellBelowPosition(); expect(mockTextEditor.document.getText()).to.equal(`testing0 # %% @@ -1091,7 +1091,7 @@ testing2` // end selection at bottom of document mockTextEditor.selection = new Selection(1, 4, 5, 8); - await codeWatcher.insertCellBelowPosition(); + codeWatcher.insertCellBelowPosition(); expect(mockTextEditor.document.getText()).to.equal(`testing0 #%% @@ -1120,7 +1120,7 @@ testing2` mockTextEditor.selection = new Selection(2, 4, 2, 4); - await codeWatcher.insertCellBelow(); + codeWatcher.insertCellBelow(); expect(mockTextEditor.document.getText()).to.equal( `testing0 @@ -1151,7 +1151,7 @@ testing2` mockTextEditor.selection = new Selection(0, 4, 0, 4); - await codeWatcher.insertCellBelow(); + codeWatcher.insertCellBelow(); expect(mockTextEditor.document.getText()).to.equal(`testing0 # %% @@ -1181,7 +1181,7 @@ testing2` // range crossing multiple cells.Insert below bottom of range. mockTextEditor.selection = new Selection(0, 4, 2, 4); - await codeWatcher.insertCellBelow(); + codeWatcher.insertCellBelow(); expect(mockTextEditor.document.getText()).to.equal( `testing0 @@ -1214,7 +1214,7 @@ testing2` // above the first cell of the range mockTextEditor.selection = new Selection(3, 4, 5, 4); - await codeWatcher.insertCellAbove(); + codeWatcher.insertCellAbove(); expect(mockTextEditor.document.getText()).to.equal( `testing0 @@ -1246,7 +1246,7 @@ testing2` mockTextEditor.selection = new Selection(0, 3, 0, 4); - await codeWatcher.insertCellAbove(); + codeWatcher.insertCellAbove(); expect(mockTextEditor.document.getText()).to.equal( `# %% @@ -1278,7 +1278,7 @@ testing2` mockTextEditor.selection = new Selection(3, 4, 3, 4); - await codeWatcher.deleteCells(); + codeWatcher.deleteCells(); expect(mockTextEditor.document.getText()).to.equal( `testing0 @@ -1301,7 +1301,7 @@ testing2` mockTextEditor.selection = new Selection(3, 4, 5, 4); - await codeWatcher.deleteCells(); + codeWatcher.deleteCells(); expect(mockTextEditor.document.getText()).to.equal(`testing0`); }); @@ -1320,7 +1320,7 @@ testing2` mockTextEditor.selection = new Selection(0, 1, 0, 4); - await codeWatcher.deleteCells(); + codeWatcher.deleteCells(); expect(mockTextEditor.document.getText()).to.equal(`testing0 #%% @@ -1344,7 +1344,7 @@ testing2` mockTextEditor.selection = new Selection(2, 1, 2, 1); - await codeWatcher.selectCell(); + codeWatcher.selectCell(); expect(mockTextEditor.selection.anchor.line).to.equal(1); expect(mockTextEditor.selection.anchor.character).to.equal(0); @@ -1366,7 +1366,7 @@ testing2` mockTextEditor.selection = new Selection(2, 1, 4, 1); - await codeWatcher.selectCell(); + codeWatcher.selectCell(); expect(mockTextEditor.selection.anchor.line).to.equal(1); expect(mockTextEditor.selection.anchor.character).to.equal(0); @@ -1388,7 +1388,7 @@ testing2` mockTextEditor.selection = new Selection(4, 1, 2, 1); - await codeWatcher.selectCell(); + codeWatcher.selectCell(); expect(mockTextEditor.selection.active.line).to.equal(1); expect(mockTextEditor.selection.active.character).to.equal(0); @@ -1410,7 +1410,7 @@ testing2` mockTextEditor.selection = new Selection(0, 1, 0, 4); - await codeWatcher.selectCell(); + codeWatcher.selectCell(); expect(mockTextEditor.selection.start.line).to.equal(0); expect(mockTextEditor.selection.start.character).to.equal(1); @@ -1432,7 +1432,7 @@ testing2` mockTextEditor.selection = new Selection(3, 4, 3, 4); - await codeWatcher.selectCellContents(); + codeWatcher.selectCellContents(); expect(mockTextEditor.selections.length).to.equal(1); @@ -1457,7 +1457,7 @@ testing2` mockTextEditor.selection = new Selection(3, 4, 5, 4); - await codeWatcher.selectCellContents(); + codeWatcher.selectCellContents(); expect(mockTextEditor.selections.length).to.equal(2); @@ -1489,7 +1489,7 @@ testing2` mockTextEditor.selection = new Selection(5, 4, 3, 4); - await codeWatcher.selectCellContents(); + codeWatcher.selectCellContents(); expect(mockTextEditor.selections.length).to.equal(2); @@ -1524,7 +1524,7 @@ testing_L8` mockTextEditor.selection = new Selection(5, 2, 5, 2); - await codeWatcher.extendSelectionByCellAbove(); + codeWatcher.extendSelectionByCellAbove(); expect(mockTextEditor.selection.anchor.line).to.equal(6); expect(mockTextEditor.selection.anchor.character).to.equal(10); @@ -1549,7 +1549,7 @@ testing_L8` mockTextEditor.selection = new Selection(5, 2, 6, 4); - await codeWatcher.extendSelectionByCellAbove(); + codeWatcher.extendSelectionByCellAbove(); expect(mockTextEditor.selection.anchor.line).to.equal(6); expect(mockTextEditor.selection.anchor.character).to.equal(10); @@ -1574,7 +1574,7 @@ testing_L8` mockTextEditor.selection = new Selection(6, 4, 5, 2); - await codeWatcher.extendSelectionByCellAbove(); + codeWatcher.extendSelectionByCellAbove(); expect(mockTextEditor.selection.anchor.line).to.equal(6); expect(mockTextEditor.selection.anchor.character).to.equal(10); @@ -1599,7 +1599,7 @@ testing_L8` mockTextEditor.selection = new Selection(5, 2, 8, 2); - await codeWatcher.extendSelectionByCellAbove(); + codeWatcher.extendSelectionByCellAbove(); expect(mockTextEditor.selection.anchor.line).to.equal(4); expect(mockTextEditor.selection.anchor.character).to.equal(0); @@ -1624,7 +1624,7 @@ testing_L8` mockTextEditor.selection = new Selection(8, 2, 5, 2); - await codeWatcher.extendSelectionByCellAbove(); + codeWatcher.extendSelectionByCellAbove(); expect(mockTextEditor.selection.anchor.line).to.equal(8); expect(mockTextEditor.selection.anchor.character).to.equal(10); @@ -1649,7 +1649,7 @@ testing_L8` mockTextEditor.selection = new Selection(6, 10, 4, 0); - await codeWatcher.extendSelectionByCellAbove(); + codeWatcher.extendSelectionByCellAbove(); expect(mockTextEditor.selection.anchor.line).to.equal(6); expect(mockTextEditor.selection.anchor.character).to.equal(10); @@ -1674,7 +1674,7 @@ testing_L8` mockTextEditor.selection = new Selection(1, 0, 6, 10); - await codeWatcher.extendSelectionByCellAbove(); + codeWatcher.extendSelectionByCellAbove(); expect(mockTextEditor.selection.anchor.line).to.equal(1); expect(mockTextEditor.selection.anchor.character).to.equal(0); @@ -1699,7 +1699,7 @@ testing_L8` mockTextEditor.selection = new Selection(5, 2, 5, 2); - await codeWatcher.extendSelectionByCellBelow(); + codeWatcher.extendSelectionByCellBelow(); expect(mockTextEditor.selection.anchor.line).to.equal(4); expect(mockTextEditor.selection.anchor.character).to.equal(0); @@ -1724,7 +1724,7 @@ testing_L8` mockTextEditor.selection = new Selection(5, 2, 6, 4); - await codeWatcher.extendSelectionByCellBelow(); + codeWatcher.extendSelectionByCellBelow(); expect(mockTextEditor.selection.anchor.line).to.equal(4); expect(mockTextEditor.selection.anchor.character).to.equal(0); @@ -1749,7 +1749,7 @@ testing_L8` mockTextEditor.selection = new Selection(6, 4, 5, 2); - await codeWatcher.extendSelectionByCellBelow(); + codeWatcher.extendSelectionByCellBelow(); expect(mockTextEditor.selection.anchor.line).to.equal(4); expect(mockTextEditor.selection.anchor.character).to.equal(0); @@ -1774,7 +1774,7 @@ testing_L8` mockTextEditor.selection = new Selection(3, 2, 6, 2); - await codeWatcher.extendSelectionByCellBelow(); + codeWatcher.extendSelectionByCellBelow(); expect(mockTextEditor.selection.anchor.line).to.equal(1); expect(mockTextEditor.selection.anchor.character).to.equal(0); @@ -1799,7 +1799,7 @@ testing_L8` mockTextEditor.selection = new Selection(6, 2, 3, 2); - await codeWatcher.extendSelectionByCellBelow(); + codeWatcher.extendSelectionByCellBelow(); expect(mockTextEditor.selection.anchor.line).to.equal(4); expect(mockTextEditor.selection.anchor.character).to.equal(0); @@ -1824,7 +1824,7 @@ testing_L8` mockTextEditor.selection = new Selection(6, 10, 4, 0); - await codeWatcher.extendSelectionByCellBelow(); + codeWatcher.extendSelectionByCellBelow(); expect(mockTextEditor.selection.anchor.line).to.equal(4); expect(mockTextEditor.selection.anchor.character).to.equal(0); @@ -1849,7 +1849,7 @@ testing_L8` mockTextEditor.selection = new Selection(6, 10, 1, 0); - await codeWatcher.extendSelectionByCellBelow(); + codeWatcher.extendSelectionByCellBelow(); expect(mockTextEditor.selection.anchor.line).to.equal(6); expect(mockTextEditor.selection.anchor.character).to.equal(10); @@ -1874,14 +1874,14 @@ testing_L8` mockTextEditor.selection = new Selection(5, 2, 6, 2); - await codeWatcher.extendSelectionByCellAbove(); // select full cell - await codeWatcher.extendSelectionByCellAbove(); // select cell above - await codeWatcher.extendSelectionByCellAbove(); // top cell no change - await codeWatcher.extendSelectionByCellAbove(); // top cell no change - await codeWatcher.extendSelectionByCellBelow(); // contract by cell - await codeWatcher.extendSelectionByCellBelow(); // expand by cell below - await codeWatcher.extendSelectionByCellBelow(); // last cell no change - await codeWatcher.extendSelectionByCellAbove(); // Original cell + codeWatcher.extendSelectionByCellAbove(); // select full cell + codeWatcher.extendSelectionByCellAbove(); // select cell above + codeWatcher.extendSelectionByCellAbove(); // top cell no change + codeWatcher.extendSelectionByCellAbove(); // top cell no change + codeWatcher.extendSelectionByCellBelow(); // contract by cell + codeWatcher.extendSelectionByCellBelow(); // expand by cell below + codeWatcher.extendSelectionByCellBelow(); // last cell no change + codeWatcher.extendSelectionByCellAbove(); // Original cell expect(mockTextEditor.selection.anchor.line).to.equal(4); expect(mockTextEditor.selection.anchor.character).to.equal(0); @@ -2014,7 +2014,7 @@ testing_L6 mockTextEditor.selection = new Selection(1, 2, 5, 5); - await codeWatcher.changeCellToMarkdown(); + codeWatcher.changeCellToMarkdown(); // NOTE: When running the function in real environment there // are comment lines added in addition to the [markdown] definition. @@ -2065,7 +2065,7 @@ testing_L6 mockTextEditor.selection = new Selection(1, 2, 5, 5); - await codeWatcher.changeCellToMarkdown(); + codeWatcher.changeCellToMarkdown(); expect(mockTextEditor.document.getText()).to.equal(text); @@ -2092,7 +2092,7 @@ testing_L6 mockTextEditor.selection = new Selection(1, 2, 5, 5); - await codeWatcher.changeCellToCode(); + codeWatcher.changeCellToCode(); // NOTE: When running the function in real environment there // are comment lines added in addition to the [markdown] definition. @@ -2143,7 +2143,7 @@ testing_L3 mockTextEditor.selection = new Selection(1, 2, 5, 5); - await codeWatcher.changeCellToCode(); + codeWatcher.changeCellToCode(); expect(mockTextEditor.document.getText()).to.equal(text); From 0b83e38d2f2ff87bb3ad9ecaabf2d810a3ec0b23 Mon Sep 17 00:00:00 2001 From: earthastronaut <> Date: Thu, 30 Jul 2020 20:23:16 -0600 Subject: [PATCH 25/26] added unit tests for move cells down --- .../codewatcher.unit.test.ts | 108 ++++++++++++++++++ 1 file changed, 108 insertions(+) diff --git a/src/test/datascience/editor-integration/codewatcher.unit.test.ts b/src/test/datascience/editor-integration/codewatcher.unit.test.ts index 3d7f414ab9c4..53bd6e9435e2 100644 --- a/src/test/datascience/editor-integration/codewatcher.unit.test.ts +++ b/src/test/datascience/editor-integration/codewatcher.unit.test.ts @@ -1997,6 +1997,114 @@ testing_L8` expect(mockTextEditor.selection.active.character).to.equal(5); }); + test('Move cells down', async () => { + const mockTextEditor = initializeMockTextEditor( + codeWatcher, + documentManager, + `testing_L0 +# %% +testing_L2 +testing_L3 +# %% +testing_L5 +testing_L6 +# %% +testing_L8` + ); + + mockTextEditor.selection = new Selection(5, 5, 5, 5); + + await codeWatcher.moveCellsDown(); + + expect(mockTextEditor.document.getText()).to.equal( + `testing_L0 +# %% +testing_L2 +testing_L3 +# %% +testing_L8 +# %% +testing_L5 +testing_L6` + ); + expect(mockTextEditor.selection.anchor.line).to.equal(7); + expect(mockTextEditor.selection.anchor.character).to.equal(5); + expect(mockTextEditor.selection.active.line).to.equal(7); + expect(mockTextEditor.selection.active.character).to.equal(5); + }); + + test('Move cells down multiple cells', async () => { + const mockTextEditor = initializeMockTextEditor( + codeWatcher, + documentManager, + `testing_L0 +# %% +testing_L2 +testing_L3 +# %% +testing_L5 +testing_L6 +# %% +testing_L8` + ); + + mockTextEditor.selection = new Selection(2, 2, 5, 5); + + await codeWatcher.moveCellsDown(); + + expect(mockTextEditor.document.getText()).to.equal( + `testing_L0 +# %% +testing_L8 +# %% +testing_L2 +testing_L3 +# %% +testing_L5 +testing_L6` + ); + expect(mockTextEditor.selection.anchor.line).to.equal(4); + expect(mockTextEditor.selection.anchor.character).to.equal(2); + expect(mockTextEditor.selection.active.line).to.equal(7); + expect(mockTextEditor.selection.active.character).to.equal(5); + }); + + test('Move cells down last cell no change', async () => { + const mockTextEditor = initializeMockTextEditor( + codeWatcher, + documentManager, + `testing_L0 +# %% +testing_L2 +testing_L3 +# %% +testing_L5 +testing_L6 +# %% +testing_L8` + ); + + mockTextEditor.selection = new Selection(5, 5, 8, 5); + + await codeWatcher.moveCellsDown(); + + expect(mockTextEditor.document.getText()).to.equal( + `testing_L0 +# %% +testing_L2 +testing_L3 +# %% +testing_L5 +testing_L6 +# %% +testing_L8` + ); + expect(mockTextEditor.selection.anchor.line).to.equal(5); + expect(mockTextEditor.selection.anchor.character).to.equal(5); + expect(mockTextEditor.selection.active.line).to.equal(8); + expect(mockTextEditor.selection.active.character).to.equal(5); + }); + test('Change cell to markdown', async () => { const mockTextEditor = initializeMockTextEditor( codeWatcher, From 716200bc03338efa82ba4ae9257b1fdae7996886 Mon Sep 17 00:00:00 2001 From: earthastronaut <> Date: Sat, 1 Aug 2020 08:52:26 -0600 Subject: [PATCH 26/26] change the when for commands to display when the python file has cells --- package.json | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/package.json b/package.json index 9129f54d7704..20172e8164cb 100644 --- a/package.json +++ b/package.json @@ -1075,73 +1075,73 @@ "command": "python.datascience.insertCellBelowPosition", "title": "%python.command.python.datascience.insertCellBelowPosition.title%", "category": "Python", - "when": "python.datascience.hascodecells && python.datascience.featureenabled && python.datascience.ispythonornativeactive" + "when": "python.datascience.hascodecells && editorFocus && editorLangId == python && python.datascience.featureenabled" }, { "command": "python.datascience.insertCellBelow", "title": "%python.command.python.datascience.insertCellBelow.title%", "category": "Python", - "when": "python.datascience.hascodecells && python.datascience.featureenabled && python.datascience.ispythonornativeactive" + "when": "python.datascience.hascodecells && editorFocus && editorLangId == python && python.datascience.featureenabled" }, { "command": "python.datascience.insertCellAbove", "title": "%python.command.python.datascience.insertCellAbove.title%", "category": "Python", - "when": "python.datascience.hascodecells && python.datascience.featureenabled && python.datascience.ispythonornativeactive" + "when": "python.datascience.hascodecells && editorFocus && editorLangId == python && python.datascience.featureenabled" }, { "command": "python.datascience.deleteCells", "title": "%python.command.python.datascience.deleteCells.title%", "category": "Python", - "when": "python.datascience.hascodecells && python.datascience.featureenabled && python.datascience.ispythonornativeactive" + "when": "python.datascience.hascodecells && editorFocus && editorLangId == python && python.datascience.featureenabled" }, { "command": "python.datascience.selectCell", "title": "%python.command.python.datascience.selectCell.title%", "category": "Python", - "when": "python.datascience.hascodecells && python.datascience.featureenabled && python.datascience.ispythonornativeactive" + "when": "python.datascience.hascodecells && editorFocus && editorLangId == python && python.datascience.featureenabled" }, { "command": "python.datascience.selectCellContents", "title": "%python.command.python.datascience.selectCellContents.title%", "category": "Python", - "when": "python.datascience.hascodecells && python.datascience.featureenabled && python.datascience.ispythonornativeactive" + "when": "python.datascience.hascodecells && editorFocus && editorLangId == python && python.datascience.featureenabled" }, { "command": "python.datascience.extendSelectionByCellAbove", "title": "%python.command.python.datascience.extendSelectionByCellAbove.title%", "category": "Python", - "when": "python.datascience.hascodecells && python.datascience.featureenabled && python.datascience.ispythonornativeactive" + "when": "python.datascience.hascodecells && editorFocus && editorLangId == python && python.datascience.featureenabled" }, { "command": "python.datascience.extendSelectionByCellBelow", "title": "%python.command.python.datascience.extendSelectionByCellBelow.title%", "category": "Python", - "when": "python.datascience.hascodecells && python.datascience.featureenabled && python.datascience.ispythonornativeactive" + "when": "python.datascience.hascodecells && editorFocus && editorLangId == python && python.datascience.featureenabled" }, { "command": "python.datascience.moveCellsUp", "title": "%python.command.python.datascience.moveCellsUp.title%", "category": "Python", - "when": "python.datascience.hascodecells && python.datascience.featureenabled && python.datascience.ispythonornativeactive" + "when": "python.datascience.hascodecells && editorFocus && editorLangId == python && python.datascience.featureenabled" }, { "command": "python.datascience.moveCellsDown", "title": "%python.command.python.datascience.moveCellsDown.title%", "category": "Python", - "when": "python.datascience.hascodecells && python.datascience.featureenabled && python.datascience.ispythonornativeactive" + "when": "python.datascience.hascodecells && editorFocus && editorLangId == python && python.datascience.featureenabled" }, { "command": "python.datascience.changeCellToMarkdown", "title": "%python.command.python.datascience.changeCellToMarkdown.title%", "category": "Python", - "when": "python.datascience.hascodecells && python.datascience.featureenabled && python.datascience.ispythonornativeactive" + "when": "python.datascience.hascodecells && editorFocus && editorLangId == python && python.datascience.featureenabled" }, { "command": "python.datascience.changeCellToCode", "title": "%python.command.python.datascience.changeCellToCode.title%", "category": "Python", - "when": "python.datascience.hascodecells && python.datascience.featureenabled && python.datascience.ispythonornativeactive" + "when": "python.datascience.hascodecells && editorFocus && editorLangId == python && python.datascience.featureenabled" }, { "command": "python.datascience.runcurrentcell", @@ -3664,4 +3664,4 @@ "publisherDisplayName": "Microsoft", "publisherId": "998b010b-e2af-44a5-a6cd-0b5fd3b9b6f8" } -} \ No newline at end of file +}