Merge pull request #117204 from microsoft/rebornix/nb-selections

Notebook selections/CellRange
This commit is contained in:
Peng Lyu 2021-02-22 16:25:10 -07:00 committed by GitHub
commit 08eac1a22d
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
27 changed files with 403 additions and 192 deletions

View file

@ -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).

View file

@ -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 {

View file

@ -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[];
}

View file

@ -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();

View file

@ -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);
}

View file

@ -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;
}

View file

@ -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);
}

View file

@ -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]);
}
}
});

View file

@ -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;

View file

@ -389,7 +389,8 @@ class NotebookCellOutline implements IOutline<OutlineEntry> {
includeCodeCells = this._configurationService.getValue<boolean>('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<OutlineEntry> {
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, []);

View file

@ -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;

View file

@ -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<number>();
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<CellViewModel[] | null> {
@ -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);
}

View file

@ -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);

View file

@ -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<CellViewModel> 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<CellViewModel> 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<CellViewModel> 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<CellViewModel> 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<CellViewModel> 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);

View file

@ -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));

View file

@ -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;
}
}

View file

@ -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<void>());
get onDidChangeSelection(): Event<void> { 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);
}
}

View file

@ -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<void>());
get onDidChangeSelection(): Event<void> { return this._onDidChangeSelection.event; }
private _selections: number[] = [];
private _selectionCollection = new NotebookCellSelectionCollection();
get selectionHandles() {
return this._selections;
private get selectionHandles() {
const handlesSet = new Set<number>();
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;
}

View file

@ -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
) {
}

View file

@ -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);

View file

@ -298,11 +298,34 @@ export type NotebookCellsChangedEventDto = {
};
export type NotebookRawContentEvent = (NotebookCellsInitializeEvent<ICell> | NotebookDocumentChangeMetadataEvent | NotebookCellContentChangeEvent | NotebookCellsModelChangedEvent<ICell> | NotebookCellsModelMoveEvent<ICell> | 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<void>;
readonly onDidChangeVisibleRanges: Event<void>;
readonly onDidChangeSelection: Event<void>;
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;
}

View file

@ -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 }]);
});
});

View file

@ -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();
}

View file

@ -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);

View file

@ -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<void> {
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.');
}

View file

@ -85,7 +85,7 @@ suite('NotebookCell#Document', function () {
addedEditors: [{
documentUri: notebookUri,
id: '_notebook_editor_0',
selections: [0],
selections: [{ start: 0, end: 1 }],
visibleRanges: []
}]
});

View file

@ -75,7 +75,7 @@ suite('NotebookConcatDocument', function () {
{
documentUri: notebookUri,
id: '_notebook_editor_0',
selections: [0],
selections: [{ start: 0, end: 1 }],
visibleRanges: []
}
]