From d256d562d6980e2fa0f042168b05c57436df55fa Mon Sep 17 00:00:00 2001 From: rebornix Date: Mon, 16 Mar 2020 14:01:03 -0700 Subject: [PATCH] replace first cut --- .../browser/find/simpleFindReplaceWidget.ts | 10 +- .../browser/contrib/notebookActions.ts | 33 +++++ .../browser/contrib/notebookFindWidget.ts | 21 ++++ .../browser/view/renderers/markdownCell.ts | 8 ++ .../viewModel/notebookCellViewModel.ts | 4 + .../browser/viewModel/notebookViewModel.ts | 113 +++++++++++++++--- .../notebook/test/notebookViewModel.test.ts | 10 +- .../notebook/test/testNotebookEditor.ts | 6 +- 8 files changed, 184 insertions(+), 21 deletions(-) diff --git a/src/vs/workbench/contrib/codeEditor/browser/find/simpleFindReplaceWidget.ts b/src/vs/workbench/contrib/codeEditor/browser/find/simpleFindReplaceWidget.ts index 9399ffe1856..fafb8102c1d 100644 --- a/src/vs/workbench/contrib/codeEditor/browser/find/simpleFindReplaceWidget.ts +++ b/src/vs/workbench/contrib/codeEditor/browser/find/simpleFindReplaceWidget.ts @@ -212,7 +212,7 @@ export abstract class SimpleFindReplaceWidget extends Widget { label: NLS_REPLACE_BTN_LABEL, className: 'codicon codicon-replace', onTrigger: () => { - // this._controller.replace(); + this.replaceOne(); } })); @@ -221,7 +221,7 @@ export abstract class SimpleFindReplaceWidget extends Widget { label: NLS_REPLACE_ALL_BTN_LABEL, className: 'codicon codicon-replace-all', onTrigger: () => { - // this._controller.replaceAll(); + this.replaceAll(); } })); @@ -234,6 +234,8 @@ export abstract class SimpleFindReplaceWidget extends Widget { protected abstract onInputChanged(): boolean; protected abstract find(previous: boolean): void; protected abstract findFirst(): void; + protected abstract replaceOne(): void; + protected abstract replaceAll(): void; protected abstract onFocusTrackerFocus(): void; protected abstract onFocusTrackerBlur(): void; protected abstract onFindInputFocusTrackerFocus(): void; @@ -245,6 +247,10 @@ export abstract class SimpleFindReplaceWidget extends Widget { return this._findInput.getValue(); } + protected get replaceValue() { + return this._replaceInput.getValue(); + } + public get focusTracker(): dom.IFocusTracker { return this._focusTracker; } diff --git a/src/vs/workbench/contrib/notebook/browser/contrib/notebookActions.ts b/src/vs/workbench/contrib/notebook/browser/contrib/notebookActions.ts index e3aa823e5a9..aa1a99a89f8 100644 --- a/src/vs/workbench/contrib/notebook/browser/contrib/notebookActions.ts +++ b/src/vs/workbench/contrib/notebook/browser/contrib/notebookActions.ts @@ -858,6 +858,39 @@ registerAction2(class extends Action2 { } }); +registerAction2(class extends Action2 { + constructor() { + super({ + id: 'workbench.action.notebook.undo', + title: 'Notebook Undo', + keybinding: { + when: ContextKeyExpr.and(NOTEBOOK_EDITOR_FOCUSED, ContextKeyExpr.not(InputFocusedContextKey)), + primary: KeyMod.CtrlCmd | KeyCode.KEY_Z, + weight: KeybindingWeight.WorkbenchContrib + } + }); + } + + async run(accessor: ServicesAccessor): Promise { + const editorService = accessor.get(IEditorService); + + const editor = getActiveNotebookEditor(editorService); + if (!editor) { + return; + } + + const viewModel = editor.viewModel; + + if (!viewModel) { + return; + } + + if (viewModel.canUndo()) { + viewModel.undo(); + } + } +}); + registerAction2(class extends Action2 { constructor() { super({ diff --git a/src/vs/workbench/contrib/notebook/browser/contrib/notebookFindWidget.ts b/src/vs/workbench/contrib/notebook/browser/contrib/notebookFindWidget.ts index f4fad991265..d22b8c73cf6 100644 --- a/src/vs/workbench/contrib/notebook/browser/contrib/notebookFindWidget.ts +++ b/src/vs/workbench/contrib/notebook/browser/contrib/notebookFindWidget.ts @@ -82,6 +82,27 @@ export class NotebookFindWidget extends SimpleFindReplaceWidget { this.revealCellRange(nextIndex.index, nextIndex.remainder); } + protected replaceOne() { + if (!this._findMatches.length) { + return; + } + + if (!this._findMatchesStarts) { + this.set(this._findMatches); + } + + const nextIndex = this._findMatchesStarts!.getIndexOf(this._currentMatch); + const cell = this._findMatches[nextIndex.index].cell; + const match = this._findMatches[nextIndex.index].matches[nextIndex.remainder]; + + return this._notebookEditor.viewModel!.replaceOne(cell, match.range, this.replaceValue); + + } + + protected replaceAll() { + return this._notebookEditor.viewModel!.replaceAll(this._findMatches, this.replaceValue); + } + private revealCellRange(cellIndex: number, matchIndex: number) { this._findMatches[cellIndex].cell.state = CellState.Editing; this._notebookEditor.selectElement(this._findMatches[cellIndex].cell); diff --git a/src/vs/workbench/contrib/notebook/browser/view/renderers/markdownCell.ts b/src/vs/workbench/contrib/notebook/browser/view/renderers/markdownCell.ts index 0def91c3e3c..d33603a2318 100644 --- a/src/vs/workbench/contrib/notebook/browser/view/renderers/markdownCell.ts +++ b/src/vs/workbench/contrib/notebook/browser/view/renderers/markdownCell.ts @@ -185,6 +185,14 @@ export class StatefullMarkdownCell extends Disposable { const clientHeight = this.cellContainer.clientHeight; notebookEditor.layoutNotebookCell(viewCell, clientHeight); })); + + this.localDisposables.add(viewCell.onDidChangeContent(() => { + this.cellContainer.innerHTML = ''; + let renderedHTML = viewCell.getHTML(); + if (renderedHTML) { + this.cellContainer.appendChild(renderedHTML); + } + })); } } }; diff --git a/src/vs/workbench/contrib/notebook/browser/viewModel/notebookCellViewModel.ts b/src/vs/workbench/contrib/notebook/browser/viewModel/notebookCellViewModel.ts index 555711e0522..26468a571f2 100644 --- a/src/vs/workbench/contrib/notebook/browser/viewModel/notebookCellViewModel.ts +++ b/src/vs/workbench/contrib/notebook/browser/viewModel/notebookCellViewModel.ts @@ -110,6 +110,8 @@ export class CellViewModel extends Disposable implements ICellViewModel { private _editorViewStates: editorCommon.ICodeEditorViewState | null; private _lastDecorationId: number = 0; private _resolvedDecorations = new Map(); + private readonly _onDidChangeContent: Emitter = this._register(new Emitter()); + public readonly onDidChangeContent: Event = this._onDidChangeContent.event; private readonly _onDidChangeCursorSelection: Emitter = this._register(new Emitter()); public readonly onDidChangeCursorSelection: Event = this._onDidChangeCursorSelection.event; @@ -275,6 +277,8 @@ export class CellViewModel extends Disposable implements ICellViewModel { this._register(ref); this._register(this._textModel.onDidChangeContent(() => { this.cell.isDirty = true; + this._html = null; + this._onDidChangeContent.fire(); })); } return this._textModel; diff --git a/src/vs/workbench/contrib/notebook/browser/viewModel/notebookViewModel.ts b/src/vs/workbench/contrib/notebook/browser/viewModel/notebookViewModel.ts index 550d72a7b7c..c2da0531af9 100644 --- a/src/vs/workbench/contrib/notebook/browser/viewModel/notebookViewModel.ts +++ b/src/vs/workbench/contrib/notebook/browser/viewModel/notebookViewModel.ts @@ -13,6 +13,11 @@ import { NotebookCellsSplice, ICell } from 'vs/workbench/contrib/notebook/common import { IModelDeltaDecoration } from 'vs/editor/common/model'; import { onUnexpectedError } from 'vs/base/common/errors'; import { CellFindMatch, CellState, ICellViewModel } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; +import { IBulkEditService } from 'vs/editor/browser/services/bulkEditService'; +import { Range } from 'vs/editor/common/core/range'; +import { WorkspaceTextEdit } from 'vs/editor/common/modes'; +import { URI } from 'vs/base/common/uri'; +import { IUndoRedoService } from 'vs/platform/undoRedo/common/undoRedo'; export interface INotebookEditorViewState { editingCells: { [key: number]: boolean }; @@ -67,10 +72,21 @@ export class NotebookViewModel extends Disposable { private readonly _onDidChangeCells = new Emitter(); get onDidChangeCells(): Event { return this._onDidChangeCells.event; } + private _lastNotebookEditResource: URI[] = []; + + get lastNotebookEditResource(): URI | null { + if (this._lastNotebookEditResource.length) { + return this._lastNotebookEditResource[this._lastNotebookEditResource.length - 1]; + } + return null; + } + constructor( public viewType: string, private _model: NotebookEditorModel, @IInstantiationService private readonly instantiationService: IInstantiationService, + @IBulkEditService private readonly bulkEditService: IBulkEditService, + @IUndoRedoService private readonly undoService: IUndoRedoService ) { super(); @@ -98,22 +114,6 @@ export class NotebookViewModel extends Disposable { return this._viewCells.indexOf(cell as CellViewModel); } - /** - * Search in notebook text model - * @param value - */ - find(value: string): CellFindMatch[] { - const matches: CellFindMatch[] = []; - this._viewCells.forEach(cell => { - const cellMatches = cell.startFind(value); - if (cellMatches) { - matches.push(cellMatches); - } - }); - - return matches; - } - insertCell(index: number, cell: ICell): CellViewModel { const newCell = this.instantiationService.createInstance(CellViewModel, this.viewType, this.handle, cell); this._viewCells!.splice(index, 0, newCell); @@ -245,6 +245,87 @@ export class NotebookViewModel extends Disposable { return ret; } + + /** + * Search in notebook text model + * @param value + */ + find(value: string): CellFindMatch[] { + const matches: CellFindMatch[] = []; + this._viewCells.forEach(cell => { + const cellMatches = cell.startFind(value); + if (cellMatches) { + matches.push(cellMatches); + } + }); + + return matches; + } + + replaceOne(cell: ICellViewModel, range: Range, text: string): Promise { + const viewCell = cell as CellViewModel; + this._lastNotebookEditResource.push(viewCell.uri); + return viewCell.resolveTextModel().then(() => { + this.bulkEditService.apply({ edits: [{ edit: { range: range, text: text }, resource: cell.uri }] }, { quotableLabel: 'Notebook Replace' }); + }); + } + + async replaceAll(matches: CellFindMatch[], text: string): Promise { + if (!matches.length) { + return; + } + + let textEdits: WorkspaceTextEdit[] = []; + this._lastNotebookEditResource.push(matches[0].cell.uri); + + matches.forEach(match => { + match.matches.forEach(singleMatch => { + textEdits.push({ + edit: { range: singleMatch.range, text: text }, + resource: match.cell.uri + }); + }); + }); + + return Promise.all(matches.map(match => { + return match.cell.resolveTextModel(); + })).then(async () => { + this.bulkEditService.apply({ edits: textEdits }, { quotableLabel: 'Notebook Replace All' }); + return; + }); + } + + canUndo(): boolean { + const lastResource = this.lastNotebookEditResource; + + if (!lastResource) { + return false; + } + + const lastElement = this.undoService.getLastElement(lastResource); + + if (lastElement?.label === 'Notebook Replace' || lastElement?.label === 'Notebook Replace All') { + return true; + } + + return false; + } + + undo() { + const lastResource = this.lastNotebookEditResource; + + if (!lastResource) { + return; + } + + const lastElement = this.undoService.getLastElement(lastResource); + + if (lastElement?.label === 'Notebook Replace' || lastElement?.label === 'Notebook Replace All') { + this.undoService.undo(lastResource); + this._lastNotebookEditResource.pop(); + } + } + equal(model: NotebookEditorModel) { return this._model === model; } diff --git a/src/vs/workbench/contrib/notebook/test/notebookViewModel.test.ts b/src/vs/workbench/contrib/notebook/test/notebookViewModel.test.ts index b7ded99e442..d423a255cf0 100644 --- a/src/vs/workbench/contrib/notebook/test/notebookViewModel.test.ts +++ b/src/vs/workbench/contrib/notebook/test/notebookViewModel.test.ts @@ -10,20 +10,26 @@ import { NotebookEditorModel } from 'vs/workbench/contrib/notebook/browser/noteb import { NotebookViewModel } from 'vs/workbench/contrib/notebook/browser/viewModel/notebookViewModel'; import { CellKind } from 'vs/workbench/contrib/notebook/common/notebookCommon'; import { TestNotebook, withTestNotebook, TestCell } from 'vs/workbench/contrib/notebook/test/testNotebookEditor'; +import { IBulkEditService } from 'vs/editor/browser/services/bulkEditService'; +import { IUndoRedoService } from 'vs/platform/undoRedo/common/undoRedo'; suite('NotebookViewModel', () => { const instantiationService = new TestInstantiationService(); + const blukEditService = instantiationService.get(IBulkEditService); + const undoRedoService = instantiationService.get(IUndoRedoService); test('ctor', function () { const notebook = new TestNotebook(0, 'notebook', URI.parse('test')); const model = new NotebookEditorModel(notebook); - const viewModel = new NotebookViewModel('notebook', model, instantiationService); + const viewModel = new NotebookViewModel('notebook', model, instantiationService, blukEditService, undoRedoService); assert.equal(viewModel.viewType, 'notebook'); }); test('insert/delete', function () { withTestNotebook( instantiationService, + blukEditService, + undoRedoService, [ [['var a = 1;'], 'javascript', CellKind.Code, []], [['var b = 2;'], 'javascript', CellKind.Code, []] @@ -45,6 +51,8 @@ suite('NotebookViewModel', () => { test('index', function () { withTestNotebook( instantiationService, + blukEditService, + undoRedoService, [ [['var a = 1;'], 'javascript', CellKind.Code, []], [['var b = 2;'], 'javascript', CellKind.Code, []] diff --git a/src/vs/workbench/contrib/notebook/test/testNotebookEditor.ts b/src/vs/workbench/contrib/notebook/test/testNotebookEditor.ts index f1fff18c631..e5847b2a3c9 100644 --- a/src/vs/workbench/contrib/notebook/test/testNotebookEditor.ts +++ b/src/vs/workbench/contrib/notebook/test/testNotebookEditor.ts @@ -17,6 +17,8 @@ import { IMouseWheelEvent } from 'vs/base/browser/mouseEvent'; import { OutputRenderer } from 'vs/workbench/contrib/notebook/browser/view/output/outputRenderer'; import { BareFontInfo } from 'vs/editor/common/config/fontInfo'; import { Range } from 'vs/editor/common/core/range'; +import { IBulkEditService } from 'vs/editor/browser/services/bulkEditService'; +import { IUndoRedoService } from 'vs/platform/undoRedo/common/undoRedo'; export class TestCell implements ICell { uri: URI; @@ -200,7 +202,7 @@ export function createTestCellViewModel(instantiationService: IInstantiationServ return instantiationService.createInstance(CellViewModel, viewType, notebookHandle, mockCell); } -export function withTestNotebook(instantiationService: IInstantiationService, cells: [string[], string, CellKind, IOutput[]][], callback: (editor: TestNotebookEditor, viewModel: NotebookViewModel) => void) { +export function withTestNotebook(instantiationService: IInstantiationService, blukEditService: IBulkEditService, undoRedoService: IUndoRedoService, cells: [string[], string, CellKind, IOutput[]][], callback: (editor: TestNotebookEditor, viewModel: NotebookViewModel) => void) { const viewType = 'notebook'; const editor = new TestNotebookEditor(); const notebook = new TestNotebook(0, viewType, URI.parse('test')); @@ -208,7 +210,7 @@ export function withTestNotebook(instantiationService: IInstantiationService, ce return new TestCell(viewType, index, cell[0], cell[1], cell[2], cell[3]); }); const model = new NotebookEditorModel(notebook); - const viewModel = new NotebookViewModel(viewType, model, instantiationService); + const viewModel = new NotebookViewModel(viewType, model, instantiationService, blukEditService, undoRedoService); callback(editor, viewModel); -- GitLab