Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[WIP] split and join #96156

Merged
merged 5 commits into from
May 4, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
97 changes: 97 additions & 0 deletions src/vs/workbench/contrib/notebook/browser/contrib/coreActions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,9 @@ const PASTE_CELL_COMMAND_ID = 'notebook.cell.paste';
const PASTE_CELL_ABOVE_COMMAND_ID = 'notebook.cell.pasteAbove';
const COPY_CELL_UP_COMMAND_ID = 'notebook.cell.copyUp';
const COPY_CELL_DOWN_COMMAND_ID = 'notebook.cell.copyDown';
const SPLIT_CELL_COMMAND_ID = 'notebook.cell.split';
const JOIN_CELL_ABOVE_COMMAND_ID = 'notebook.cell.joinAbove';
const JOIN_CELL_BELOW_COMMAND_ID = 'notebook.cell.joinBelow';

const EXECUTE_CELL_COMMAND_ID = 'notebook.cell.execute';
const CANCEL_CELL_COMMAND_ID = 'notebook.cell.cancelExecution';
Expand All @@ -72,6 +75,7 @@ const enum CellToolbarOrder {
MoveCellUp,
MoveCellDown,
EditCell,
SplitCell,
SaveCell,
ClearCellOutput,
InsertCell,
Expand Down Expand Up @@ -1396,3 +1400,96 @@ registerAction2(class extends Action2 {
editor.viewModel.notebookDocument.clearAllCellOutputs();
}
});

async function splitCell(context: INotebookCellActionContext): Promise<void> {
if (context.cell.cellKind === CellKind.Code) {
const newCells = context.notebookEditor.splitNotebookCell(context.cell);
if (newCells) {
context.notebookEditor.focusNotebookCell(newCells[newCells.length - 1], true);
}
}
}

registerAction2(class extends Action2 {
constructor() {
super(
{
id: SPLIT_CELL_COMMAND_ID,
title: localize('notebookActions.splitCell', "Split Cell"),
category: NOTEBOOK_ACTIONS_CATEGORY,
menu: {
id: MenuId.NotebookCellTitle,
when: ContextKeyExpr.and(NOTEBOOK_EDITOR_FOCUSED, NOTEBOOK_CELL_TYPE.isEqualTo('code'), NOTEBOOK_EDITOR_EDITABLE, InputFocusedContext),
order: CellToolbarOrder.SplitCell
},
icon: { id: 'codicon/split-vertical' },
f1: true
});
}

async run(accessor: ServicesAccessor, context?: INotebookCellActionContext) {
if (!isCellActionContext(context)) {
context = getActiveCellContext(accessor);
if (!context) {
return;
}
}

return splitCell(context);
}
});


async function joinCells(context: INotebookCellActionContext, direction: 'above' | 'below'): Promise<void> {
const cell = await context.notebookEditor.joinNotebookCells(context.cell, direction, CellKind.Code);
if (cell) {
context.notebookEditor.focusNotebookCell(cell, true);
}
}

registerAction2(class extends Action2 {
constructor() {
super(
{
id: JOIN_CELL_ABOVE_COMMAND_ID,
title: localize('notebookActions.joinCellAbove', "Join with Previous Cell"),
category: NOTEBOOK_ACTIONS_CATEGORY,
f1: true
});
}

async run(accessor: ServicesAccessor, context?: INotebookCellActionContext) {
if (!isCellActionContext(context)) {
context = getActiveCellContext(accessor);
if (!context) {
return;
}
}

return joinCells(context, 'above');
}
});

registerAction2(class extends Action2 {
constructor() {
super(
{
id: JOIN_CELL_BELOW_COMMAND_ID,
title: localize('notebookActions.joinCellBelow', "Join with Next Cell"),
category: NOTEBOOK_ACTIONS_CATEGORY,
f1: true
});
}

async run(accessor: ServicesAccessor, context?: INotebookCellActionContext) {
if (!isCellActionContext(context)) {
context = getActiveCellContext(accessor);
if (!context) {
return;
}
}

return joinCells(context, 'below');
}
});

14 changes: 14 additions & 0 deletions src/vs/workbench/contrib/notebook/browser/notebookBrowser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import { URI } from 'vs/base/common/uri';
import { CodeEditorWidget } from 'vs/editor/browser/widget/codeEditorWidget';
import { BareFontInfo } from 'vs/editor/common/config/fontInfo';
import { Range } from 'vs/editor/common/core/range';
import { IPosition } from 'vs/editor/common/core/position';
import { FindMatch } from 'vs/editor/common/model';
import { RawContextKey, ContextKeyExpr } from 'vs/platform/contextkey/common/contextkey';
import { OutputRenderer } from 'vs/workbench/contrib/notebook/browser/view/output/outputRenderer';
Expand Down Expand Up @@ -105,6 +106,9 @@ export interface ICellViewModel {
save(): void;
metadata: NotebookCellMetadata | undefined;
getEvaluatedMetadata(documentMetadata: NotebookDocumentMetadata | undefined): NotebookCellMetadata;
getSelectionsStartPosition(): IPosition[] | undefined;
setLinesContent(value: string[]): void;
getLinesContent(): string[];
}

