diff --git a/src/vs/vscode.proposed.d.ts b/src/vs/vscode.proposed.d.ts index 64e9072b04d..406007f866b 100644 --- a/src/vs/vscode.proposed.d.ts +++ b/src/vs/vscode.proposed.d.ts @@ -1264,11 +1264,13 @@ declare module 'vscode' { // todo@API should not be undefined, rather a default readonly selection?: NotebookCell; - // @rebornix - // todo@API should replace selection - // never empty! - // primary/secondary selections - // readonly selections: NotebookCellRange[]; + /** + * todo@API should replace selection + * The selections on this notebook editor. + * + * The primary selection (or focused range) is `selections[0]`. When the document has no cells, the primary selection is empty `{ start: 0, end: 0 }`; + */ + readonly selections: NotebookCellRange[]; /** * The current visible ranges in the editor (vertically). diff --git a/src/vs/workbench/api/browser/mainThreadNotebook.ts b/src/vs/workbench/api/browser/mainThreadNotebook.ts index 2fc6d313a7e..a50501f24fe 100644 --- a/src/vs/workbench/api/browser/mainThreadNotebook.ts +++ b/src/vs/workbench/api/browser/mainThreadNotebook.ts @@ -96,7 +96,7 @@ class DocumentAndEditorState { return { id: add.getId(), documentUri: add.uri!, - selections: add.getSelectionHandles(), + selections: add.getSelections(), visibleRanges: add.visibleRanges }; } @@ -244,8 +244,7 @@ export class MainThreadNotebooks extends Disposable implements MainThreadNoteboo })); disposableStore.add(editor.onDidChangeSelection(() => { - const selectionHandles = editor.getSelectionHandles(); - this._proxy.$acceptEditorPropertiesChanged(editor.getId(), { visibleRanges: null, selections: { selections: selectionHandles } }); + this._proxy.$acceptEditorPropertiesChanged(editor.getId(), { visibleRanges: null, selections: { selections: editor.getSelections() } }); })); this._editorEventListenersMapping.set(editor.getId(), disposableStore); @@ -689,7 +688,7 @@ export class MainThreadNotebooks extends Disposable implements MainThreadNoteboo if (notebookEditor.viewModel && options.selection && notebookEditor.viewModel.viewCells[options.selection.start]) { const focusedCell = notebookEditor.viewModel.viewCells[options.selection.start]; notebookEditor.revealInCenterIfOutsideViewport(focusedCell); - notebookEditor.selectElement(focusedCell); + notebookEditor.focusElement(focusedCell); } return notebookEditor.getId(); } else { diff --git a/src/vs/workbench/api/common/extHost.protocol.ts b/src/vs/workbench/api/common/extHost.protocol.ts index 0e15a91c0d8..ae95efe38b8 100644 --- a/src/vs/workbench/api/common/extHost.protocol.ts +++ b/src/vs/workbench/api/common/extHost.protocol.ts @@ -1742,8 +1742,7 @@ export interface ExtHostCommentsShape { } export interface INotebookSelectionChangeEvent { - // handles - selections: number[]; + selections: ICellRange[]; } export interface INotebookVisibleRangesEvent { @@ -1771,7 +1770,7 @@ export interface INotebookModelAddedData { export interface INotebookEditorAddData { id: string; documentUri: UriComponents; - selections: number[]; + selections: ICellRange[]; visibleRanges: ICellRange[]; } diff --git a/src/vs/workbench/api/common/extHostNotebook.ts b/src/vs/workbench/api/common/extHostNotebook.ts index 9e14a515811..7ef370259ef 100644 --- a/src/vs/workbench/api/common/extHostNotebook.ts +++ b/src/vs/workbench/api/common/extHostNotebook.ts @@ -17,7 +17,7 @@ import { IExtensionStoragePaths } from 'vs/workbench/api/common/extHostStoragePa import * as typeConverters from 'vs/workbench/api/common/extHostTypeConverters'; import * as extHostTypes from 'vs/workbench/api/common/extHostTypes'; import { asWebviewUri, WebviewInitData } from 'vs/workbench/api/common/shared/webview'; -import { CellStatusbarAlignment, CellUri, INotebookCellStatusBarEntry, INotebookExclusiveDocumentFilter, NotebookCellMetadata, NotebookCellsChangedEventDto, NotebookCellsChangeType, NotebookDataDto, notebookDocumentMetadataDefaults } from 'vs/workbench/contrib/notebook/common/notebookCommon'; +import { CellStatusbarAlignment, CellUri, ICellRange, INotebookCellStatusBarEntry, INotebookExclusiveDocumentFilter, NotebookCellMetadata, NotebookCellsChangedEventDto, NotebookCellsChangeType, NotebookDataDto, notebookDocumentMetadataDefaults } from 'vs/workbench/contrib/notebook/common/notebookCommon'; import * as vscode from 'vscode'; import { ResourceMap } from 'vs/base/common/map'; import { ExtHostCell, ExtHostNotebookDocument } from './extHostNotebookDocument'; @@ -589,16 +589,11 @@ export class ExtHostNotebookController implements ExtHostNotebookShape { } if (data.selections) { - if (data.selections.selections.length) { - const firstCell = data.selections.selections[0]; - editor.editor.selection = editor.editor.notebookData.getCell(firstCell)?.cell; - } else { - editor.editor.selection = undefined; - } + editor.editor._acceptSelections(data.selections.selections); this._onDidChangeNotebookEditorSelection.fire({ notebookEditor: editor.editor.editor, - selection: editor.editor.selection + selection: editor.editor.editor.selection }); } } @@ -609,7 +604,7 @@ export class ExtHostNotebookController implements ExtHostNotebookShape { document.acceptDocumentPropertiesChanged(data); } - private _createExtHostEditor(document: ExtHostNotebookDocument, editorId: string, selections: number[], visibleRanges: extHostTypes.NotebookCellRange[]) { + private _createExtHostEditor(document: ExtHostNotebookDocument, editorId: string, selections: ICellRange[], visibleRanges: extHostTypes.NotebookCellRange[]) { const revivedUri = document.uri; let webComm = this._webviewComm.get(editorId); @@ -625,13 +620,7 @@ export class ExtHostNotebookController implements ExtHostNotebookShape { document ); - if (selections.length) { - const firstCell = selections[0]; - editor.selection = editor.notebookData.getCell(firstCell)?.cell; - } else { - editor.selection = undefined; - } - + editor._acceptSelections(selections); editor._acceptVisibleRanges(visibleRanges); this._editors.get(editorId)?.editor.dispose(); diff --git a/src/vs/workbench/api/common/extHostNotebookDocument.ts b/src/vs/workbench/api/common/extHostNotebookDocument.ts index b34332aa23e..df2833cb03d 100644 --- a/src/vs/workbench/api/common/extHostNotebookDocument.ts +++ b/src/vs/workbench/api/common/extHostNotebookDocument.ts @@ -326,6 +326,10 @@ export class ExtHostNotebookDocument extends Disposable { this._emitter.emitCellMetadataChange(event); } + getCellFromIndex(index: number): ExtHostCell | undefined { + return this._cells[index]; + } + getCell(cellHandle: number): ExtHostCell | undefined { return this._cells.find(cell => cell.handle === cellHandle); } diff --git a/src/vs/workbench/api/common/extHostNotebookEditor.ts b/src/vs/workbench/api/common/extHostNotebookEditor.ts index e0d9f9426a7..495c0ebad03 100644 --- a/src/vs/workbench/api/common/extHostNotebookEditor.ts +++ b/src/vs/workbench/api/common/extHostNotebookEditor.ts @@ -8,7 +8,7 @@ import { Emitter, Event } from 'vs/base/common/event'; import { MainThreadNotebookShape } from 'vs/workbench/api/common/extHost.protocol'; import * as extHostTypes from 'vs/workbench/api/common/extHostTypes'; import * as extHostConverter from 'vs/workbench/api/common/extHostTypeConverters'; -import { CellEditType, ICellEditOperation, ICellReplaceEdit, notebookDocumentMetadataDefaults } from 'vs/workbench/contrib/notebook/common/notebookCommon'; +import { CellEditType, ICellEditOperation, ICellRange, ICellReplaceEdit, notebookDocumentMetadataDefaults } from 'vs/workbench/contrib/notebook/common/notebookCommon'; import * as vscode from 'vscode'; import { ExtHostNotebookDocument } from './extHostNotebookDocument'; @@ -85,9 +85,8 @@ class NotebookEditorCellEditBuilder implements vscode.NotebookEditorEdit { } export class ExtHostNotebookEditor { - - //TODO@rebornix noop setter? - selection?: vscode.NotebookCell; + private _selection?: vscode.NotebookCell; + private _selections: vscode.NotebookCellRange[] = []; private _visibleRanges: extHostTypes.NotebookCellRange[] = []; private _viewColumn?: vscode.ViewColumn; @@ -123,7 +122,10 @@ export class ExtHostNotebookEditor { return that.notebookData.notebookDocument; }, get selection() { - return that.selection; + return that._selection; + }, + get selections() { + return that._selections; }, get visibleRanges() { return that._visibleRanges; @@ -173,6 +175,12 @@ export class ExtHostNotebookEditor { this._visibleRanges = value; } + _acceptSelections(selections: ICellRange[]): void { + const primarySelection = selections[0]; + this._selection = primarySelection ? this.notebookData.getCellFromIndex(primarySelection.start)?.cell : undefined; + this._selections = selections.map(val => new extHostTypes.NotebookCellRange(val.start, val.end)); + } + get active(): boolean { return this._active; } diff --git a/src/vs/workbench/contrib/notebook/browser/contrib/find/findController.ts b/src/vs/workbench/contrib/notebook/browser/contrib/find/findController.ts index c7f0f53cd1f..baab6d97611 100644 --- a/src/vs/workbench/contrib/notebook/browser/contrib/find/findController.ts +++ b/src/vs/workbench/contrib/notebook/browser/contrib/find/findController.ts @@ -160,7 +160,7 @@ export class NotebookFindWidget extends SimpleFindReplaceWidget implements INote private revealCellRange(cellIndex: number, matchIndex: number) { this._findMatches[cellIndex].cell.editState = CellEditState.Editing; - this._notebookEditor.selectElement(this._findMatches[cellIndex].cell); + this._notebookEditor.focusElement(this._findMatches[cellIndex].cell); this._notebookEditor.setCellSelection(this._findMatches[cellIndex].cell, this._findMatches[cellIndex].matches[matchIndex].range); this._notebookEditor.revealRangeInCenterIfOutsideViewportAsync(this._findMatches[cellIndex].cell, this._findMatches[cellIndex].matches[matchIndex].range); } diff --git a/src/vs/workbench/contrib/notebook/browser/contrib/fold/folding.ts b/src/vs/workbench/contrib/notebook/browser/contrib/fold/folding.ts index 0235b4431fb..7af4d88c0e0 100644 --- a/src/vs/workbench/contrib/notebook/browser/contrib/fold/folding.ts +++ b/src/vs/workbench/contrib/notebook/browser/contrib/fold/folding.ts @@ -134,6 +134,7 @@ export class FoldingController extends Disposable implements INotebookEditorCont } this.setFoldingStateUp(modelIndex, state === CellFoldingState.Collapsed ? CellFoldingState.Expanded : CellFoldingState.Collapsed, 1); + this._notebookEditor.focusElement(cellViewModel); } return; @@ -225,7 +226,7 @@ registerAction2(class extends Action2 { } const viewIndex = editor.viewModel!.getNearestVisibleCellIndexUpwards(index); - editor.selectElement(editor.viewModel!.viewCells[viewIndex]); + editor.focusElement(editor.viewModel!.viewCells[viewIndex]); } } }); diff --git a/src/vs/workbench/contrib/notebook/browser/contrib/fold/foldingModel.ts b/src/vs/workbench/contrib/notebook/browser/contrib/fold/foldingModel.ts index 777b97b7de6..56fae46698f 100644 --- a/src/vs/workbench/contrib/notebook/browser/contrib/fold/foldingModel.ts +++ b/src/vs/workbench/contrib/notebook/browser/contrib/fold/foldingModel.ts @@ -9,7 +9,7 @@ import { TrackedRangeStickiness } from 'vs/editor/common/model'; import { FoldingRegion, FoldingRegions } from 'vs/editor/contrib/folding/foldingRanges'; import { IFoldingRangeData, sanitizeRanges } from 'vs/editor/contrib/folding/syntaxRangeProvider'; import { CellViewModel, NotebookViewModel } from 'vs/workbench/contrib/notebook/browser/viewModel/notebookViewModel'; -import { CellKind, ICellRange } from 'vs/workbench/contrib/notebook/common/notebookCommon'; +import { CellKind, cellRangesToIndexes, ICellRange } from 'vs/workbench/contrib/notebook/common/notebookCommon'; type RegionFilter = (r: FoldingRegion) => boolean; type RegionFilterWithLevel = (r: FoldingRegion, level: number) => boolean; @@ -52,10 +52,7 @@ export class FoldingModel extends Disposable { return; } - const selectionHandles = this._viewModel.selectionHandles; - const indexes = selectionHandles.map(handle => - this._viewModel!.getCellIndex(this._viewModel!.getCellByHandle(handle)!) - ); + const indexes = cellRangesToIndexes(this._viewModel.getSelections()); let changed = false; diff --git a/src/vs/workbench/contrib/notebook/browser/contrib/outline/notebookOutline.ts b/src/vs/workbench/contrib/notebook/browser/contrib/outline/notebookOutline.ts index f6c2dd20120..83c50bd0dd6 100644 --- a/src/vs/workbench/contrib/notebook/browser/contrib/outline/notebookOutline.ts +++ b/src/vs/workbench/contrib/notebook/browser/contrib/outline/notebookOutline.ts @@ -389,7 +389,8 @@ class NotebookCellOutline implements IOutline { includeCodeCells = this._configurationService.getValue('notebook.breadcrumbs.showCodeCells'); } - const [selected] = viewModel.selectionHandles; + const selectedCellIndex = viewModel.getSelection().start; + const selected = viewModel.getCellByIndex(selectedCellIndex)?.handle; const entries: OutlineEntry[] = []; for (let i = 0; i < viewModel.viewCells.length; i++) { @@ -511,8 +512,7 @@ class NotebookCellOutline implements IOutline { const { viewModel } = this._editor; if (viewModel) { - const [selected] = viewModel.selectionHandles; - const cell = viewModel.getCellByHandle(selected); + const cell = viewModel.getCellByIndex(viewModel.getSelection()?.start); if (cell) { for (let entry of this._entries) { newActive = entry.find(cell, []); diff --git a/src/vs/workbench/contrib/notebook/browser/notebookBrowser.ts b/src/vs/workbench/contrib/notebook/browser/notebookBrowser.ts index c79e4eeb9c5..140369f61cf 100644 --- a/src/vs/workbench/contrib/notebook/browser/notebookBrowser.ts +++ b/src/vs/workbench/contrib/notebook/browser/notebookBrowser.ts @@ -324,6 +324,8 @@ export interface INotebookEditorCreationOptions { export interface IActiveNotebookEditor extends INotebookEditor { viewModel: NotebookViewModel; uri: URI; + // selection is never undefined when the editor is attached to a document. + getSelection(): ICellRange; } export interface INotebookEditor extends IEditor, ICommonNotebookEditor { @@ -358,7 +360,6 @@ export interface INotebookEditor extends IEditor, ICommonNotebookEditor { getDomNode(): HTMLElement; getOverflowContainerDomNode(): HTMLElement; getInnerWebview(): Webview | undefined; - getSelectionHandles(): number[]; getSelectionViewModels(): ICellViewModel[]; /** @@ -375,7 +376,7 @@ export interface INotebookEditor extends IEditor, ICommonNotebookEditor { /** * Select & focus cell */ - selectElement(cell: ICellViewModel): void; + focusElement(cell: ICellViewModel): void; /** * Layout info for the notebook editor @@ -642,6 +643,7 @@ export interface INotebookCellList { focusElement(element: ICellViewModel): void; selectElement(element: ICellViewModel): void; getFocusedElements(): ICellViewModel[]; + getSelectedElements(): ICellViewModel[]; revealElementsInView(range: ICellRange): void; revealElementInView(element: ICellViewModel): void; revealElementInViewAtTop(element: ICellViewModel): void; diff --git a/src/vs/workbench/contrib/notebook/browser/notebookEditorWidget.ts b/src/vs/workbench/contrib/notebook/browser/notebookEditorWidget.ts index 3284d8ed841..5258d2f70a2 100644 --- a/src/vs/workbench/contrib/notebook/browser/notebookEditorWidget.ts +++ b/src/vs/workbench/contrib/notebook/browser/notebookEditorWidget.ts @@ -334,8 +334,12 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditor return this._uuid; } - getSelectionHandles(): number[] { - return this.viewModel?.selectionHandles || []; + getSelections() { + return this.viewModel?.getSelections() ?? []; + } + + getSelection() { + return this.viewModel?.getSelection(); } getSelectionViewModels(): ICellViewModel[] { @@ -343,7 +347,18 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditor return []; } - return this.viewModel.selectionHandles.map(handle => this.viewModel!.getCellByHandle(handle)) as ICellViewModel[]; + const cellsSet = new Set(); + + return this.viewModel.getSelections().map(range => this.viewModel!.viewCells.slice(range.start, range.end)).reduce((a, b) => { + b.forEach(cell => { + if (!cellsSet.has(cell.handle)) { + cellsSet.add(cell.handle); + a.push(cell); + } + }); + + return a; + }, [] as ICellViewModel[]); } hasModel(): this is IActiveNotebookEditor { @@ -725,7 +740,7 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditor const cellOptions = options.cellOptions; const cell = this.viewModel.viewCells.find(cell => cell.uri.toString() === cellOptions.resource.toString()); if (cell) { - this.selectElement(cell); + this.focusElement(cell); await this.revealInCenterIfOutsideViewportAsync(cell); const editor = this._renderedEditors.get(cell)!; if (editor) { @@ -1279,9 +1294,8 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditor //#region Editor Features - selectElement(cell: ICellViewModel) { - this._list.selectElement(cell); - // this.viewModel!.selectionHandles = [cell.handle]; + focusElement(cell: ICellViewModel) { + this._list.focusElement(cell); } revealCellRangeInView(range: ICellRange) { @@ -1516,7 +1530,8 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditor (direction === 'above' ? index : nextIndex) : index; const focused = this._list.getFocusedElements(); - return this.viewModel.createCell(insertIndex, initialText, language, type, undefined, [], true, undefined, focused); + const selections = this._list.getSelectedElements(); + return this.viewModel.createCell(insertIndex, initialText, language, type, undefined, [], true, undefined, focused[0]?.handle ?? null, selections); } async splitNotebookCell(cell: ICellViewModel): Promise { @@ -1884,7 +1899,7 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditor } if (focusItem === 'editor') { - this.selectElement(cell); + this.focusElement(cell); this._list.focusView(); cell.editState = CellEditState.Editing; @@ -1893,7 +1908,7 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditor this.revealInCenterIfOutsideViewport(cell); } } else if (focusItem === 'output') { - this.selectElement(cell); + this.focusElement(cell); this._list.focusView(); if (!this._webview) { @@ -1915,7 +1930,7 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditor cell.editState = CellEditState.Preview; cell.focusMode = CellFocusMode.Container; - this.selectElement(cell); + this.focusElement(cell); if (!options?.skipReveal) { this.revealInCenterIfOutsideViewport(cell); } diff --git a/src/vs/workbench/contrib/notebook/browser/notebookServiceImpl.ts b/src/vs/workbench/contrib/notebook/browser/notebookServiceImpl.ts index 9c40d441000..a8993be0823 100644 --- a/src/vs/workbench/contrib/notebook/browser/notebookServiceImpl.ts +++ b/src/vs/workbench/contrib/notebook/browser/notebookServiceImpl.ts @@ -31,7 +31,7 @@ import { NotebookKernelProviderAssociationRegistry, NotebookViewTypesExtensionRe import { CellViewModel } from 'vs/workbench/contrib/notebook/browser/viewModel/notebookViewModel'; import { NotebookCellTextModel } from 'vs/workbench/contrib/notebook/common/model/notebookCellTextModel'; import { NotebookTextModel } from 'vs/workbench/contrib/notebook/common/model/notebookTextModel'; -import { ACCESSIBLE_NOTEBOOK_DISPLAY_ORDER, BUILTIN_RENDERER_ID, CellEditType, CellKind, DisplayOrderKey, ICellEditOperation, INotebookDecorationRenderOptions, INotebookKernel, INotebookKernelProvider, INotebookMarkdownRendererInfo, INotebookRendererInfo, INotebookTextModel, IOrderedMimeType, IOutputDto, mimeTypeIsAlwaysSecure, mimeTypeSupportedByCore, NotebookDataDto, notebookDocumentFilterMatch, NotebookEditorPriority, NOTEBOOK_DISPLAY_ORDER, RENDERER_NOT_AVAILABLE, sortMimeTypes, TransientOptions } from 'vs/workbench/contrib/notebook/common/notebookCommon'; +import { ACCESSIBLE_NOTEBOOK_DISPLAY_ORDER, BUILTIN_RENDERER_ID, CellEditType, CellKind, DisplayOrderKey, ICellEditOperation, INotebookDecorationRenderOptions, INotebookKernel, INotebookKernelProvider, INotebookMarkdownRendererInfo, INotebookRendererInfo, INotebookTextModel, IOrderedMimeType, IOutputDto, mimeTypeIsAlwaysSecure, mimeTypeSupportedByCore, NotebookDataDto, notebookDocumentFilterMatch, NotebookEditorPriority, NOTEBOOK_DISPLAY_ORDER, RENDERER_NOT_AVAILABLE, SelectionStateType, sortMimeTypes, TransientOptions } from 'vs/workbench/contrib/notebook/common/notebookCommon'; import { NotebookMarkdownRendererInfo } from 'vs/workbench/contrib/notebook/common/notebookMarkdownRenderer'; import { NotebookOutputRendererInfo } from 'vs/workbench/contrib/notebook/common/notebookOutputRenderer'; import { NotebookEditorDescriptor, NotebookProviderInfo } from 'vs/workbench/contrib/notebook/common/notebookProvider'; @@ -534,7 +534,10 @@ export class NotebookService extends Disposable implements INotebookService, IEd source: cell.getValue(), language: cell.language, cellKind: cell.cellKind, - outputs: cell.outputs.map(output => ({ ...output, /* paste should generate new outputId */ outputId: UUID.generateUuid() })), + outputs: cell.outputs.map(output => ({ + outputs: output.outputs, + /* paste should generate new outputId */ outputId: UUID.generateUuid() + })), metadata: cloneMetadata(cell) }; }; @@ -600,14 +603,17 @@ export class NotebookService extends Disposable implements INotebookService, IEd clipboardService.writeText(selectedCells.map(cell => cell.getText()).join('\n')); const selectionIndexes = selectedCells.map(cell => [cell, viewModel.getCellIndex(cell)] as [ICellViewModel, number]).sort((a, b) => b[1] - a[1]); const edits: ICellEditOperation[] = selectionIndexes.map(value => ({ editType: CellEditType.Replace, index: value[1], count: 1, cells: [] })); + const firstSelectIndex = selectionIndexes[0][1]; + const newFocusedCellHandle = firstSelectIndex < viewModel.notebookDocument.cells.length + ? viewModel.notebookDocument.cells[firstSelectIndex].handle + : viewModel.notebookDocument.cells[viewModel.notebookDocument.cells.length - 1].handle; - viewModel.notebookDocument.applyEdits(viewModel.notebookDocument.versionId, edits, true, editor.getSelectionHandles(), () => { - const firstSelectIndex = selectionIndexes[0][1]; - if (firstSelectIndex < viewModel.notebookDocument.cells.length) { - return [viewModel.notebookDocument.cells[firstSelectIndex].handle]; - } else { - return [viewModel.notebookDocument.cells[viewModel.notebookDocument.cells.length - 1].handle]; - } + viewModel.notebookDocument.applyEdits(viewModel.notebookDocument.versionId, edits, true, { kind: SelectionStateType.Index, selections: viewModel.getSelections() }, () => { + return { + kind: SelectionStateType.Handle, + primary: newFocusedCellHandle, + selections: [newFocusedCellHandle] + }; }, undefined, true); notebookService.setToCopy(selectedCells.map(cell => cell.model), false); diff --git a/src/vs/workbench/contrib/notebook/browser/view/notebookCellList.ts b/src/vs/workbench/contrib/notebook/browser/view/notebookCellList.ts index 72a6fac7b31..6390212339d 100644 --- a/src/vs/workbench/contrib/notebook/browser/view/notebookCellList.ts +++ b/src/vs/workbench/contrib/notebook/browser/view/notebookCellList.ts @@ -21,7 +21,7 @@ import { IListService, IWorkbenchListOptions, WorkbenchList } from 'vs/platform/ import { IThemeService } from 'vs/platform/theme/common/themeService'; import { CellRevealPosition, CellRevealType, CursorAtBoundary, getVisibleCells, ICellViewModel, INotebookCellList, reduceCellRanges, CellEditState, CellFocusMode, BaseCellRenderTemplate, NOTEBOOK_CELL_LIST_FOCUSED, cellRangesEqual, ICellOutputViewModel } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; import { CellViewModel, NotebookViewModel } from 'vs/workbench/contrib/notebook/browser/viewModel/notebookViewModel'; -import { diff, NOTEBOOK_EDITOR_CURSOR_BOUNDARY, CellKind, ICellRange, NOTEBOOK_EDITOR_CURSOR_BEGIN_END } from 'vs/workbench/contrib/notebook/common/notebookCommon'; +import { diff, NOTEBOOK_EDITOR_CURSOR_BOUNDARY, CellKind, ICellRange, NOTEBOOK_EDITOR_CURSOR_BEGIN_END, cellRangesToIndexes, SelectionStateType } from 'vs/workbench/contrib/notebook/common/notebookCommon'; import { clamp } from 'vs/base/common/numbers'; import { SCROLLABLE_ELEMENT_PADDING_TOP } from 'vs/workbench/contrib/notebook/browser/constants'; @@ -365,12 +365,12 @@ export class NotebookCellList extends WorkbenchList implements ID this._viewModelStore.add(model.onDidChangeSelection(() => { // convert model selections to view selections - const viewSelections = model.selectionHandles.map(handle => { - return model.getCellByHandle(handle); - }).filter(cell => !!cell).map(cell => this._getViewIndexUpperBound(cell!)); + const viewSelections = cellRangesToIndexes(model.getSelections()).map(index => model.getCellByIndex(index)).filter(cell => !!cell).map(cell => this._getViewIndexUpperBound(cell!)); this.setSelection(viewSelections, undefined, true); - if (viewSelections.length) { - this.setFocus([viewSelections[0]]); + const primary = cellRangesToIndexes([model.getSelection()]).map(index => model.getCellByIndex(index)).filter(cell => !!cell).map(cell => this._getViewIndexUpperBound(cell!)); + + if (primary.length) { + this.setFocus(primary, undefined, true); } })); @@ -501,7 +501,7 @@ export class NotebookCellList extends WorkbenchList implements ID } const selectionsLeft = []; - this._viewModel!.selectionHandles.forEach(handle => { + this.getSelectedElements().map(el => el.handle).forEach(handle => { if (this._viewModel!.hasCell(handle)) { selectionsLeft.push(handle); } @@ -509,7 +509,7 @@ export class NotebookCellList extends WorkbenchList implements ID if (!selectionsLeft.length && this._viewModel!.viewCells.length) { // after splice, the selected cells are deleted - this._viewModel!.selectionHandles = [this._viewModel!.viewCells[0].handle]; + this._viewModel!.updateSelectionsFromEdits({ kind: SelectionStateType.Index, selections: [{ start: 0, end: 1 }] }); } } @@ -574,19 +574,14 @@ export class NotebookCellList extends WorkbenchList implements ID const index = this._getViewIndexUpperBound(cell); if (index >= 0) { - this.setFocus([index]); + this.setFocus([index], undefined, false); } } selectElement(cell: ICellViewModel) { - if (this._viewModel) { - this._viewModel.selectionHandles = [cell.handle]; - } - const index = this._getViewIndexUpperBound(cell); if (index >= 0) { this.setSelection([index]); - this.setFocus([index]); } } @@ -599,24 +594,39 @@ export class NotebookCellList extends WorkbenchList implements ID } setFocus(indexes: number[], browserEvent?: UIEvent, ignoreTextModelUpdate?: boolean): void { - // if (!indexes.length) { - // return; - // } + if (ignoreTextModelUpdate) { + super.setFocus(indexes, browserEvent); + return; + } - // if (this._viewModel && !ignoreTextModelUpdate) { - // this._viewModel.selectionHandles = indexes.map(index => this.element(index)).map(cell => cell.handle); - // } + if (!indexes.length) { + if (this._viewModel) { + this._viewModel.updateSelectionsFromView(null, []); + } + } else { + if (this._viewModel) { + const focusedElementHandle = this.element(indexes[0]).handle; + this._viewModel.updateSelectionsFromView(focusedElementHandle, [focusedElementHandle]); + } + } super.setFocus(indexes, browserEvent); } setSelection(indexes: number[], browserEvent?: UIEvent | undefined, ignoreTextModelUpdate?: boolean) { - if (!indexes.length) { + if (ignoreTextModelUpdate) { + super.setSelection(indexes, browserEvent); return; } - if (this._viewModel && !ignoreTextModelUpdate) { - this._viewModel.selectionHandles = indexes.map(index => this.element(index)).map(cell => cell.handle); + if (!indexes.length) { + if (this._viewModel) { + this._viewModel.updateSelectionsFromView(this.getFocusedElements()[0]?.handle ?? null, []); + } + } else { + if (this._viewModel) { + this._viewModel.updateSelectionsFromView(this.getFocusedElements()[0]?.handle ?? null, indexes.map(index => this.element(index)).map(cell => cell.handle)); + } } super.setSelection(indexes, browserEvent); diff --git a/src/vs/workbench/contrib/notebook/browser/view/renderers/cellRenderer.ts b/src/vs/workbench/contrib/notebook/browser/view/renderers/cellRenderer.ts index 6f8836dc5ae..ba92b4ef6b1 100644 --- a/src/vs/workbench/contrib/notebook/browser/view/renderers/cellRenderer.ts +++ b/src/vs/workbench/contrib/notebook/browser/view/renderers/cellRenderer.ts @@ -313,7 +313,7 @@ abstract class AbstractCellRenderer { protected commonRenderTemplate(templateData: BaseCellRenderTemplate): void { templateData.disposables.add(DOM.addDisposableListener(templateData.container, DOM.EventType.FOCUS, () => { if (templateData.currentRenderedCell) { - this.notebookEditor.selectElement(templateData.currentRenderedCell); + this.notebookEditor.focusElement(templateData.currentRenderedCell); } }, true)); diff --git a/src/vs/workbench/contrib/notebook/browser/viewModel/cellEdit.ts b/src/vs/workbench/contrib/notebook/browser/viewModel/cellEdit.ts index 07c9350af1c..8a4f5280eb1 100644 --- a/src/vs/workbench/contrib/notebook/browser/viewModel/cellEdit.ts +++ b/src/vs/workbench/contrib/notebook/browser/viewModel/cellEdit.ts @@ -5,7 +5,7 @@ import { Range } from 'vs/editor/common/core/range'; import { Selection } from 'vs/editor/common/core/selection'; -import { CellKind, IOutputDto, NotebookCellMetadata } from 'vs/workbench/contrib/notebook/common/notebookCommon'; +import { CellKind, IOutputDto, NotebookCellMetadata, SelectionStateType } from 'vs/workbench/contrib/notebook/common/notebookCommon'; import { IResourceUndoRedoElement, UndoRedoElementType } from 'vs/platform/undoRedo/common/undoRedo'; import { URI } from 'vs/base/common/uri'; import { BaseCellViewModel } from 'vs/workbench/contrib/notebook/browser/viewModel/baseCellViewModel'; @@ -52,10 +52,10 @@ export class JoinCellEdit implements IResourceUndoRedoElement { const cell = this.editingDelegate.createCellViewModel(this._deletedRawCell); if (this.direction === 'above') { - this.editingDelegate.insertCell(this.index, this._deletedRawCell, [cell.handle]); + this.editingDelegate.insertCell(this.index, this._deletedRawCell, { kind: SelectionStateType.Handle, primary: cell.handle, selections: [cell.handle] }); cell.focusMode = CellFocusMode.Editor; } else { - this.editingDelegate.insertCell(this.index, cell.model, [this.cell.handle]); + this.editingDelegate.insertCell(this.index, cell.model, { kind: SelectionStateType.Handle, primary: this.cell.handle, selections: [this.cell.handle] }); this.cell.focusMode = CellFocusMode.Editor; } } @@ -70,7 +70,7 @@ export class JoinCellEdit implements IResourceUndoRedoElement { { range: this.inverseRange, text: this.insertContent } ]); - this.editingDelegate.deleteCell(this.index, [this.cell.handle]); + this.editingDelegate.deleteCell(this.index, { kind: SelectionStateType.Handle, primary: this.cell.handle, selections: [this.cell.handle] }); this.cell.focusMode = CellFocusMode.Editor; } } diff --git a/src/vs/workbench/contrib/notebook/browser/viewModel/cellSelectionCollection.ts b/src/vs/workbench/contrib/notebook/browser/viewModel/cellSelectionCollection.ts new file mode 100644 index 00000000000..3ed5b2cf5d3 --- /dev/null +++ b/src/vs/workbench/contrib/notebook/browser/viewModel/cellSelectionCollection.ts @@ -0,0 +1,85 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { Emitter, Event } from 'vs/base/common/event'; +import { Disposable } from 'vs/base/common/lifecycle'; +import { ICellRange } from 'vs/workbench/contrib/notebook/common/notebookCommon'; + +function rangesEqual(a: ICellRange[], b: ICellRange[]) { + if (a.length !== b.length) { + return false; + } + + for (let i = 0; i < a.length; i++) { + if (a[i].start !== b[i].start || a[i].end !== b[i].end) { + return false; + } + } + + return true; +} + +// Handle first, then we migrate to ICellRange competely +// Challenge is List View talks about `element`, which needs extra work to convert to ICellRange as we support Folding and Cell Move +export class NotebookCellSelectionCollection extends Disposable { + private readonly _onDidChangeSelection = this._register(new Emitter()); + get onDidChangeSelection(): Event { return this._onDidChangeSelection.event; } + constructor() { + super(); + } + + private _primary: ICellRange | null = null; + + private _selections: ICellRange[] = []; + + get selections(): ICellRange[] { + return this._selections; + } + + get selection(): ICellRange { + return this._selections[0]; + } + + setState(primary: ICellRange | null, selections: ICellRange[], forceEventEmit: boolean) { + if (primary !== null) { + const primaryRange = primary; + // TODO@rebornix deal with overlap + const newSelections = [primaryRange, ...selections.filter(selection => !(selection.start === primaryRange.start && selection.end === primaryRange.end)).sort((a, b) => a.start - b.start)]; + + const changed = primary !== this._primary || !rangesEqual(this._selections, newSelections); + this._primary = primary; + this._selections = newSelections; + + if (!this._selections.length) { + this._selections.push({ start: 0, end: 0 }); + } + + if (changed || forceEventEmit) { + this._onDidChangeSelection.fire(); + } + } else { + const changed = primary !== this._primary || !rangesEqual(this._selections, selections); + + this._primary = primary; + this._selections = selections; + + if (!this._selections.length) { + this._selections.push({ start: 0, end: 0 }); + } + + if (changed || forceEventEmit) { + this._onDidChangeSelection.fire(); + } + } + } + + setFocus(selection: ICellRange | null, forceEventEmit: boolean) { + this.setState(selection, this._selections, forceEventEmit); + } + + setSelections(selections: ICellRange[], forceEventEmit: boolean) { + this.setState(this._primary, selections, forceEventEmit); + } +} diff --git a/src/vs/workbench/contrib/notebook/browser/viewModel/notebookViewModel.ts b/src/vs/workbench/contrib/notebook/browser/viewModel/notebookViewModel.ts index dc191b89763..750ce6f987a 100644 --- a/src/vs/workbench/contrib/notebook/browser/viewModel/notebookViewModel.ts +++ b/src/vs/workbench/contrib/notebook/browser/viewModel/notebookViewModel.ts @@ -23,7 +23,7 @@ import { NotebookEventDispatcher, NotebookMetadataChangedEvent } from 'vs/workbe import { CellFoldingState, EditorFoldingStateDelegate } from 'vs/workbench/contrib/notebook/browser/contrib/fold/foldingModel'; import { MarkdownCellViewModel } from 'vs/workbench/contrib/notebook/browser/viewModel/markdownCellViewModel'; import { NotebookCellTextModel } from 'vs/workbench/contrib/notebook/common/model/notebookCellTextModel'; -import { CellKind, NotebookCellMetadata, INotebookSearchOptions, ICellRange, NotebookCellsChangeType, ICell, NotebookCellTextModelSplice, CellEditType, IOutputDto } from 'vs/workbench/contrib/notebook/common/notebookCommon'; +import { CellKind, NotebookCellMetadata, INotebookSearchOptions, ICellRange, NotebookCellsChangeType, ICell, NotebookCellTextModelSplice, CellEditType, IOutputDto, SelectionStateType, ISelectionState, cellIndexesToRanges, cellRangesToIndexes } from 'vs/workbench/contrib/notebook/common/notebookCommon'; import { FoldingRegions } from 'vs/editor/contrib/folding/foldingRanges'; import { NotebookTextModel } from 'vs/workbench/contrib/notebook/common/model/notebookTextModel'; import { MarkdownRenderer } from 'vs/editor/browser/core/markdownRenderer'; @@ -31,6 +31,7 @@ import { dirname } from 'vs/base/common/resources'; import { IPosition, Position } from 'vs/editor/common/core/position'; import { MultiModelEditStackElement, SingleModelEditStackElement } from 'vs/editor/common/model/editStack'; import { ResourceNotebookCellEdit } from 'vs/workbench/contrib/bulkEdit/browser/bulkCellEdits'; +import { NotebookCellSelectionCollection } from 'vs/workbench/contrib/notebook/browser/viewModel/cellSelectionCollection'; export interface INotebookEditorViewState { editingCells: { [key: number]: boolean }; @@ -130,20 +131,6 @@ function _normalizeOptions(options: IModelDecorationOptions): ModelDecorationOpt return ModelDecorationOptions.createDynamic(options); } -function selectionsEqual(a: number[], b: number[]) { - if (a.length !== b.length) { - return false; - } - - for (let i = 0; i < a.length; i++) { - if (a[i] !== b[i]) { - return false; - } - } - - return true; -} - let MODEL_ID = 0; @@ -199,20 +186,23 @@ export class NotebookViewModel extends Disposable implements EditorFoldingStateD private readonly _onDidChangeSelection = this._register(new Emitter()); get onDidChangeSelection(): Event { return this._onDidChangeSelection.event; } - private _selections: number[] = []; + private _selectionCollection = new NotebookCellSelectionCollection(); - get selectionHandles() { - return this._selections; + private get selectionHandles() { + const handlesSet = new Set(); + const handles: number[] = []; + cellRangesToIndexes(this._selectionCollection.selections).map(index => this.getCellByIndex(index)).forEach(cell => { + if (cell && !handlesSet.has(cell.handle)) { + handles.push(cell.handle); + } + }); + + return handles; } - set selectionHandles(selections: number[]) { - selections = selections.sort(); - if (selectionsEqual(selections, this.selectionHandles)) { - return; - } - - this._selections = selections; - this._onDidChangeSelection.fire(); + private set selectionHandles(selectionHandles: number[]) { + const indexes = selectionHandles.map(handle => this._viewCells.findIndex(cell => cell.handle === handle)); + this._selectionCollection.setSelections(cellIndexesToRanges(indexes), true); } private _decorationsTree = new DecorationsTree(); @@ -299,6 +289,7 @@ export class NotebookViewModel extends Disposable implements EditorFoldingStateD } } + // TODO@rebornix this.selectionHandles = endSelectionHandles; }; @@ -327,8 +318,8 @@ export class NotebookViewModel extends Disposable implements EditorFoldingStateD } }); - if (contentChanges.endSelections) { - this.updateSelectionsFromEdits(contentChanges.endSelections); + if (contentChanges.endSelectionState) { + this.updateSelectionsFromEdits(contentChanges.endSelectionState); } })); @@ -348,6 +339,10 @@ export class NotebookViewModel extends Disposable implements EditorFoldingStateD }); })); + this._register(this._selectionCollection.onDidChangeSelection(e => { + this._onDidChangeSelection.fire(e); + })); + this._viewCells = this._notebook.cells.map(cell => { return createCellViewModel(this._instantiationService, this, cell); }); @@ -357,13 +352,33 @@ export class NotebookViewModel extends Disposable implements EditorFoldingStateD }); } + getSelection() { + return this._selectionCollection.selection; + } + + getSelections() { + return this._selectionCollection.selections; + } + setFocus(focused: boolean) { this._focused = focused; } - updateSelectionsFromEdits(selections: number[]) { + updateSelectionsFromView(primary: number | null, selections: number[]) { + const primaryIndex = primary !== null ? this.getCellIndexByHandle(primary) : null; + const selectionIndexes = selections.map(sel => this.getCellIndexByHandle(sel)); + this._selectionCollection.setState(primaryIndex !== null ? { start: primaryIndex, end: primaryIndex + 1 } : null, cellIndexesToRanges(selectionIndexes), false); + } + + updateSelectionsFromEdits(state: ISelectionState) { if (this._focused) { - this.selectionHandles = selections; + if (state.kind === SelectionStateType.Handle) { + const primaryIndex = state.primary !== null ? this.getCellIndexByHandle(state.primary) : null; + const selectionIndexes = state.selections.map(sel => this.getCellIndexByHandle(sel)); + this._selectionCollection.setState(primaryIndex !== null ? { start: primaryIndex, end: primaryIndex + 1 } : null, cellIndexesToRanges(selectionIndexes), true); + } else { + this._selectionCollection.setState(state.selections[0] ?? null, state.selections, true); + } } } @@ -454,10 +469,18 @@ export class NotebookViewModel extends Disposable implements EditorFoldingStateD return this._handleToViewCellMapping.get(handle); } + getCellIndexByHandle(handle: number): number { + return this._viewCells.findIndex(cell => cell.handle === handle); + } + getCellIndex(cell: ICellViewModel) { return this._viewCells.indexOf(cell as CellViewModel); } + getCellByIndex(index: number) { + return this._viewCells[index]; + } + /** * If this._viewCells[index] is visible then return index */ @@ -645,7 +668,7 @@ export class NotebookViewModel extends Disposable implements EditorFoldingStateD return result; } - createCell(index: number, source: string, language: string, type: CellKind, metadata: NotebookCellMetadata | undefined, outputs: IOutputDto[], synchronous: boolean, pushUndoStop: boolean = true, previouslyFocused: ICellViewModel[] = []): CellViewModel { + createCell(index: number, source: string, language: string, type: CellKind, metadata: NotebookCellMetadata | undefined, outputs: IOutputDto[], synchronous: boolean, pushUndoStop: boolean = true, previouslyPrimary: number | null = null, previouslyFocused: ICellViewModel[] = []): CellViewModel { const beforeSelections = previouslyFocused.map(e => e.handle); this._notebook.applyEdits(this._notebook.versionId, [ { @@ -662,29 +685,24 @@ export class NotebookViewModel extends Disposable implements EditorFoldingStateD } ] } - ], synchronous, beforeSelections, () => undefined, undefined); + ], synchronous, { kind: SelectionStateType.Handle, primary: previouslyPrimary, selections: beforeSelections }, () => undefined, undefined); return this._viewCells[index]; } deleteCell(index: number, synchronous: boolean, pushUndoStop: boolean = true) { - const primarySelectionIndex = this.selectionHandles.length ? this._viewCells.indexOf(this.getCellByHandle(this.selectionHandles[0])!) : null; - let endSelections: number[] = []; - if (this.selectionHandles.length) { - const primarySelectionHandle = this.selectionHandles[0]; + const primarySelectionIndex = this.getSelection()?.start ?? null; + let endPrimarySelection: number | null = null; - if (index === primarySelectionIndex) { - if (primarySelectionIndex < this.length - 1) { - endSelections = [this._viewCells[primarySelectionIndex + 1].handle]; - } else if (primarySelectionIndex === this.length - 1 && this.length > 1) { - endSelections = [this._viewCells[primarySelectionIndex - 1].handle]; - } else { - endSelections = []; - } - } else { - endSelections = [primarySelectionHandle]; + if (index === primarySelectionIndex) { + if (primarySelectionIndex < this.length - 1) { + endPrimarySelection = this._viewCells[primarySelectionIndex + 1].handle; + } else if (primarySelectionIndex === this.length - 1 && this.length > 1) { + endPrimarySelection = this._viewCells[primarySelectionIndex - 1].handle; } } + let endSelections: number[] = this.selectionHandles.filter(handle => handle !== endPrimarySelection); + this._notebook.applyEdits(this._notebook.versionId, [ { editType: CellEditType.Replace, @@ -693,8 +711,8 @@ export class NotebookViewModel extends Disposable implements EditorFoldingStateD cells: [] }], synchronous, - this.selectionHandles, - () => endSelections, + { kind: SelectionStateType.Index, selections: this.getSelections() }, + () => ({ kind: SelectionStateType.Handle, primary: endPrimarySelection, selections: endSelections }), undefined, pushUndoStop ); @@ -721,7 +739,7 @@ export class NotebookViewModel extends Disposable implements EditorFoldingStateD length, newIdx } - ], synchronous, undefined, () => [viewCell.handle], undefined); + ], synchronous, { kind: SelectionStateType.Index, selections: this.getSelections() }, () => ({ kind: SelectionStateType.Index, selections: [{ start: newIdx, end: newIdx + 1 }] }), undefined); return true; } diff --git a/src/vs/workbench/contrib/notebook/common/model/cellEdit.ts b/src/vs/workbench/contrib/notebook/common/model/cellEdit.ts index 61453ae9a74..52d2a01a081 100644 --- a/src/vs/workbench/contrib/notebook/common/model/cellEdit.ts +++ b/src/vs/workbench/contrib/notebook/common/model/cellEdit.ts @@ -6,15 +6,15 @@ import { IResourceUndoRedoElement, UndoRedoElementType } from 'vs/platform/undoRedo/common/undoRedo'; import { URI } from 'vs/base/common/uri'; import { NotebookCellTextModel } from 'vs/workbench/contrib/notebook/common/model/notebookCellTextModel'; -import { NotebookCellMetadata } from 'vs/workbench/contrib/notebook/common/notebookCommon'; +import { ISelectionState, NotebookCellMetadata } from 'vs/workbench/contrib/notebook/common/notebookCommon'; /** * It should not modify Undo/Redo stack */ export interface ITextCellEditingDelegate { - insertCell?(index: number, cell: NotebookCellTextModel, endSelections?: number[]): void; - deleteCell?(index: number, endSelections?: number[]): void; - moveCell?(fromIndex: number, length: number, toIndex: number, beforeSelections: number[] | undefined, endSelections: number[] | undefined): void; + insertCell?(index: number, cell: NotebookCellTextModel, endSelections?: ISelectionState): void; + deleteCell?(index: number, endSelections?: ISelectionState): void; + moveCell?(fromIndex: number, length: number, toIndex: number, beforeSelections: ISelectionState | undefined, endSelections?: ISelectionState): void; updateCellMetadata?(index: number, newMetadata: NotebookCellMetadata): void; } @@ -28,8 +28,8 @@ export class MoveCellEdit implements IResourceUndoRedoElement { private length: number, private toIndex: number, private editingDelegate: ITextCellEditingDelegate, - private beforedSelections: number[] | undefined, - private endSelections: number[] | undefined + private beforedSelections: ISelectionState | undefined, + private endSelections: ISelectionState | undefined ) { } @@ -57,8 +57,8 @@ export class SpliceCellsEdit implements IResourceUndoRedoElement { public resource: URI, private diffs: [number, NotebookCellTextModel[], NotebookCellTextModel[]][], private editingDelegate: ITextCellEditingDelegate, - private beforeHandles: number[] | undefined, - private endHandles: number[] | undefined + private beforeHandles: ISelectionState | undefined, + private endHandles: ISelectionState | undefined ) { } diff --git a/src/vs/workbench/contrib/notebook/common/model/notebookTextModel.ts b/src/vs/workbench/contrib/notebook/common/model/notebookTextModel.ts index eb7907739e4..6422841aab1 100644 --- a/src/vs/workbench/contrib/notebook/common/model/notebookTextModel.ts +++ b/src/vs/workbench/contrib/notebook/common/model/notebookTextModel.ts @@ -7,7 +7,7 @@ import { Emitter, Event } from 'vs/base/common/event'; import { Disposable, dispose, IDisposable } from 'vs/base/common/lifecycle'; import { URI } from 'vs/base/common/uri'; import { NotebookCellTextModel } from 'vs/workbench/contrib/notebook/common/model/notebookCellTextModel'; -import { INotebookTextModel, NotebookCellOutputsSplice, NotebookDocumentMetadata, NotebookCellMetadata, ICellEditOperation, CellEditType, CellUri, notebookDocumentMetadataDefaults, diff, NotebookCellsChangeType, ICellDto2, TransientOptions, NotebookTextModelChangedEvent, NotebookRawContentEvent, IOutputDto, ICellOutput, IOutputItemDto } from 'vs/workbench/contrib/notebook/common/notebookCommon'; +import { INotebookTextModel, NotebookCellOutputsSplice, NotebookDocumentMetadata, NotebookCellMetadata, ICellEditOperation, CellEditType, CellUri, notebookDocumentMetadataDefaults, diff, NotebookCellsChangeType, ICellDto2, TransientOptions, NotebookTextModelChangedEvent, NotebookRawContentEvent, IOutputDto, ICellOutput, IOutputItemDto, ISelectionState } from 'vs/workbench/contrib/notebook/common/notebookCommon'; import { ITextSnapshot } from 'vs/editor/common/model'; import { IUndoRedoService, UndoRedoElementType, IUndoRedoElement, IResourceUndoRedoElement, UndoRedoGroup, IWorkspaceUndoRedoElement } from 'vs/platform/undoRedo/common/undoRedo'; import { MoveCellEdit, SpliceCellsEdit, CellMetadataEdit } from 'vs/workbench/contrib/notebook/common/model/cellEdit'; @@ -59,10 +59,10 @@ class StackOperation implements IWorkspaceUndoRedoElement { type: UndoRedoElementType.Workspace; private _operations: IUndoRedoElement[] = []; - private _beginSelectionState: number[] | undefined = undefined; - private _resultSelectionState: number[] | undefined = undefined; + private _beginSelectionState: ISelectionState | undefined = undefined; + private _resultSelectionState: ISelectionState | undefined = undefined; - constructor(readonly resource: URI, readonly label: string, readonly undoRedoGroup: UndoRedoGroup | undefined, private _delayedEmitter: DelayedEmitter, selectionState: number[] | undefined) { + constructor(readonly resource: URI, readonly label: string, readonly undoRedoGroup: UndoRedoGroup | undefined, private _delayedEmitter: DelayedEmitter, selectionState: ISelectionState | undefined) { this.type = UndoRedoElementType.Workspace; this._beginSelectionState = selectionState; } @@ -74,13 +74,13 @@ class StackOperation implements IWorkspaceUndoRedoElement { return this._operations.length === 0; } - pushEndSelectionState(selectionState: number[] | undefined) { + pushEndSelectionState(selectionState: ISelectionState | undefined) { this._resultSelectionState = selectionState; } - pushEditOperation(element: IUndoRedoElement, beginSelectionState: number[] | undefined, resultSelectionState: number[] | undefined) { + pushEditOperation(element: IUndoRedoElement, beginSelectionState: ISelectionState | undefined, resultSelectionState: ISelectionState | undefined) { if (this._operations.length === 0) { - this._beginSelectionState = this._beginSelectionState || beginSelectionState; + this._beginSelectionState = this._beginSelectionState ?? beginSelectionState; } this._operations.push(element); this._resultSelectionState = resultSelectionState; @@ -109,7 +109,7 @@ export class NotebookOperationManager { } - pushStackElement(label: string, selectionState: number[] | undefined, undoRedoGroup: UndoRedoGroup | undefined) { + pushStackElement(label: string, selectionState: ISelectionState | undefined, undoRedoGroup: UndoRedoGroup | undefined) { if (this._pendingStackOperation) { this._pendingStackOperation.pushEndSelectionState(selectionState); if (!this._pendingStackOperation.isEmpty) { @@ -122,7 +122,7 @@ export class NotebookOperationManager { this._pendingStackOperation = new StackOperation(this._resource, label, undoRedoGroup, this._delayedEmitter, selectionState); } - pushEditOperation(element: IUndoRedoElement, beginSelectionState: number[] | undefined, resultSelectionState: number[] | undefined) { + pushEditOperation(element: IUndoRedoElement, beginSelectionState: ISelectionState | undefined, resultSelectionState: ISelectionState | undefined) { if (this._pendingStackOperation) { this._pendingStackOperation.pushEditOperation(element, beginSelectionState, resultSelectionState); return; @@ -148,7 +148,7 @@ class DelayedEmitter { this._deferredCnt++; } - endDeferredEmit(endSelections: number[] | undefined): void { + endDeferredEmit(endSelections: ISelectionState | undefined): void { this._deferredCnt--; if (this._deferredCnt === 0) { this._computeEndState(); @@ -158,7 +158,7 @@ class DelayedEmitter { { rawEvents: this._notebookTextModelChangedEvent.rawEvents, versionId: this._textModel.versionId, - endSelections: endSelections || this._notebookTextModelChangedEvent.endSelections, + endSelectionState: endSelections, synchronous: this._notebookTextModelChangedEvent.synchronous } ); @@ -169,7 +169,7 @@ class DelayedEmitter { } - emit(data: NotebookRawContentEvent, synchronous: boolean, endSelections?: number[]) { + emit(data: NotebookRawContentEvent, synchronous: boolean, endSelections?: ISelectionState) { if (this._deferredCnt === 0) { this._computeEndState(); @@ -178,7 +178,7 @@ class DelayedEmitter { rawEvents: [data], versionId: this._textModel.versionId, synchronous, - endSelections + endSelectionState: endSelections } ); } else { @@ -186,7 +186,7 @@ class DelayedEmitter { this._notebookTextModelChangedEvent = { rawEvents: [data], versionId: this._textModel.versionId, - endSelections: endSelections, + endSelectionState: endSelections, synchronous: synchronous }; } else { @@ -194,7 +194,7 @@ class DelayedEmitter { this._notebookTextModelChangedEvent = { rawEvents: [...this._notebookTextModelChangedEvent.rawEvents, data], versionId: this._textModel.versionId, - endSelections: endSelections ? endSelections : this._notebookTextModelChangedEvent.endSelections, + endSelectionState: endSelections !== undefined ? endSelections : this._notebookTextModelChangedEvent.endSelectionState, synchronous: synchronous }; } @@ -279,11 +279,11 @@ export class NotebookTextModel extends Disposable implements INotebookTextModel super.dispose(); } - pushStackElement(label: string, selectionState: number[] | undefined, undoRedoGroup: UndoRedoGroup | undefined) { + pushStackElement(label: string, selectionState: ISelectionState | undefined, undoRedoGroup: UndoRedoGroup | undefined) { this._operationManager.pushStackElement(label, selectionState, undoRedoGroup); } - applyEdits(modelVersionId: number, rawEdits: ICellEditOperation[], synchronous: boolean, beginSelectionState: number[] | undefined, endSelectionsComputer: () => number[] | undefined, undoRedoGroup: UndoRedoGroup | undefined, computeUndoRedo: boolean = true): boolean { + applyEdits(modelVersionId: number, rawEdits: ICellEditOperation[], synchronous: boolean, beginSelectionState: ISelectionState | undefined, endSelectionsComputer: () => ISelectionState | undefined, undoRedoGroup: UndoRedoGroup | undefined, computeUndoRedo: boolean = true): boolean { if (modelVersionId !== this._versionId) { return false; } @@ -415,8 +415,8 @@ export class NotebookTextModel extends Disposable implements INotebookTextModel if (computeUndoRedo) { this._operationManager.pushEditOperation(new SpliceCellsEdit(this.uri, undoDiff, { - insertCell: (index, cell, endSelections?: number[]) => { this._insertNewCell(index, [cell], true, endSelections); }, - deleteCell: (index, endSelections?: number[]) => { this._removeCell(index, 1, true, endSelections); }, + insertCell: (index, cell, endSelections) => { this._insertNewCell(index, [cell], true, endSelections); }, + deleteCell: (index, endSelections) => { this._removeCell(index, 1, true, endSelections); }, }, undefined, undefined), undefined, undefined); } @@ -473,7 +473,7 @@ export class NotebookTextModel extends Disposable implements INotebookTextModel this._eventEmitter.emit({ kind: NotebookCellsChangeType.ChangeDocumentMetadata, metadata: this.metadata, transient: this._isDocumentMetadataChangeTransient(oldMetadata, metadata) }, true); } - private _insertNewCell(index: number, cells: NotebookCellTextModel[], synchronous: boolean, endSelections?: number[]): void { + private _insertNewCell(index: number, cells: NotebookCellTextModel[], synchronous: boolean, endSelections: ISelectionState | undefined): void { for (let i = 0; i < cells.length; i++) { this._mapping.set(cells[i].handle, cells[i]); const dirtyStateListener = cells[i].onDidChangeContent(() => { @@ -498,7 +498,7 @@ export class NotebookTextModel extends Disposable implements INotebookTextModel return; } - private _removeCell(index: number, count: number, synchronous: boolean, endSelections?: number[]) { + private _removeCell(index: number, count: number, synchronous: boolean, endSelections: ISelectionState | undefined) { for (let i = index; i < index + count; i++) { const cell = this._cells[i]; this._cellListeners.get(cell.handle)?.dispose(); @@ -617,7 +617,7 @@ export class NotebookTextModel extends Disposable implements INotebookTextModel }(), undefined, undefined); } - this._eventEmitter.emit({ kind: NotebookCellsChangeType.ChangeLanguage, index: this._cells.indexOf(cell), language: languageId, transient: false }, true); + this._eventEmitter.emit({ kind: NotebookCellsChangeType.ChangeLanguage, index: this._cells.indexOf(cell), language: languageId, transient: false }, true, undefined); } private _spliceNotebookCellOutputs2(cellHandle: number, outputs: ICellOutput[], computeUndoRedo: boolean): void { @@ -691,13 +691,13 @@ export class NotebookTextModel extends Disposable implements INotebookTextModel outputItems: items, append: false, transient: this.transientOptions.transientOutputs - }, true); + }, true, undefined); } - private _moveCellToIdx(index: number, length: number, newIdx: number, synchronous: boolean, pushedToUndoStack: boolean, beforeSelections: number[] | undefined, endSelections: number[] | undefined): boolean { + private _moveCellToIdx(index: number, length: number, newIdx: number, synchronous: boolean, pushedToUndoStack: boolean, beforeSelections: ISelectionState | undefined, endSelections: ISelectionState | undefined): boolean { if (pushedToUndoStack) { this._operationManager.pushEditOperation(new MoveCellEdit(this.uri, index, length, newIdx, { - moveCell: (fromIndex: number, length: number, toIndex: number, beforeSelections: number[] | undefined, endSelections: number[] | undefined) => { + moveCell: (fromIndex: number, length: number, toIndex: number, beforeSelections: ISelectionState | undefined, endSelections: ISelectionState | undefined) => { this._moveCellToIdx(fromIndex, length, toIndex, true, false, beforeSelections, endSelections); }, }, beforeSelections, endSelections), beforeSelections, endSelections); diff --git a/src/vs/workbench/contrib/notebook/common/notebookCommon.ts b/src/vs/workbench/contrib/notebook/common/notebookCommon.ts index bd8a78be86d..934568101c5 100644 --- a/src/vs/workbench/contrib/notebook/common/notebookCommon.ts +++ b/src/vs/workbench/contrib/notebook/common/notebookCommon.ts @@ -298,11 +298,34 @@ export type NotebookCellsChangedEventDto = { }; export type NotebookRawContentEvent = (NotebookCellsInitializeEvent | NotebookDocumentChangeMetadataEvent | NotebookCellContentChangeEvent | NotebookCellsModelChangedEvent | NotebookCellsModelMoveEvent | NotebookOutputChangedEvent | NotebookOutputItemChangedEvent | NotebookCellsChangeLanguageEvent | NotebookCellsChangeMetadataEvent | NotebookDocumentUnknownChangeEvent) & { transient: boolean; }; + +export enum SelectionStateType { + Handle = 0, + Index = 1 +} + +export interface ISelectionHandleState { + kind: SelectionStateType.Handle; + primary: number | null; + selections: number[]; +} + +export interface ISelectionIndexState { + kind: SelectionStateType.Index; + + /** + * [primarySelection, ...secondarySelections] + */ + selections: ICellRange[]; +} + +export type ISelectionState = ISelectionHandleState | ISelectionIndexState; + export type NotebookTextModelChangedEvent = { readonly rawEvents: NotebookRawContentEvent[]; readonly versionId: number; readonly synchronous: boolean; - readonly endSelections?: number[]; + readonly endSelectionState: ISelectionState | undefined; }; export const enum CellEditType { @@ -657,7 +680,8 @@ export interface IEditor extends editorCommon.ICompositeCodeEditor { readonly onDidFocusEditorWidget: Event; readonly onDidChangeVisibleRanges: Event; readonly onDidChangeSelection: Event; - getSelectionHandles(): number[]; + getSelection(): ICellRange | undefined; + getSelections(): ICellRange[]; isNotebookEditor: boolean; visibleRanges: ICellRange[]; uri?: URI; @@ -800,3 +824,33 @@ export interface INotebookDecorationRenderOptions { borderColor?: string | ThemeColor; top?: editorCommon.IContentDecorationRenderOptions; } + + +export function cellIndexesToRanges(indexes: number[]) { + const first = indexes.shift(); + + if (first === undefined) { + return []; + } + + return indexes.reduce(function (ranges, num) { + if (num <= ranges[0][1]) { + ranges[0][1] = num + 1; + } else { + ranges.unshift([num, num + 1]); + } + return ranges; + }, [[first, first + 1]]).reverse().map(val => ({ start: val[0], end: val[1] })); +} + +export function cellRangesToIndexes(ranges: ICellRange[]) { + const indexes = ranges.reduce((a, b) => { + for (let i = b.start; i < b.end; i++) { + a.push(i); + } + + return a; + }, [] as number[]); + + return indexes; +} diff --git a/src/vs/workbench/contrib/notebook/test/notebookCommon.test.ts b/src/vs/workbench/contrib/notebook/test/notebookCommon.test.ts index 38a91a236ce..cbd91d00ed4 100644 --- a/src/vs/workbench/contrib/notebook/test/notebookCommon.test.ts +++ b/src/vs/workbench/contrib/notebook/test/notebookCommon.test.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import * as assert from 'assert'; -import { NOTEBOOK_DISPLAY_ORDER, sortMimeTypes, CellKind, diff, CellUri } from 'vs/workbench/contrib/notebook/common/notebookCommon'; +import { NOTEBOOK_DISPLAY_ORDER, sortMimeTypes, CellKind, diff, CellUri, cellRangesToIndexes, cellIndexesToRanges } from 'vs/workbench/contrib/notebook/common/notebookCommon'; import { TestCell, setupInstantiationService } from 'vs/workbench/contrib/notebook/test/testNotebookEditor'; import { URI } from 'vs/base/common/uri'; import { ITextModelService } from 'vs/editor/common/services/resolverService'; @@ -355,3 +355,24 @@ suite('CellUri', function () { assert.equal(actual?.notebook.toString(), nb.toString()); }); }); + + +suite('CellRange', function () { + + test('Cell range to index', function () { + assert.deepStrictEqual(cellRangesToIndexes([]), []); + assert.deepStrictEqual(cellRangesToIndexes([{ start: 0, end: 0 }]), []); + assert.deepStrictEqual(cellRangesToIndexes([{ start: 0, end: 1 }]), [0]); + assert.deepStrictEqual(cellRangesToIndexes([{ start: 0, end: 2 }]), [0, 1]); + assert.deepStrictEqual(cellRangesToIndexes([{ start: 0, end: 2 }, { start: 2, end: 3 }]), [0, 1, 2]); + assert.deepStrictEqual(cellRangesToIndexes([{ start: 0, end: 2 }, { start: 3, end: 4 }]), [0, 1, 3]); + }); + + test('Cell index to range', function () { + assert.deepStrictEqual(cellIndexesToRanges([]), []); + assert.deepStrictEqual(cellIndexesToRanges([0]), [{ start: 0, end: 1 }]); + assert.deepStrictEqual(cellIndexesToRanges([0, 1]), [{ start: 0, end: 2 }]); + assert.deepStrictEqual(cellIndexesToRanges([0, 1, 2]), [{ start: 0, end: 3 }]); + assert.deepStrictEqual(cellIndexesToRanges([0, 1, 3]), [{ start: 0, end: 2 }, { start: 3, end: 4 }]); + }); +}); diff --git a/src/vs/workbench/contrib/notebook/test/notebookTextModel.test.ts b/src/vs/workbench/contrib/notebook/test/notebookTextModel.test.ts index 867e0bcb193..df4d6a86b6c 100644 --- a/src/vs/workbench/contrib/notebook/test/notebookTextModel.test.ts +++ b/src/vs/workbench/contrib/notebook/test/notebookTextModel.test.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import * as assert from 'assert'; -import { CellKind, CellEditType, NotebookTextModelChangedEvent } from 'vs/workbench/contrib/notebook/common/notebookCommon'; +import { CellKind, CellEditType, NotebookTextModelChangedEvent, SelectionStateType } from 'vs/workbench/contrib/notebook/common/notebookCommon'; import { withTestNotebook, TestCell, setupInstantiationService } from 'vs/workbench/contrib/notebook/test/testNotebookEditor'; import { IBulkEditService } from 'vs/editor/browser/services/bulkEditService'; import { IUndoRedoService } from 'vs/platform/undoRedo/common/undoRedo'; @@ -300,7 +300,7 @@ suite('NotebookTextModel', () => { textModel.applyEdits(textModel.versionId, [ { editType: CellEditType.Replace, index: 1, count: 1, cells: [] }, { editType: CellEditType.Replace, index: 1, count: 0, cells: [new TestCell(viewModel.viewType, 5, 'var e = 5;', 'javascript', CellKind.Code, [], textModelService)] }, - ], true, undefined, () => [0], undefined); + ], true, undefined, () => ({ kind: SelectionStateType.Index, selections: [{ start: 0, end: 1 }] }), undefined); assert.equal(textModel.cells.length, 4); assert.equal(textModel.cells[0].getValue(), 'var a = 1;'); @@ -309,7 +309,7 @@ suite('NotebookTextModel', () => { assert.notEqual(changeEvent, undefined); assert.equal(changeEvent!.rawEvents.length, 2); - assert.deepEqual(changeEvent!.endSelections, [0]); + assert.deepEqual(changeEvent!.endSelectionState?.selections, [{ start: 0, end: 1 }]); assert.equal(textModel.versionId, version + 1); eventListener.dispose(); } @@ -341,11 +341,11 @@ suite('NotebookTextModel', () => { editType: CellEditType.Metadata, metadata: { editable: false }, } - ], true, undefined, () => [0], undefined); + ], true, undefined, () => ({ kind: SelectionStateType.Index, selections: [{ start: 0, end: 1 }] }), undefined); assert.notEqual(changeEvent, undefined); assert.equal(changeEvent!.rawEvents.length, 2); - assert.deepEqual(changeEvent!.endSelections, [0]); + assert.deepEqual(changeEvent!.endSelectionState?.selections, [{ start: 0, end: 1 }]); assert.equal(textModel.versionId, version + 1); eventListener.dispose(); } diff --git a/src/vs/workbench/contrib/notebook/test/notebookViewModel.test.ts b/src/vs/workbench/contrib/notebook/test/notebookViewModel.test.ts index aa6025f2bc7..b8af391edaf 100644 --- a/src/vs/workbench/contrib/notebook/test/notebookViewModel.test.ts +++ b/src/vs/workbench/contrib/notebook/test/notebookViewModel.test.ts @@ -43,7 +43,7 @@ suite('NotebookViewModel', () => { assert.equal(viewModel.viewCells[0].metadata?.editable, true); assert.equal(viewModel.viewCells[1].metadata?.editable, false); - const cell = viewModel.createCell(1, 'var c = 3', 'javascript', CellKind.Code, {}, [], true, true, []); + const cell = viewModel.createCell(1, 'var c = 3', 'javascript', CellKind.Code, {}, [], true, true, null, []); assert.equal(viewModel.viewCells.length, 3); assert.equal(viewModel.notebookDocument.cells.length, 3); assert.equal(viewModel.getCellIndex(cell), 1); diff --git a/src/vs/workbench/contrib/notebook/test/testNotebookEditor.ts b/src/vs/workbench/contrib/notebook/test/testNotebookEditor.ts index e3bdeff84d2..0a4a677bad6 100644 --- a/src/vs/workbench/contrib/notebook/test/testNotebookEditor.ts +++ b/src/vs/workbench/contrib/notebook/test/testNotebookEditor.ts @@ -65,6 +65,12 @@ export class TestNotebookEditor implements INotebookEditor { constructor( ) { } + getSelection(): ICellRange | undefined { + throw new Error('Method not implemented.'); + } + getSelections(): ICellRange[] { + throw new Error('Method not implemented.'); + } getSelectionViewModels(): ICellViewModel[] { throw new Error('Method not implemented.'); } @@ -111,11 +117,6 @@ export class TestNotebookEditor implements INotebookEditor { removeEditorDecorations(key: string): void { // throw new Error('Method not implemented.'); } - getSelectionHandles(): number[] { - return []; - } - - setOptions(options: NotebookEditorOptions | undefined): Promise { throw new Error('Method not implemented.'); } @@ -227,7 +228,7 @@ export class TestNotebookEditor implements INotebookEditor { throw new Error('Method not implemented.'); } - selectElement(cell: CellViewModel): void { + focusElement(cell: CellViewModel): void { throw new Error('Method not implemented.'); } diff --git a/src/vs/workbench/test/browser/api/extHostNotebook.test.ts b/src/vs/workbench/test/browser/api/extHostNotebook.test.ts index a85f77ba805..1014b49cd96 100644 --- a/src/vs/workbench/test/browser/api/extHostNotebook.test.ts +++ b/src/vs/workbench/test/browser/api/extHostNotebook.test.ts @@ -85,7 +85,7 @@ suite('NotebookCell#Document', function () { addedEditors: [{ documentUri: notebookUri, id: '_notebook_editor_0', - selections: [0], + selections: [{ start: 0, end: 1 }], visibleRanges: [] }] }); diff --git a/src/vs/workbench/test/browser/api/extHostNotebookConcatDocument.test.ts b/src/vs/workbench/test/browser/api/extHostNotebookConcatDocument.test.ts index 5cd6fcf79cf..49962a0dadb 100644 --- a/src/vs/workbench/test/browser/api/extHostNotebookConcatDocument.test.ts +++ b/src/vs/workbench/test/browser/api/extHostNotebookConcatDocument.test.ts @@ -75,7 +75,7 @@ suite('NotebookConcatDocument', function () { { documentUri: notebookUri, id: '_notebook_editor_0', - selections: [0], + selections: [{ start: 0, end: 1 }], visibleRanges: [] } ]