add tests for joining single cell.

This commit is contained in:
rebornix 2021-03-15 20:15:29 -07:00
parent 3f7651fed0
commit 881e370539
No known key found for this signature in database
GPG key ID: 181FC90D15393C20
6 changed files with 172 additions and 151 deletions

View file

@ -9,12 +9,16 @@ import { MenuId, registerAction2 } from 'vs/platform/actions/common/actions';
import { ContextKeyExpr } from 'vs/platform/contextkey/common/contextkey';
import { InputFocusedContext } from 'vs/platform/contextkey/common/contextkeys';
import { KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegistry';
import { Range } from 'vs/editor/common/core/range';
import { CellOverflowToolbarGroups, CellToolbarOrder, CELL_TITLE_CELL_GROUP_ID, INotebookCellActionContext, NotebookCellAction } from 'vs/workbench/contrib/notebook/browser/contrib/coreActions';
import { expandCellRangesWithHiddenCells, NOTEBOOK_CELL_EDITABLE, NOTEBOOK_CELL_EDITOR_FOCUSED, NOTEBOOK_EDITOR_EDITABLE, NOTEBOOK_EDITOR_FOCUSED } from 'vs/workbench/contrib/notebook/browser/notebookBrowser';
import { expandCellRangesWithHiddenCells, ICellViewModel, NOTEBOOK_CELL_EDITABLE, NOTEBOOK_CELL_EDITOR_FOCUSED, NOTEBOOK_EDITOR_EDITABLE, NOTEBOOK_EDITOR_FOCUSED } from 'vs/workbench/contrib/notebook/browser/notebookBrowser';
import * as icons from 'vs/workbench/contrib/notebook/browser/notebookIcons';
import { ServicesAccessor } from 'vs/platform/instantiation/common/instantiation';
import { CellEditType, cellRangeContains, cellRangesToIndexes, NOTEBOOK_EDITOR_CURSOR_BEGIN_END, SelectionStateType } from 'vs/workbench/contrib/notebook/common/notebookCommon';
import { CellEditType, CellKind, cellRangeContains, cellRangesToIndexes, NOTEBOOK_EDITOR_CURSOR_BEGIN_END, SelectionStateType } from 'vs/workbench/contrib/notebook/common/notebookCommon';
import { cloneNotebookCellTextModel } from 'vs/workbench/contrib/notebook/common/model/notebookCellTextModel';
import { CellViewModel, NotebookViewModel } from 'vs/workbench/contrib/notebook/browser/viewModel/notebookViewModel';
import { IBulkEditService, ResourceEdit, ResourceTextEdit } from 'vs/editor/browser/services/bulkEditService';
import { ResourceNotebookCellEdit } from 'vs/workbench/contrib/bulkEdit/browser/bulkCellEdits';
const MOVE_CELL_UP_COMMAND_ID = 'notebook.cell.moveUp';
const MOVE_CELL_DOWN_COMMAND_ID = 'notebook.cell.moveDown';
@ -282,11 +286,108 @@ registerAction2(class extends NotebookCellAction {
}
});
export async function joinCells(context: INotebookCellActionContext, direction: 'above' | 'below'): Promise<void> {
const cell = await context.notebookEditor.joinNotebookCells(context.cell, direction);
if (cell) {
context.notebookEditor.focusNotebookCell(cell, 'editor');
export async function joinNotebookCells(viewModel: NotebookViewModel, cell: ICellViewModel, direction: 'above' | 'below', constraint?: CellKind): Promise<{ edits: ResourceEdit[], cell: ICellViewModel } | null> {
if (!viewModel) {
return null;
}
if (!viewModel.metadata.editable) {
return null;
}
const index = viewModel.getCellIndex(cell);
if (!cell.getEvaluatedMetadata(viewModel.notebookDocument.metadata).editable) {
return null;
}
if (constraint && cell.cellKind !== constraint) {
return null;
}
if (index === 0 && direction === 'above') {
return null;
}
if (index === viewModel.length - 1 && direction === 'below') {
return null;
}
if (direction === 'above') {
const above = viewModel.viewCells[index - 1] as CellViewModel;
if (constraint && above.cellKind !== constraint) {
return null;
}
if (!above.getEvaluatedMetadata(viewModel.notebookDocument.metadata).editable) {
return null;
}
// const endSelections = [above.handle];
const insertContent = (cell.textBuffer.getEOL() ?? '') + cell.getText();
const aboveCellLineCount = above.textBuffer.getLineCount();
const aboveCellLastLineEndColumn = above.textBuffer.getLineLength(aboveCellLineCount);
return {
edits: [
new ResourceTextEdit(above.uri, { range: new Range(aboveCellLineCount, aboveCellLastLineEndColumn + 1, aboveCellLineCount, aboveCellLastLineEndColumn + 1), text: insertContent }),
new ResourceNotebookCellEdit(viewModel.notebookDocument.uri,
{
editType: CellEditType.Replace,
index: index,
count: 1,
cells: []
}
)
],
cell: above
};
} else {
const below = viewModel.viewCells[index + 1] as CellViewModel;
if (constraint && below.cellKind !== constraint) {
return null;
}
if (!below.getEvaluatedMetadata(viewModel.notebookDocument.metadata).editable) {
return null;
}
const insertContent = (cell.textBuffer.getEOL() ?? '') + below.getText();
const cellLineCount = cell.textBuffer.getLineCount();
const cellLastLineEndColumn = cell.textBuffer.getLineLength(cellLineCount);
return {
edits: [
new ResourceTextEdit(cell.uri, { range: new Range(cellLineCount, cellLastLineEndColumn + 1, cellLineCount, cellLastLineEndColumn + 1), text: insertContent }),
new ResourceNotebookCellEdit(viewModel.notebookDocument.uri,
{
editType: CellEditType.Replace,
index: index + 1,
count: 1,
cells: []
}
)
],
cell
};
}
}
export async function joinCells(bulkEditService: IBulkEditService, context: INotebookCellActionContext, direction: 'above' | 'below'): Promise<void> {
const ret = await joinNotebookCells(context.notebookEditor.viewModel, context.cell, direction);
if (!ret) {
return;
}
await bulkEditService.apply(
ret?.edits,
{ quotableLabel: 'Join Notebook Cells' }
);
context.notebookEditor.focusNotebookCell(ret.cell, 'editor');
// TODO
// viewModel.selectionHandles = endSelections;
}
registerAction2(class extends NotebookCellAction {
@ -310,7 +411,8 @@ registerAction2(class extends NotebookCellAction {
}
async runWithContext(accessor: ServicesAccessor, context: INotebookCellActionContext) {
return joinCells(context, 'above');
const bulkEditService = accessor.get(IBulkEditService);
return joinCells(bulkEditService, context, 'above');
}
});
@ -335,6 +437,7 @@ registerAction2(class extends NotebookCellAction {
}
async runWithContext(accessor: ServicesAccessor, context: INotebookCellActionContext) {
return joinCells(context, 'below');
const bulkEditService = accessor.get(IBulkEditService);
return joinCells(bulkEditService, context, 'below');
}
});

View file

@ -4,9 +4,10 @@
*--------------------------------------------------------------------------------------------*/
import * as assert from 'assert';
import { copyCellRange, moveCellRange } from 'vs/workbench/contrib/notebook/browser/contrib/cellOperations/cellOperations';
import { ResourceNotebookCellEdit } from 'vs/workbench/contrib/bulkEdit/browser/bulkCellEdits';
import { copyCellRange, joinNotebookCells, moveCellRange } from 'vs/workbench/contrib/notebook/browser/contrib/cellOperations/cellOperations';
import { FoldingModel, updateFoldingStateAtIndex } from 'vs/workbench/contrib/notebook/browser/contrib/fold/foldingModel';
import { CellKind, SelectionStateType } from 'vs/workbench/contrib/notebook/common/notebookCommon';
import { CellEditType, CellKind, SelectionStateType } from 'vs/workbench/contrib/notebook/common/notebookCommon';
import { withTestNotebook } from 'vs/workbench/contrib/notebook/test/testNotebookEditor';
suite('CellOperations', () => {
@ -141,5 +142,56 @@ suite('CellOperations', () => {
assert.strictEqual(viewModel.viewCells[3].getText(), 'var b = 1;');
});
});
test('Join cell with below - single cell', async function () {
await withTestNotebook(
[
['# header a', 'markdown', CellKind.Markdown, [], {}],
['var b = 1;', 'javascript', CellKind.Code, [], {}],
['# header b', 'markdown', CellKind.Markdown, [], {}],
['var b = 2;', 'javascript', CellKind.Code, [], {}],
['var c = 3;', 'javascript', CellKind.Code, [], {}]
],
async (editor, accessor) => {
const viewModel = editor.viewModel;
viewModel.updateSelectionsState({ kind: SelectionStateType.Index, focus: { start: 3, end: 4 }, selections: [{ start: 3, end: 4 }] });
const ret = await joinNotebookCells(editor.viewModel, viewModel.viewCells[3], 'below');
assert.strictEqual(ret?.edits.length, 2);
assert.deepStrictEqual(ret?.edits[1], new ResourceNotebookCellEdit(viewModel.notebookDocument.uri,
{
editType: CellEditType.Replace,
index: 4,
count: 1,
cells: []
}
));
});
});
test('Join cell with above - single cell', async function () {
await withTestNotebook(
[
['# header a', 'markdown', CellKind.Markdown, [], {}],
['var b = 1;', 'javascript', CellKind.Code, [], {}],
['# header b', 'markdown', CellKind.Markdown, [], {}],
['var b = 2;', 'javascript', CellKind.Code, [], {}],
['var c = 3;', 'javascript', CellKind.Code, [], {}]
],
async (editor, accessor) => {
const viewModel = editor.viewModel;
viewModel.updateSelectionsState({ kind: SelectionStateType.Index, focus: { start: 3, end: 4 }, selections: [{ start: 3, end: 4 }] });
const ret = await joinNotebookCells(editor.viewModel, viewModel.viewCells[4], 'above');
assert.strictEqual(ret?.edits.length, 2);
assert.deepStrictEqual(ret?.edits[1], new ResourceNotebookCellEdit(viewModel.notebookDocument.uri,
{
editType: CellEditType.Replace,
index: 4,
count: 1,
cells: []
}
));
});
});
});