export interface INotebookEditorMouseEvent {
Expand Down Expand Up @@ -168,6 +172,16 @@ export interface INotebookEditor {
*/
insertNotebookCell(cell: ICellViewModel | undefined, type: CellKind, direction?: 'above' | 'below', initialText?: string, ui?: boolean): CellViewModel | null;

/**
* Split a given cell into multiple cells of the same type using the selection start positions.
*/
splitNotebookCell(cell: ICellViewModel): CellViewModel[] | null;

/**
* Joins the given cell either with the cell above or the one below depending on the given direction.
*/
joinNotebookCells(cell: ICellViewModel, direction: 'above' | 'below', constraint?: CellKind): Promise<ICellViewModel | null>;

/**
* Delete a cell from the notebook
*/
Expand Down
154 changes: 154 additions & 0 deletions src/vs/workbench/contrib/notebook/browser/notebookEditor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ import { getExtraColor } from 'vs/workbench/contrib/welcome/walkThrough/common/w
import { IEditorGroup, IEditorGroupsService } from 'vs/workbench/services/editor/common/editorGroupsService';
import { NotebookEditorExtensionsRegistry } from 'vs/workbench/contrib/notebook/browser/notebookEditorExtensions';
import { onUnexpectedError } from 'vs/base/common/errors';
import { IPosition, Position } from 'vs/editor/common/core/position';

const $ = DOM.$;
const NOTEBOOK_EDITOR_VIEW_STATE_PREFERENCE_KEY = 'NotebookEditorViewState';
Expand Down Expand Up @@ -783,6 +784,159 @@ export class NotebookEditor extends BaseEditor implements INotebookEditor {
return newCell;
}

private isAtEOL(p: IPosition, lines: string[]) {
const line = lines[p.lineNumber - 1];
return line.length + 1 === p.column;
}

private pushIfAbsent(positions: IPosition[], p: IPosition) {
const last = positions.length > 0 ? positions[positions.length - 1] : undefined;
if (!last || last.lineNumber !== p.lineNumber || last.column !== p.column) {
positions.push(p);
}
}

/**
* Add split point at the beginning and the end;
* Move end of line split points to the beginning of the next line;
* Avoid duplicate split points
*/
private splitPointsToBoundaries(splitPoints: IPosition[], lines: string[]): IPosition[] | null {
const boundaries: IPosition[] = [];

// split points need to be sorted
splitPoints = splitPoints.sort((l, r) => {
const lineDiff = l.lineNumber - r.lineNumber;
const columnDiff = l.column - r.column;
return lineDiff !== 0 ? lineDiff : columnDiff;
});

// eat-up any split point at the beginning, i.e. we ignore the split point at the very beginning
this.pushIfAbsent(boundaries, new Position(1, 1));

for (let sp of splitPoints) {
if (this.isAtEOL(sp, lines) && sp.lineNumber < lines.length) {
sp = new Position(sp.lineNumber + 1, 1);
}
this.pushIfAbsent(boundaries, sp);
}

// eat-up any split point at the beginning, i.e. we ignore the split point at the very end
this.pushIfAbsent(boundaries, new Position(lines.length, lines[lines.length - 1].length + 1));

// if we only have two then they describe the whole range and nothing needs to be split
return boundaries.length > 2 ? boundaries : null;
}

private computeCellLinesContents(cell: ICellViewModel, splitPoints: IPosition[]): string[][] | null {
const lines = cell.getLinesContent();
const rangeBoundaries = this.splitPointsToBoundaries(splitPoints, lines);
if (!rangeBoundaries) {
return null;
}
const newLineModels: string[][] = [];
for (let i = 1; i < rangeBoundaries.length; i++) {
const start = rangeBoundaries[i - 1];
const end = rangeBoundaries[i];
// get the right lines
const newLines = lines.slice(start.lineNumber - 1, end.lineNumber);
if (start.lineNumber === end.lineNumber) {
// cut the line at the beginning and the end
let line = newLines[0];
line = line.slice(start.column - 1, end.column - 1);
newLines[0] = line;
}
else {
// cut last line at the end
let lastLine = newLines[newLines.length - 1];
lastLine = lastLine.slice(0, end.column - 1);
if (lastLine) {
newLines[newLines.length - 1] = lastLine;
} else {
newLines.pop();
}

// cut first line at the beginning
let firstLine = newLines[0];
firstLine = firstLine.slice(start.column - 1);
if (firstLine) {
newLines[0] = firstLine;
} else {
newLines.shift();
}
}
newLineModels.push(newLines);
}
return newLineModels;
}

splitNotebookCell(cell: ICellViewModel): CellViewModel[] | null {
if (!this.notebookViewModel!.metadata.editable) {
return null;
}

let splitPoints = cell.getSelectionsStartPosition();
if (splitPoints && splitPoints.length > 0) {
let newLinesContents = this.computeCellLinesContents(cell, splitPoints);
if (newLinesContents) {

// update the contents of the first cell
cell.setLinesContent(newLinesContents[0]);

// create new cells based on the new text models
const language = cell.model.language;
const kind = cell.cellKind;
let insertIndex = this.notebookViewModel!.getCellIndex(cell) + 1;
const newCells = [];
for (let j = 1; j < newLinesContents.length; j++, insertIndex++) {
newCells.push(this.notebookViewModel!.createCell(insertIndex, newLinesContents[j], language, kind, true));
}
return newCells;
}
}

return null;
}

async joinNotebookCells(cell: ICellViewModel, direction: 'above' | 'below', constraint?: CellKind): Promise<ICellViewModel | null> {
if (!this.notebookViewModel!.metadata.editable) {
return null;
}

if (constraint && cell.cellKind !== constraint) {
return null;
}

const index = this.notebookViewModel!.getCellIndex(cell);
if (index === 0 && direction === 'above') {
return null;
}

if (index === this.notebookViewModel!.length - 1 && direction === 'below') {
return null;
}

if (direction === 'above') {
const above = this.notebookViewModel!.viewCells[index - 1];
if (constraint && above.cellKind !== constraint) {
return null;
}
const newContent = above.getLinesContent().concat(cell.getLinesContent());
above.setLinesContent(newContent);
await this.deleteNotebookCell(cell);
return above;
} else {
const below = this.notebookViewModel!.viewCells[index + 1];
if (constraint && below.cellKind !== constraint) {
return null;
}
const newContent = cell.getLinesContent().concat(below.getLinesContent());
cell.setLinesContent(newContent);
await this.deleteNotebookCell(below);
return cell;
}
}

async deleteNotebookCell(cell: ICellViewModel): Promise<boolean> {
if (!this.notebookViewModel!.metadata.editable) {
return false;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -241,7 +241,7 @@ export class StatefullMarkdownCell extends Disposable {

bindEditorListeners(model: ITextModel, dimension?: IDimension) {
this.localDisposables.add(model.onDidChangeContent(() => {
this.viewCell.setText(model.getLinesContent());
this.viewCell.setLinesContent(model.getLinesContent());
let clientHeight = this.markdownContainer.clientHeight;
this.markdownContainer.innerHTML = '';
let renderedHTML = this.viewCell.getHTML();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { Emitter, Event } from 'vs/base/common/event';
import { Disposable, IDisposable } from 'vs/base/common/lifecycle';
import { ICodeEditor } from 'vs/editor/browser/editorBrowser';
import { Range } from 'vs/editor/common/core/range';
import { IPosition } from 'vs/editor/common/core/position';
import * as editorCommon from 'vs/editor/common/editorCommon';
import * as model from 'vs/editor/common/model';
import { SearchParams } from 'vs/editor/common/model/textModelSearch';
Expand Down Expand Up @@ -199,6 +200,23 @@ export abstract class BaseCellViewModel extends Disposable implements ICellViewM
return this.model.source.join('\n');
}

getLinesContent(): string[] {
if (this._textModel) {
return this._textModel.getLinesContent();
}

return this.model.source;
}

setLinesContent(value: string[]) {
if (this._textModel) {
// TODO @rebornix we should avoid creating a new string here
return this._textModel.setValue(value.join('\n'));
} else {
this.model.source = value;
}
}

abstract save(): void;

private saveViewState(): void {
Expand Down Expand Up @@ -271,6 +289,16 @@ export abstract class BaseCellViewModel extends Disposable implements ICellViewM
this._textEditor?.setSelection(range);
}

getSelectionsStartPosition(): IPosition[] | undefined {
if (this._textEditor) {
const selections = this._textEditor.getSelections();
return selections?.map(s => s.getStartPosition());
} else {
const selections = this._editorViewStates?.cursorState;
return selections?.map(s => s.selectionStart);
}
}

getLineScrollTopOffset(line: number): number {
if (!this._textEditor) {
return 0;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -112,7 +112,7 @@ export class MarkdownCellViewModel extends BaseCellViewModel implements ICellVie
}
}

setText(strs: string[]) {
setLinesContent(strs: string[]) {
this.model.source = strs;
this._html = null;
}
Expand Down
8 changes: 8 additions & 0 deletions src/vs/workbench/contrib/notebook/test/testNotebookEditor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,14 @@ export class TestNotebookEditor implements INotebookEditor {
throw new Error('Method not implemented.');
}

splitNotebookCell(cell: ICellViewModel): CellViewModel[] | null {
throw new Error('Method not implemented.');
}

joinNotebookCells(cell: ICellViewModel, direction: 'above' | 'below', constraint?: CellKind): Promise<ICellViewModel | null> {
throw new Error('Method not implemented.');
}

setSelection(cell: CellViewModel, selection: Range): void {
throw new Error('Method not implemented.');
}
Expand Down