View file

@ -410,11 +410,6 @@ export interface INotebookEditor extends ICommonNotebookEditor {
*/
splitNotebookCell(cell: ICellViewModel): Promise<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
*/

View file

@ -1669,31 +1669,6 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditor
return this.viewModel.splitNotebookCell(index);
}
async joinNotebookCells(cell: ICellViewModel, direction: 'above' | 'below', constraint?: CellKind): Promise<ICellViewModel | null> {
if (!this.viewModel) {
return null;
}
if (!this.viewModel.metadata.editable) {
return null;
}
const index = this.viewModel.getCellIndex(cell);
const ret = await this.viewModel.joinNotebookCells(index, direction, constraint);
if (ret) {
ret.deletedCells.forEach(cell => {
if (this._pendingLayouts.has(cell)) {
this._pendingLayouts.get(cell)!.dispose();
}
});
return ret.cell;
} else {
return null;
}
}
async deleteNotebookCell(cell: ICellViewModel): Promise<boolean> {
if (!this.viewModel) {
return false;

View file

@ -878,106 +878,6 @@ export class NotebookViewModel extends Disposable implements EditorFoldingStateD
return null;
}
async joinNotebookCells(index: number, direction: 'above' | 'below', constraint?: CellKind): Promise<{ cell: ICellViewModel, deletedCells: ICellViewModel[] } | null> {
const cell = this.viewCells[index] as CellViewModel;
if (!this.metadata.editable) {
return null;
}
if (!cell.getEvaluatedMetadata(this.notebookDocument.metadata).editable) {
return null;
}
if (constraint && cell.cellKind !== constraint) {
return null;
}
if (index === 0 && direction === 'above') {
return null;
}
if (index === this.length - 1 && direction === 'below') {
return null;
}
if (direction === 'above') {
const above = this.viewCells[index - 1] as CellViewModel;
if (constraint && above.cellKind !== constraint) {
return null;
}
if (!above.getEvaluatedMetadata(this.notebookDocument.metadata).editable) {
return null;
}
await above.resolveTextModel();
if (!above.hasModel()) {
return null;
}
const endSelections = [above.handle];
const insertContent = (cell.textModel?.getEOL() ?? '') + cell.getText();
const aboveCellLineCount = above.textModel.getLineCount();
const aboveCellLastLineEndColumn = above.textModel.getLineLength(aboveCellLineCount);
await this._bulkEditService.apply(
[
new ResourceTextEdit(above.uri, { range: new Range(aboveCellLineCount, aboveCellLastLineEndColumn + 1, aboveCellLineCount, aboveCellLastLineEndColumn + 1), text: insertContent }),
new ResourceNotebookCellEdit(this._notebook.uri,
{
editType: CellEditType.Replace,
index: index,
count: 1,
cells: []
}
)
],
{ quotableLabel: 'Join Notebook Cells' }
);
this.selectionHandles = endSelections;
return { cell: above, deletedCells: [cell] };
} else {
const below = this.viewCells[index + 1] as CellViewModel;
if (constraint && below.cellKind !== constraint) {
return null;
}
if (!below.getEvaluatedMetadata(this.notebookDocument.metadata).editable) {
return null;
}
await cell.resolveTextModel();
if (!cell.hasModel()) {
return null;
}
const insertContent = (cell.textModel?.getEOL() ?? '') + below.getText();
const cellLineCount = cell.textModel.getLineCount();
const cellLastLineEndColumn = cell.textModel.getLineLength(cellLineCount);
await this._bulkEditService.apply(
[
new ResourceTextEdit(cell.uri, { range: new Range(cellLineCount, cellLastLineEndColumn + 1, cellLineCount, cellLastLineEndColumn + 1), text: insertContent }),
new ResourceNotebookCellEdit(this._notebook.uri,
{
editType: CellEditType.Replace,
index: index + 1,
count: 1,
cells: []
}
)
],
{ quotableLabel: 'Join Notebook Cells' }
);
return { cell, deletedCells: [below] };
}
}
getEditorViewState(): INotebookEditorViewState {
const editingCells: { [key: number]: boolean } = {};
this._viewCells.forEach((cell, i) => {

View file

@ -9,7 +9,6 @@ import { NotImplementedError } from 'vs/base/common/errors';
import { Emitter, Event } from 'vs/base/common/event';
import { Schemas } from 'vs/base/common/network';
import { URI } from 'vs/base/common/uri';
import { IBulkEditService } from 'vs/editor/browser/services/bulkEditService';
import { ITextModelService } from 'vs/editor/common/services/resolverService';
import { ContextKeyService } from 'vs/platform/contextkey/browser/contextKeyService';
import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey';
@ -35,6 +34,7 @@ import { IFileStatWithMetadata } from 'vs/platform/files/common/files';
import { NotebookCellList } from 'vs/workbench/contrib/notebook/browser/view/notebookCellList';
import { ListViewInfoAccessor } from 'vs/workbench/contrib/notebook/browser/notebookEditorWidget';
import { mock } from 'vs/base/test/common/mock';
import { ServicesAccessor } from 'vs/platform/instantiation/common/instantiation';
export class TestCell extends NotebookCellTextModel {
constructor(
@ -125,7 +125,6 @@ export class NotebookEditorTestModel extends EditorModel implements INotebookEdi
export function setupInstantiationService() {
const instantiationService = new TestInstantiationService();
instantiationService.stub(IUndoRedoService, instantiationService.createInstance(UndoRedoService));
instantiationService.stub(IConfigurationService, new TestConfigurationService());
instantiationService.stub(IThemeService, new TestThemeService());
@ -137,14 +136,10 @@ export function setupInstantiationService() {
return instantiationService;
}
export async function withTestNotebook<R = any>(cells: [source: string, lang: string, kind: CellKind, output?: IOutputDto[], metadata?: NotebookCellMetadata][], callback: (editor: IActiveNotebookEditor, textModel: NotebookTextModel) => Promise<R> | R): Promise<R> {
export async function withTestNotebook<R = any>(cells: [source: string, lang: string, kind: CellKind, output?: IOutputDto[], metadata?: NotebookCellMetadata][], callback: (editor: IActiveNotebookEditor, accessor: ServicesAccessor) => Promise<R> | R): Promise<R> {
const instantiationService = setupInstantiationService();
const undoRedoService = instantiationService.get(IUndoRedoService);
const textModelService = instantiationService.get(ITextModelService);
const bulkEditService = instantiationService.get(IBulkEditService);
const viewType = 'notebook';
const notebook = new NotebookTextModel(viewType, URI.parse('test'), cells.map(cell => {
const notebook = instantiationService.createInstance(NotebookTextModel, viewType, URI.parse('test'), cells.map(cell => {
return {
source: cell[0],
language: cell[1],
@ -152,10 +147,11 @@ export async function withTestNotebook<R = any>(cells: [source: string, lang: st
outputs: cell[3] ?? [],
metadata: cell[4]
};
}), notebookDocumentMetadataDefaults, { transientMetadata: {}, transientOutputs: false }, undoRedoService, textModelService);
}), notebookDocumentMetadataDefaults, { transientMetadata: {}, transientOutputs: false });
const model = new NotebookEditorTestModel(notebook);
const eventDispatcher = new NotebookEventDispatcher();
const viewModel = new NotebookViewModel(viewType, model.notebook, eventDispatcher, null, instantiationService, bulkEditService, undoRedoService);
const viewModel: NotebookViewModel = instantiationService.createInstance(NotebookViewModel, viewType, model.notebook, eventDispatcher, null);
const cellList = createNotebookCellList(instantiationService);
cellList.attachViewModel(viewModel);
@ -177,7 +173,7 @@ export async function withTestNotebook<R = any>(cells: [source: string, lang: st
}
};
const res = await callback(notebookEditor, notebook);
const res = await callback(notebookEditor, instantiationService);
if (res instanceof Promise) {
res.finally(() => viewModel.dispose());
} else {