diff --git a/src/vs/editor/browser/controller/coreCommands.ts b/src/vs/editor/browser/controller/coreCommands.ts index 13ec59bc49f..567b9cdc08a 100644 --- a/src/vs/editor/browser/controller/coreCommands.ts +++ b/src/vs/editor/browser/controller/coreCommands.ts @@ -25,6 +25,7 @@ import { ICommandHandlerDescription } from 'vs/platform/commands/common/commands import { ContextKeyExpr } from 'vs/platform/contextkey/common/contextkey'; import { ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; import { KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegistry'; +import { EditorOption } from 'vs/editor/common/config/editorOptions'; const CORE_WEIGHT = KeybindingWeight.EditorCore; @@ -1529,6 +1530,102 @@ export namespace CoreNavigationCommands { }); } +/** + * A command that will: + * 1. invoke a command on the focused editor. + * 2. otherwise, invoke a browser built-in command on the `activeElement`. + * 3. otherwise, invoke a command on the workbench active editor. + */ +abstract class EditorOrNativeTextInputCommand extends Command { + + public runCommand(accessor: ServicesAccessor, args: any): void { + + const focusedEditor = accessor.get(ICodeEditorService).getFocusedCodeEditor(); + // Only if editor text focus (i.e. not if editor has widget focus). + if (focusedEditor && focusedEditor.hasTextFocus()) { + return this.runEditorCommand(accessor, focusedEditor, args); + } + + // Ignore this action when user is focused on an element that allows for entering text + const activeElement = document.activeElement; + if (activeElement && ['input', 'textarea'].indexOf(activeElement.tagName.toLowerCase()) >= 0) { + return this.runDOMCommand(); + } + + // Redirecting to active editor + const activeEditor = accessor.get(ICodeEditorService).getActiveCodeEditor(); + if (activeEditor) { + activeEditor.focus(); + return this.runEditorCommand(accessor, activeEditor, args); + } + } + + public abstract runDOMCommand(): void; + public abstract runEditorCommand(accessor: ServicesAccessor, editor: ICodeEditor, args: any): void; +} + +class SelectAllCommand extends EditorOrNativeTextInputCommand { + constructor() { + super({ + id: 'editor.action.selectAll', + precondition: EditorContextKeys.textInputFocus, + kbOpts: { + weight: CORE_WEIGHT, + kbExpr: null, + primary: KeyMod.CtrlCmd | KeyCode.KEY_A + }, + menuOpts: [{ + menuId: MenuId.MenubarSelectionMenu, + group: '1_basic', + title: nls.localize({ key: 'miSelectAll', comment: ['&& denotes a mnemonic'] }, "&&Select All"), + order: 1 + }, { + menuId: MenuId.CommandPalette, + group: '', + title: nls.localize('selectAll', "Select All"), + order: 1 + }] + }); + } + public runDOMCommand(): void { + document.execCommand('selectAll'); + } + public runEditorCommand(accessor: ServicesAccessor, editor: ICodeEditor, args: any): void { + args = args || {}; + args.source = 'keyboard'; + CoreNavigationCommands.SelectAll.runEditorCommand(accessor, editor, args); + } +} + +class UndoCommand extends EditorOrNativeTextInputCommand { + public runDOMCommand(): void { + document.execCommand('undo'); + } + public runEditorCommand(accessor: ServicesAccessor | null, editor: ICodeEditor, args: any): void { + if (!editor.hasModel() || editor.getOption(EditorOption.readOnly) === true) { + return; + } + editor.getModel().undo(); + } +} + +class RedoCommand extends EditorOrNativeTextInputCommand { + public runDOMCommand(): void { + document.execCommand('redo'); + } + public runEditorCommand(accessor: ServicesAccessor | null, editor: ICodeEditor, args: any): void { + if (!editor.hasModel() || editor.getOption(EditorOption.readOnly) === true) { + return; + } + editor.getModel().redo(); + } +} + +function registerCommand(command: T): T { + command.register(); + return command; +} + export namespace CoreEditingCommands { export abstract class CoreEditingCommand extends EditorCommand { @@ -1659,62 +1756,53 @@ export namespace CoreEditingCommands { } }); -} + export const Undo: UndoCommand = registerCommand(new UndoCommand({ + id: 'undo', + precondition: EditorContextKeys.writable, + kbOpts: { + weight: CORE_WEIGHT, + kbExpr: EditorContextKeys.textInputFocus, + primary: KeyMod.CtrlCmd | KeyCode.KEY_Z + }, + menuOpts: [{ + menuId: MenuId.MenubarEditMenu, + group: '1_do', + title: nls.localize({ key: 'miUndo', comment: ['&& denotes a mnemonic'] }, "&&Undo"), + order: 1 + }, { + menuId: MenuId.CommandPalette, + group: '', + title: nls.localize('undo', "Undo"), + order: 1 + }] + })); -function registerCommand(command: Command) { - command.register(); -} + export const DefaultUndo: UndoCommand = registerCommand(new UndoCommand({ id: 'default:undo', precondition: EditorContextKeys.writable })); -/** - * A command that will: - * 1. invoke a command on the focused editor. - * 2. otherwise, invoke a browser built-in command on the `activeElement`. - * 3. otherwise, invoke a command on the workbench active editor. - */ -class EditorOrNativeTextInputCommand extends Command { + export const Redo: RedoCommand = registerCommand(new RedoCommand({ + id: 'redo', + precondition: EditorContextKeys.writable, + kbOpts: { + weight: CORE_WEIGHT, + kbExpr: EditorContextKeys.textInputFocus, + primary: KeyMod.CtrlCmd | KeyCode.KEY_Y, + secondary: [KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.KEY_Z], + mac: { primary: KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.KEY_Z } + }, + menuOpts: [{ + menuId: MenuId.MenubarEditMenu, + group: '1_do', + title: nls.localize({ key: 'miRedo', comment: ['&& denotes a mnemonic'] }, "&&Redo"), + order: 2 + }, { + menuId: MenuId.CommandPalette, + group: '', + title: nls.localize('redo', "Redo"), + order: 1 + }] + })); - private readonly _editorHandler: string | EditorCommand; - private readonly _inputHandler: string; - - constructor(opts: ICommandOptions & { editorHandler: string | EditorCommand; inputHandler: string; }) { - super(opts); - this._editorHandler = opts.editorHandler; - this._inputHandler = opts.inputHandler; - } - - public runCommand(accessor: ServicesAccessor, args: any): void { - - const focusedEditor = accessor.get(ICodeEditorService).getFocusedCodeEditor(); - // Only if editor text focus (i.e. not if editor has widget focus). - if (focusedEditor && focusedEditor.hasTextFocus()) { - return this._runEditorHandler(accessor, focusedEditor, args); - } - - // Ignore this action when user is focused on an element that allows for entering text - const activeElement = document.activeElement; - if (activeElement && ['input', 'textarea'].indexOf(activeElement.tagName.toLowerCase()) >= 0) { - document.execCommand(this._inputHandler); - return; - } - - // Redirecting to active editor - const activeEditor = accessor.get(ICodeEditorService).getActiveCodeEditor(); - if (activeEditor) { - activeEditor.focus(); - return this._runEditorHandler(accessor, activeEditor, args); - } - } - - private _runEditorHandler(accessor: ServicesAccessor, editor: ICodeEditor, args: any): void { - const HANDLER = this._editorHandler; - if (typeof HANDLER === 'string') { - editor.trigger('keyboard', HANDLER, args); - } else { - args = args || {}; - args.source = 'keyboard'; - HANDLER.runEditorCommand(accessor, editor, args); - } - } + export const DefaultRedo: RedoCommand = registerCommand(new RedoCommand({ id: 'default:redo', precondition: EditorContextKeys.writable })); } /** @@ -1743,78 +1831,7 @@ class EditorHandlerCommand extends Command { } } -registerCommand(new EditorOrNativeTextInputCommand({ - editorHandler: CoreNavigationCommands.SelectAll, - inputHandler: 'selectAll', - id: 'editor.action.selectAll', - precondition: EditorContextKeys.textInputFocus, - kbOpts: { - weight: CORE_WEIGHT, - kbExpr: null, - primary: KeyMod.CtrlCmd | KeyCode.KEY_A - }, - menuOpts: [{ - menuId: MenuId.MenubarSelectionMenu, - group: '1_basic', - title: nls.localize({ key: 'miSelectAll', comment: ['&& denotes a mnemonic'] }, "&&Select All"), - order: 1 - }, { - menuId: MenuId.CommandPalette, - group: '', - title: nls.localize('selectAll', "Select All"), - order: 1 - }] -})); - -registerCommand(new EditorOrNativeTextInputCommand({ - editorHandler: Handler.Undo, - inputHandler: 'undo', - id: Handler.Undo, - precondition: EditorContextKeys.writable, - kbOpts: { - weight: CORE_WEIGHT, - kbExpr: EditorContextKeys.textInputFocus, - primary: KeyMod.CtrlCmd | KeyCode.KEY_Z - }, - menuOpts: [{ - menuId: MenuId.MenubarEditMenu, - group: '1_do', - title: nls.localize({ key: 'miUndo', comment: ['&& denotes a mnemonic'] }, "&&Undo"), - order: 1 - }, { - menuId: MenuId.CommandPalette, - group: '', - title: nls.localize('undo', "Undo"), - order: 1 - }] -})); -registerCommand(new EditorHandlerCommand('default:' + Handler.Undo, Handler.Undo)); - -registerCommand(new EditorOrNativeTextInputCommand({ - editorHandler: Handler.Redo, - inputHandler: 'redo', - id: Handler.Redo, - precondition: EditorContextKeys.writable, - kbOpts: { - weight: CORE_WEIGHT, - kbExpr: EditorContextKeys.textInputFocus, - primary: KeyMod.CtrlCmd | KeyCode.KEY_Y, - secondary: [KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.KEY_Z], - mac: { primary: KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.KEY_Z } - }, - menuOpts: [{ - menuId: MenuId.MenubarEditMenu, - group: '1_do', - title: nls.localize({ key: 'miRedo', comment: ['&& denotes a mnemonic'] }, "&&Redo"), - order: 2 - }, { - menuId: MenuId.CommandPalette, - group: '', - title: nls.localize('redo', "Redo"), - order: 1 - }] -})); -registerCommand(new EditorHandlerCommand('default:' + Handler.Redo, Handler.Redo)); +registerCommand(new SelectAllCommand()); function registerOverwritableCommand(handlerId: string, description?: ICommandHandlerDescription): void { registerCommand(new EditorHandlerCommand('default:' + handlerId, handlerId)); diff --git a/src/vs/editor/browser/widget/codeEditorWidget.ts b/src/vs/editor/browser/widget/codeEditorWidget.ts index e5c63003465..5262d8003b9 100644 --- a/src/vs/editor/browser/widget/codeEditorWidget.ts +++ b/src/vs/editor/browser/widget/codeEditorWidget.ts @@ -1543,11 +1543,18 @@ export class CodeEditorWidget extends Disposable implements editorBrowser.ICodeE }; } + const onDidChangeTextFocus = (textFocus: boolean) => { + if (this._modelData) { + this._modelData.cursor.setHasFocus(textFocus); + } + this._editorTextFocus.setValue(textFocus); + }; + const viewOutgoingEvents = new ViewOutgoingEvents(viewModel); viewOutgoingEvents.onDidContentSizeChange = (e) => this._onDidContentSizeChange.fire(e); viewOutgoingEvents.onDidScroll = (e) => this._onDidScrollChange.fire(e); - viewOutgoingEvents.onDidGainFocus = () => this._editorTextFocus.setValue(true); - viewOutgoingEvents.onDidLoseFocus = () => this._editorTextFocus.setValue(false); + viewOutgoingEvents.onDidGainFocus = () => onDidChangeTextFocus(true); + viewOutgoingEvents.onDidLoseFocus = () => onDidChangeTextFocus(false); viewOutgoingEvents.onContextMenu = (e) => this._onContextMenu.fire(e); viewOutgoingEvents.onMouseDown = (e) => this._onMouseDown.fire(e); viewOutgoingEvents.onMouseUp = (e) => this._onMouseUp.fire(e); diff --git a/src/vs/editor/common/controller/cursor.ts b/src/vs/editor/common/controller/cursor.ts index 82204018f2c..cd488b900ac 100644 --- a/src/vs/editor/common/controller/cursor.ts +++ b/src/vs/editor/common/controller/cursor.ts @@ -16,7 +16,7 @@ import { Range, IRange } from 'vs/editor/common/core/range'; import { ISelection, Selection, SelectionDirection } from 'vs/editor/common/core/selection'; import * as editorCommon from 'vs/editor/common/editorCommon'; import { ITextModel, TrackedRangeStickiness, IModelDeltaDecoration, ICursorStateComputer, IIdentifiedSingleEditOperation, IValidEditOperation } from 'vs/editor/common/model'; -import { RawContentChangedType } from 'vs/editor/common/model/textModelEvents'; +import { RawContentChangedType, ModelRawContentChangedEvent } from 'vs/editor/common/model/textModelEvents'; import * as viewEvents from 'vs/editor/common/view/viewEvents'; import { IViewModel } from 'vs/editor/common/viewModel/viewModel'; import { dispose } from 'vs/base/common/lifecycle'; @@ -186,6 +186,7 @@ export class Cursor extends viewEvents.ViewEventEmitter implements ICursors { public context: CursorContext; private _cursors: CursorCollection; + private _hasFocus: boolean; private _isHandling: boolean; private _isDoingComposition: boolean; private _selectionsWhenCompositionStarted: Selection[] | null; @@ -202,6 +203,7 @@ export class Cursor extends viewEvents.ViewEventEmitter implements ICursors { this.context = new CursorContext(this._configuration, this._model, this._viewModel); this._cursors = new CursorCollection(this.context); + this._hasFocus = false; this._isHandling = false; this._isDoingComposition = false; this._selectionsWhenCompositionStarted = null; @@ -215,8 +217,7 @@ export class Cursor extends viewEvents.ViewEventEmitter implements ICursors { return; } - let hadFlushEvent = e.containsEvent(RawContentChangedType.Flush); - this._onModelContentChanged(hadFlushEvent); + this._onModelContentChanged(e); })); this._register(viewModel.addEventListener((events: viewEvents.ViewEvent[]) => { @@ -264,6 +265,10 @@ export class Cursor extends viewEvents.ViewEventEmitter implements ICursors { super.dispose(); } + public setHasFocus(hasFocus: boolean): void { + this._hasFocus = hasFocus; + } + private _validateAutoClosedActions(): void { if (this._autoClosedActions.length > 0) { let selections: Range[] = this._cursors.getSelections(); @@ -392,8 +397,9 @@ export class Cursor extends viewEvents.ViewEventEmitter implements ICursors { this.reveal('restoreState', true, RevealTarget.Primary, editorCommon.ScrollType.Immediate); } - private _onModelContentChanged(hadFlushEvent: boolean): void { + private _onModelContentChanged(e: ModelRawContentChangedEvent): void { + const hadFlushEvent = e.containsEvent(RawContentChangedType.Flush); this._prevEditOperationType = EditOperationType.Other; if (hadFlushEvent) { @@ -403,8 +409,13 @@ export class Cursor extends viewEvents.ViewEventEmitter implements ICursors { this._validateAutoClosedActions(); this._emitStateChangedIfNecessary('model', CursorChangeReason.ContentFlush, null); } else { - const selectionsFromMarkers = this._cursors.readSelectionFromMarkers(); - this.setStates('modelChange', CursorChangeReason.RecoverFromMarkers, CursorState.fromModelSelections(selectionsFromMarkers)); + if (this._hasFocus && e.resultingSelection && e.resultingSelection.length > 0) { + const cursorState = CursorState.fromModelSelections(e.resultingSelection); + this.setStates('modelChange', e.isUndoing ? CursorChangeReason.Undo : e.isRedoing ? CursorChangeReason.Redo : CursorChangeReason.RecoverFromMarkers, cursorState); + } else { + const selectionsFromMarkers = this._cursors.readSelectionFromMarkers(); + this.setStates('modelChange', CursorChangeReason.RecoverFromMarkers, CursorState.fromModelSelections(selectionsFromMarkers)); + } } } @@ -704,11 +715,7 @@ export class Cursor extends viewEvents.ViewEventEmitter implements ICursors { const oldState = new CursorModelState(this._model, this); let cursorChangeReason = CursorChangeReason.NotSet; - if (handlerId !== H.Undo && handlerId !== H.Redo) { - // TODO@Alex: if the undo/redo stack contains non-null selections - // it would also be OK to stop tracking selections here - this._cursors.stopTrackingSelections(); - } + this._cursors.stopTrackingSelections(); // ensure valid state on all cursors this._cursors.ensureValidState(); @@ -734,16 +741,6 @@ export class Cursor extends viewEvents.ViewEventEmitter implements ICursors { this._cut(); break; - case H.Undo: - cursorChangeReason = CursorChangeReason.Undo; - this._interpretCommandResult(this._model.undo()); - break; - - case H.Redo: - cursorChangeReason = CursorChangeReason.Redo; - this._interpretCommandResult(this._model.redo()); - break; - case H.ExecuteCommand: this._externalExecuteCommand(payload); break; @@ -762,9 +759,7 @@ export class Cursor extends viewEvents.ViewEventEmitter implements ICursors { this._isHandling = false; - if (handlerId !== H.Undo && handlerId !== H.Redo) { - this._cursors.startTrackingSelections(); - } + this._cursors.startTrackingSelections(); this._validateAutoClosedActions(); diff --git a/src/vs/editor/common/editorCommon.ts b/src/vs/editor/common/editorCommon.ts index fca67398738..e176e90ede3 100644 --- a/src/vs/editor/common/editorCommon.ts +++ b/src/vs/editor/common/editorCommon.ts @@ -678,9 +678,5 @@ export const Handler = { CompositionStart: 'compositionStart', CompositionEnd: 'compositionEnd', Paste: 'paste', - Cut: 'cut', - - Undo: 'undo', - Redo: 'redo', }; diff --git a/src/vs/editor/common/model.ts b/src/vs/editor/common/model.ts index 566aca3d7a1..644a224686f 100644 --- a/src/vs/editor/common/model.ts +++ b/src/vs/editor/common/model.ts @@ -379,6 +379,13 @@ export interface IValidEditOperation { forceMoveMarkers: boolean; } +/** + * @internal + */ +export interface IValidEditOperations { + operations: IValidEditOperation[]; +} + /** * A callback that can compute the cursor state after applying a series of edit operations. */ @@ -1086,18 +1093,28 @@ export interface ITextModel { */ applyEdits(operations: IIdentifiedSingleEditOperation[]): IValidEditOperation[]; + /** + * @internal + */ + _applyEdits(edits: IValidEditOperations[], isUndoing: boolean, isRedoing: boolean, resultingAlternativeVersionId: number, resultingSelection: Selection[] | null): IValidEditOperations[]; + /** * Change the end of line sequence without recording in the undo stack. * This can have dire consequences on the undo stack! See @pushEOL for the preferred way. */ setEOL(eol: EndOfLineSequence): void; + /** + * @internal + */ + _setEOL(eol: EndOfLineSequence, isUndoing: boolean, isRedoing: boolean, resultingAlternativeVersionId: number, resultingSelection: Selection[] | null): void; + /** * Undo edit operations until the first previous stop point created by `pushStackElement`. * The inverse edit operations will be pushed on the redo stack. * @internal */ - undo(): Selection[] | null; + undo(): void; /** * Is there anything in the undo stack? @@ -1110,7 +1127,7 @@ export interface ITextModel { * The inverse edit operations will be pushed on the undo stack. * @internal */ - redo(): Selection[] | null; + redo(): void; /** * Is there anything in the redo stack? diff --git a/src/vs/editor/common/model/editStack.ts b/src/vs/editor/common/model/editStack.ts index a870a0822d1..1e6c83563d8 100644 --- a/src/vs/editor/common/model/editStack.ts +++ b/src/vs/editor/common/model/editStack.ts @@ -3,61 +3,72 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import * as nls from 'vs/nls'; import { onUnexpectedError } from 'vs/base/common/errors'; import { Selection } from 'vs/editor/common/core/selection'; -import { EndOfLineSequence, ICursorStateComputer, IIdentifiedSingleEditOperation, IValidEditOperation } from 'vs/editor/common/model'; +import { EndOfLineSequence, ICursorStateComputer, IIdentifiedSingleEditOperation, IValidEditOperation, ITextModel, IValidEditOperations } from 'vs/editor/common/model'; import { TextModel } from 'vs/editor/common/model/textModel'; +import { IUndoRedoService, IUndoRedoElement, IUndoRedoContext } from 'vs/platform/undoRedo/common/undoRedo'; +import { URI } from 'vs/base/common/uri'; -interface IEditOperation { - operations: IValidEditOperation[]; -} +class EditStackElement implements IUndoRedoElement { -interface IStackElement { - readonly beforeVersionId: number; - readonly beforeCursorState: Selection[] | null; - readonly afterCursorState: Selection[] | null; - readonly afterVersionId: number; + public readonly label: string; + private _isOpen: boolean; + private readonly _model: TextModel; + private readonly _beforeVersionId: number; + private readonly _beforeCursorState: Selection[]; + private _afterVersionId: number; + private _afterCursorState: Selection[] | null; + private _edits: IValidEditOperations[]; - undo(model: TextModel): void; - redo(model: TextModel): void; -} - -class EditStackElement implements IStackElement { - public readonly beforeVersionId: number; - public readonly beforeCursorState: Selection[]; - public afterCursorState: Selection[] | null; - public afterVersionId: number; - - public editOperations: IEditOperation[]; - - constructor(beforeVersionId: number, beforeCursorState: Selection[]) { - this.beforeVersionId = beforeVersionId; - this.beforeCursorState = beforeCursorState; - this.afterCursorState = null; - this.afterVersionId = -1; - this.editOperations = []; + public get resources(): readonly URI[] { + return [this._model.uri]; } - public undo(model: TextModel): void { - // Apply all operations in reverse order - for (let i = this.editOperations.length - 1; i >= 0; i--) { - this.editOperations[i] = { - operations: model.applyEdits(this.editOperations[i].operations) - }; - } + constructor(model: TextModel, beforeVersionId: number, beforeCursorState: Selection[], afterVersionId: number, afterCursorState: Selection[] | null, operations: IValidEditOperation[]) { + this.label = nls.localize('edit', "Typing"); + this._isOpen = true; + this._model = model; + this._beforeVersionId = beforeVersionId; + this._beforeCursorState = beforeCursorState; + this._afterVersionId = afterVersionId; + this._afterCursorState = afterCursorState; + this._edits = [{ operations: operations }]; } - public redo(model: TextModel): void { - // Apply all operations - for (let i = 0; i < this.editOperations.length; i++) { - this.editOperations[i] = { - operations: model.applyEdits(this.editOperations[i].operations) - }; - } + public isOpen(): boolean { + return this._isOpen; + } + + public append(operations: IValidEditOperation[], afterVersionId: number, afterCursorState: Selection[] | null): void { + this._edits.push({ operations: operations }); + this._afterVersionId = afterVersionId; + this._afterCursorState = afterCursorState; + } + + public close(): void { + this._isOpen = false; + } + + undo(ctx: IUndoRedoContext): void { + this._isOpen = false; + this._edits.reverse(); + this._edits = this._model._applyEdits(this._edits, true, false, this._beforeVersionId, this._beforeCursorState); + } + + redo(ctx: IUndoRedoContext): void { + this._isOpen = false; + this._edits.reverse(); + this._edits = this._model._applyEdits(this._edits, false, true, this._afterVersionId, this._afterCursorState); + } + + invalidate(resource: URI): void { + // nothing to do } } -function getModelEOL(model: TextModel): EndOfLineSequence { +function getModelEOL(model: ITextModel): EndOfLineSequence { const eol = model.getEOL(); if (eol === '\n') { return EndOfLineSequence.LF; @@ -66,32 +77,40 @@ function getModelEOL(model: TextModel): EndOfLineSequence { } } -class EOLStackElement implements IStackElement { - public readonly beforeVersionId: number; - public readonly beforeCursorState: Selection[] | null; - public readonly afterCursorState: Selection[] | null; - public afterVersionId: number; +class EOLStackElement implements IUndoRedoElement { - public eol: EndOfLineSequence; + public readonly label: string; + private readonly _model: TextModel; + private readonly _beforeVersionId: number; + private readonly _afterVersionId: number; + private _eol: EndOfLineSequence; - constructor(beforeVersionId: number, setEOL: EndOfLineSequence) { - this.beforeVersionId = beforeVersionId; - this.beforeCursorState = null; - this.afterCursorState = null; - this.afterVersionId = -1; - this.eol = setEOL; + public get resources(): readonly URI[] { + return [this._model.uri]; } - public undo(model: TextModel): void { - let redoEOL = getModelEOL(model); - model.setEOL(this.eol); - this.eol = redoEOL; + constructor(model: TextModel, beforeVersionId: number, afterVersionId: number, eol: EndOfLineSequence) { + this.label = nls.localize('eol', "Change End Of Line Sequence"); + this._model = model; + this._beforeVersionId = beforeVersionId; + this._afterVersionId = afterVersionId; + this._eol = eol; } - public redo(model: TextModel): void { - let undoEOL = getModelEOL(model); - model.setEOL(this.eol); - this.eol = undoEOL; + undo(ctx: IUndoRedoContext): void { + const redoEOL = getModelEOL(this._model); + this._model._setEOL(this._eol, true, false, this._beforeVersionId, null); + this._eol = redoEOL; + } + + redo(ctx: IUndoRedoContext): void { + const undoEOL = getModelEOL(this._model); + this._model._setEOL(this._eol, false, true, this._afterVersionId, null); + this._eol = undoEOL; + } + + invalidate(resource: URI): void { + // nothing to do } } @@ -102,76 +121,52 @@ export interface IUndoRedoResult { export class EditStack { - private readonly model: TextModel; - private currentOpenStackElement: IStackElement | null; - private past: IStackElement[]; - private future: IStackElement[]; + private readonly _model: TextModel; + private readonly _undoRedoService: IUndoRedoService; - constructor(model: TextModel) { - this.model = model; - this.currentOpenStackElement = null; - this.past = []; - this.future = []; + constructor(model: TextModel, undoRedoService: IUndoRedoService) { + this._model = model; + this._undoRedoService = undoRedoService; } public pushStackElement(): void { - if (this.currentOpenStackElement !== null) { - this.past.push(this.currentOpenStackElement); - this.currentOpenStackElement = null; + const lastElement = this._undoRedoService.getLastElement(this._model.uri); + if (lastElement && lastElement instanceof EditStackElement) { + lastElement.close(); } } public clear(): void { - this.currentOpenStackElement = null; - this.past = []; - this.future = []; + this._undoRedoService.removeElements(this._model.uri); } public pushEOL(eol: EndOfLineSequence): void { - // No support for parallel universes :( - this.future = []; + const beforeVersionId = this._model.getAlternativeVersionId(); + const inverseEOL = getModelEOL(this._model); + this._model.setEOL(eol); + const afterVersionId = this._model.getAlternativeVersionId(); - if (this.currentOpenStackElement) { - this.pushStackElement(); + const lastElement = this._undoRedoService.getLastElement(this._model.uri); + if (lastElement && lastElement instanceof EditStackElement) { + lastElement.close(); } - - const prevEOL = getModelEOL(this.model); - let stackElement = new EOLStackElement(this.model.getAlternativeVersionId(), prevEOL); - - this.model.setEOL(eol); - - stackElement.afterVersionId = this.model.getVersionId(); - this.currentOpenStackElement = stackElement; - this.pushStackElement(); + this._undoRedoService.pushElement(new EOLStackElement(this._model, inverseEOL, beforeVersionId, afterVersionId)); } public pushEditOperation(beforeCursorState: Selection[], editOperations: IIdentifiedSingleEditOperation[], cursorStateComputer: ICursorStateComputer | null): Selection[] | null { - // No support for parallel universes :( - this.future = []; + const beforeVersionId = this._model.getAlternativeVersionId(); + const inverseEditOperations = this._model.applyEdits(editOperations); + const afterVersionId = this._model.getAlternativeVersionId(); + const afterCursorState = EditStack._computeCursorState(cursorStateComputer, inverseEditOperations); - let stackElement: EditStackElement | null = null; - - if (this.currentOpenStackElement) { - if (this.currentOpenStackElement instanceof EditStackElement) { - stackElement = this.currentOpenStackElement; - } else { - this.pushStackElement(); - } + const lastElement = this._undoRedoService.getLastElement(this._model.uri); + if (lastElement && lastElement instanceof EditStackElement && lastElement.isOpen()) { + lastElement.append(inverseEditOperations, afterVersionId, afterCursorState); + } else { + this._undoRedoService.pushElement(new EditStackElement(this._model, beforeVersionId, beforeCursorState, afterVersionId, afterCursorState, inverseEditOperations)); } - if (!this.currentOpenStackElement) { - stackElement = new EditStackElement(this.model.getAlternativeVersionId(), beforeCursorState); - this.currentOpenStackElement = stackElement; - } - - const inverseEditOperation: IEditOperation = { - operations: this.model.applyEdits(editOperations) - }; - - stackElement!.editOperations.push(inverseEditOperation); - stackElement!.afterCursorState = EditStack._computeCursorState(cursorStateComputer, inverseEditOperation.operations); - stackElement!.afterVersionId = this.model.getVersionId(); - return stackElement!.afterCursorState; + return afterCursorState; } private static _computeCursorState(cursorStateComputer: ICursorStateComputer | null, inverseEditOperations: IValidEditOperation[]): Selection[] | null { @@ -182,62 +177,4 @@ export class EditStack { return null; } } - - public undo(): IUndoRedoResult | null { - - this.pushStackElement(); - - if (this.past.length > 0) { - const pastStackElement = this.past.pop()!; - - try { - pastStackElement.undo(this.model); - } catch (e) { - onUnexpectedError(e); - this.clear(); - return null; - } - - this.future.push(pastStackElement); - - return { - selections: pastStackElement.beforeCursorState, - recordedVersionId: pastStackElement.beforeVersionId - }; - } - - return null; - } - - public canUndo(): boolean { - return (this.past.length > 0) || this.currentOpenStackElement !== null; - } - - public redo(): IUndoRedoResult | null { - - if (this.future.length > 0) { - const futureStackElement = this.future.pop()!; - - try { - futureStackElement.redo(this.model); - } catch (e) { - onUnexpectedError(e); - this.clear(); - return null; - } - - this.past.push(futureStackElement); - - return { - selections: futureStackElement.afterCursorState, - recordedVersionId: futureStackElement.afterVersionId - }; - } - - return null; - } - - public canRedo(): boolean { - return (this.future.length > 0); - } } diff --git a/src/vs/editor/common/model/textModel.ts b/src/vs/editor/common/model/textModel.ts index ae8071f7e51..9c999e23ce5 100644 --- a/src/vs/editor/common/model/textModel.ts +++ b/src/vs/editor/common/model/textModel.ts @@ -255,6 +255,7 @@ export class TextModel extends Disposable implements model.ITextModel { public readonly id: string; public readonly isForSimpleWidget: boolean; private readonly _associatedResource: URI; + private readonly _undoRedoService: IUndoRedoService; private _attachedEditorCount: number; private _buffer: model.ITextBuffer; private _options: model.TextModelResolvedOptions; @@ -270,7 +271,7 @@ export class TextModel extends Disposable implements model.ITextModel { private readonly _isTooLargeForTokenization: boolean; //#region Editing - private _commandManager: EditStack; + private readonly _commandManager: EditStack; private _isUndoing: boolean; private _isRedoing: boolean; private _trimAutoWhitespaceLines: number[] | null; @@ -313,6 +314,7 @@ export class TextModel extends Disposable implements model.ITextModel { } else { this._associatedResource = associatedResource; } + this._undoRedoService = undoRedoService; this._attachedEditorCount = 0; this._buffer = createTextBuffer(source, creationOptions.defaultEOL); @@ -355,7 +357,7 @@ export class TextModel extends Disposable implements model.ITextModel { this._decorations = Object.create(null); this._decorationsTree = new DecorationsTrees(); - this._commandManager = new EditStack(this); + this._commandManager = new EditStack(this, undoRedoService); this._isUndoing = false; this._isRedoing = false; this._trimAutoWhitespaceLines = null; @@ -370,6 +372,7 @@ export class TextModel extends Disposable implements model.ITextModel { this._onWillDispose.fire(); this._languageRegistryListener.dispose(); this._tokenization.dispose(); + this._undoRedoService.removeElements(this.uri); this._isDisposed = true; super.dispose(); this._isDisposing = false; @@ -444,7 +447,7 @@ export class TextModel extends Disposable implements model.ITextModel { this._decorationsTree = new DecorationsTrees(); // Destroy my edit history and settings - this._commandManager = new EditStack(this); + this._commandManager.clear(); this._trimAutoWhitespaceLines = null; this._emitContentChangedEvent( @@ -491,6 +494,21 @@ export class TextModel extends Disposable implements model.ITextModel { ); } + _setEOL(eol: model.EndOfLineSequence, isUndoing: boolean, isRedoing: boolean, resultingAlternativeVersionId: number, resultingSelection: Selection[] | null): void { + try { + this._onDidChangeDecorations.beginDeferredEmit(); + this._eventEmitter.beginDeferredEmit(); + this._isUndoing = isUndoing; + this._isRedoing = isRedoing; + this.setEOL(eol); + this._overwriteAlternativeVersionId(resultingAlternativeVersionId); + } finally { + this._isUndoing = false; + this._eventEmitter.endDeferredEmit(resultingSelection); + this._onDidChangeDecorations.endDeferredEmit(); + } + } + private _onBeforeEOLChange(): void { // Ensure all decorations get their `range` set. const versionId = this.getVersionId(); @@ -1280,18 +1298,37 @@ export class TextModel extends Disposable implements model.ITextModel { return this._commandManager.pushEditOperation(beforeCursorState, editOperations, cursorStateComputer); } + _applyEdits(edits: model.IValidEditOperations[], isUndoing: boolean, isRedoing: boolean, resultingAlternativeVersionId: number, resultingSelection: Selection[] | null): model.IValidEditOperations[] { + try { + this._onDidChangeDecorations.beginDeferredEmit(); + this._eventEmitter.beginDeferredEmit(); + this._isUndoing = isUndoing; + this._isRedoing = isRedoing; + let reverseEdits: model.IValidEditOperations[] = []; + for (let i = 0, len = edits.length; i < len; i++) { + reverseEdits[i] = { operations: this.applyEdits(edits[i].operations) }; + } + this._overwriteAlternativeVersionId(resultingAlternativeVersionId); + return reverseEdits; + } finally { + this._isUndoing = false; + this._eventEmitter.endDeferredEmit(resultingSelection); + this._onDidChangeDecorations.endDeferredEmit(); + } + } + public applyEdits(rawOperations: model.IIdentifiedSingleEditOperation[]): model.IValidEditOperation[] { try { this._onDidChangeDecorations.beginDeferredEmit(); this._eventEmitter.beginDeferredEmit(); - return this._applyEdits(this._validateEditOperations(rawOperations)); + return this._doApplyEdits(this._validateEditOperations(rawOperations)); } finally { this._eventEmitter.endDeferredEmit(); this._onDidChangeDecorations.endDeferredEmit(); } } - private _applyEdits(rawOperations: model.ValidAnnotatedEditOperation[]): model.IValidEditOperation[] { + private _doApplyEdits(rawOperations: model.ValidAnnotatedEditOperation[]): model.IValidEditOperation[] { const oldLineCount = this._buffer.getLineCount(); const result = this._buffer.applyEdits(rawOperations, this._options.trimAutoWhitespace); @@ -1372,62 +1409,20 @@ export class TextModel extends Disposable implements model.ITextModel { return result.reverseEdits; } - private _undo(): Selection[] | null { - this._isUndoing = true; - let r = this._commandManager.undo(); - this._isUndoing = false; - - if (!r) { - return null; - } - - this._overwriteAlternativeVersionId(r.recordedVersionId); - - return r.selections; - } - - public undo(): Selection[] | null { - try { - this._onDidChangeDecorations.beginDeferredEmit(); - this._eventEmitter.beginDeferredEmit(); - return this._undo(); - } finally { - this._eventEmitter.endDeferredEmit(); - this._onDidChangeDecorations.endDeferredEmit(); - } + public undo(): void { + this._undoRedoService.undo(this.uri); } public canUndo(): boolean { - return this._commandManager.canUndo(); + return this._undoRedoService.canUndo(this.uri); } - private _redo(): Selection[] | null { - this._isRedoing = true; - let r = this._commandManager.redo(); - this._isRedoing = false; - - if (!r) { - return null; - } - - this._overwriteAlternativeVersionId(r.recordedVersionId); - - return r.selections; - } - - public redo(): Selection[] | null { - try { - this._onDidChangeDecorations.beginDeferredEmit(); - this._eventEmitter.beginDeferredEmit(); - return this._redo(); - } finally { - this._eventEmitter.endDeferredEmit(); - this._onDidChangeDecorations.endDeferredEmit(); - } + public redo(): void { + this._undoRedoService.redo(this.uri); } public canRedo(): boolean { - return this._commandManager.canRedo(); + return this._undoRedoService.canRedo(this.uri); } //#endregion @@ -3199,10 +3194,11 @@ export class DidChangeContentEmitter extends Disposable { this._deferredCnt++; } - public endDeferredEmit(): void { + public endDeferredEmit(resultingSelection: Selection[] | null = null): void { this._deferredCnt--; if (this._deferredCnt === 0) { if (this._deferredEvent !== null) { + this._deferredEvent.rawContentChangedEvent.resultingSelection = resultingSelection; const e = this._deferredEvent; this._deferredEvent = null; this._fastEmitter.fire(e); diff --git a/src/vs/editor/common/model/textModelEvents.ts b/src/vs/editor/common/model/textModelEvents.ts index a09a1abf0fe..17f45db65ad 100644 --- a/src/vs/editor/common/model/textModelEvents.ts +++ b/src/vs/editor/common/model/textModelEvents.ts @@ -4,6 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { IRange } from 'vs/editor/common/core/range'; +import { Selection } from 'vs/editor/common/core/selection'; /** * An event describing that the current mode associated with a model has changed. @@ -225,11 +226,14 @@ export class ModelRawContentChangedEvent { */ public readonly isRedoing: boolean; + public resultingSelection: Selection[] | null; + constructor(changes: ModelRawChange[], versionId: number, isUndoing: boolean, isRedoing: boolean) { this.changes = changes; this.versionId = versionId; this.isUndoing = isUndoing; this.isRedoing = isRedoing; + this.resultingSelection = null; } public containsEvent(type: RawContentChangedType): boolean { diff --git a/src/vs/editor/contrib/linesOperations/test/linesOperations.test.ts b/src/vs/editor/contrib/linesOperations/test/linesOperations.test.ts index 64b26b23b76..6090bd19e7e 100644 --- a/src/vs/editor/contrib/linesOperations/test/linesOperations.test.ts +++ b/src/vs/editor/contrib/linesOperations/test/linesOperations.test.ts @@ -317,7 +317,7 @@ suite('Editor Contrib - Line Operations', () => { assert.equal(model.getLineContent(1), 'one'); assert.deepEqual(editor.getSelection(), new Selection(1, 1, 1, 1)); - editor.trigger('keyboard', Handler.Undo, {}); + CoreEditingCommands.Undo.runEditorCommand(null, editor, null); assert.equal(model.getLineContent(1), 'Typing some text here on line one'); assert.deepEqual(editor.getSelection(), new Selection(1, 31, 1, 31)); }); @@ -447,7 +447,7 @@ suite('Editor Contrib - Line Operations', () => { assert.equal(model.getLineContent(1), 'hello my dear world'); assert.deepEqual(editor.getSelection(), new Selection(1, 14, 1, 14)); - editor.trigger('keyboard', Handler.Undo, {}); + CoreEditingCommands.Undo.runEditorCommand(null, editor, null); assert.equal(model.getLineContent(1), 'hello my dear'); assert.deepEqual(editor.getSelection(), new Selection(1, 14, 1, 14)); }); @@ -815,13 +815,13 @@ suite('Editor Contrib - Line Operations', () => { new Selection(2, 4, 2, 4) ]); - editor.trigger('tests', Handler.Undo, {}); + CoreEditingCommands.Undo.runEditorCommand(null, editor, null); assert.deepEqual(editor.getSelections(), [ new Selection(1, 3, 1, 3), new Selection(1, 6, 1, 6), new Selection(3, 4, 3, 4) ]); - editor.trigger('tests', Handler.Redo, {}); + CoreEditingCommands.Redo.runEditorCommand(null, editor, null); assert.deepEqual(editor.getSelections(), [ new Selection(1, 3, 1, 3), new Selection(2, 4, 2, 4) diff --git a/src/vs/editor/contrib/wordOperations/test/wordOperations.test.ts b/src/vs/editor/contrib/wordOperations/test/wordOperations.test.ts index 15ea81e6a44..2b1d99f2aa9 100644 --- a/src/vs/editor/contrib/wordOperations/test/wordOperations.test.ts +++ b/src/vs/editor/contrib/wordOperations/test/wordOperations.test.ts @@ -13,6 +13,7 @@ import { CursorWordEndLeft, CursorWordEndLeftSelect, CursorWordEndRight, CursorW import { withTestCodeEditor } from 'vs/editor/test/browser/testCodeEditor'; import { Handler } from 'vs/editor/common/editorCommon'; import { Cursor } from 'vs/editor/common/controller/cursor'; +import { CoreEditingCommands } from 'vs/editor/browser/controller/coreCommands'; suite('WordOperations', () => { @@ -216,7 +217,7 @@ suite('WordOperations', () => { assert.equal(editor.getValue(), 'foo qbar baz'); - cursorCommand(cursor, Handler.Undo, {}); + CoreEditingCommands.Undo.runEditorCommand(null, editor, null); assert.equal(editor.getValue(), 'foo bar baz'); }); }); diff --git a/src/vs/editor/test/browser/controller/cursor.test.ts b/src/vs/editor/test/browser/controller/cursor.test.ts index 6c6595c516e..fa60d5989ec 100644 --- a/src/vs/editor/test/browser/controller/cursor.test.ts +++ b/src/vs/editor/test/browser/controller/cursor.test.ts @@ -1240,22 +1240,22 @@ suite('Editor Controller - Regression tests', () => { CoreEditingCommands.DeleteLeft.runEditorCommand(null, editor, null); assert.equal(model.getValue(EndOfLinePreference.LF), 'x', 'assert9'); - cursorCommand(cursor, H.Undo, {}); + CoreEditingCommands.Undo.runEditorCommand(null, editor, null); assert.equal(model.getValue(EndOfLinePreference.LF), '\nx', 'assert10'); - cursorCommand(cursor, H.Undo, {}); + CoreEditingCommands.Undo.runEditorCommand(null, editor, null); assert.equal(model.getValue(EndOfLinePreference.LF), '\n\t\nx', 'assert11'); - cursorCommand(cursor, H.Undo, {}); + CoreEditingCommands.Undo.runEditorCommand(null, editor, null); assert.equal(model.getValue(EndOfLinePreference.LF), '\n\t\n\tx', 'assert12'); - cursorCommand(cursor, H.Redo, {}); + CoreEditingCommands.Redo.runEditorCommand(null, editor, null); assert.equal(model.getValue(EndOfLinePreference.LF), '\n\t\nx', 'assert13'); - cursorCommand(cursor, H.Redo, {}); + CoreEditingCommands.Redo.runEditorCommand(null, editor, null); assert.equal(model.getValue(EndOfLinePreference.LF), '\nx', 'assert14'); - cursorCommand(cursor, H.Redo, {}); + CoreEditingCommands.Redo.runEditorCommand(null, editor, null); assert.equal(model.getValue(EndOfLinePreference.LF), 'x', 'assert15'); }); @@ -1263,12 +1263,12 @@ suite('Editor Controller - Regression tests', () => { }); test('issue #23539: Setting model EOL isn\'t undoable', () => { - usingCursor({ - text: [ - 'Hello', - 'world' - ] - }, (model, cursor) => { + withTestCodeEditor([ + 'Hello', + 'world' + ], {}, (editor, cursor) => { + const model = editor.getModel()!; + assertCursor(cursor, new Position(1, 1)); model.setEOL(EndOfLineSequence.LF); assert.equal(model.getValue(), 'Hello\nworld'); @@ -1276,7 +1276,7 @@ suite('Editor Controller - Regression tests', () => { model.pushEOL(EndOfLineSequence.CRLF); assert.equal(model.getValue(), 'Hello\r\nworld'); - cursorCommand(cursor, H.Undo); + CoreEditingCommands.Undo.runEditorCommand(null, editor, null); assert.equal(model.getValue(), 'Hello\nworld'); }); }); @@ -1301,7 +1301,7 @@ suite('Editor Controller - Regression tests', () => { cursorCommand(cursor, H.Type, { text: '%' }, 'keyboard'); assert.equal(model.getValue(EndOfLinePreference.LF), '%\'%👁\'', 'assert1'); - cursorCommand(cursor, H.Undo, {}); + CoreEditingCommands.Undo.runEditorCommand(null, editor, null); assert.equal(model.getValue(EndOfLinePreference.LF), '\'👁\'', 'assert2'); }); @@ -1327,39 +1327,39 @@ suite('Editor Controller - Regression tests', () => { assert.equal(model.getLineContent(1), 'Hello world'); assertCursor(cursor, new Position(1, 12)); - cursorCommand(cursor, H.Undo, {}); + CoreEditingCommands.Undo.runEditorCommand(null, editor, null); assert.equal(model.getLineContent(1), 'Hello world '); assertCursor(cursor, new Position(1, 13)); - cursorCommand(cursor, H.Undo, {}); + CoreEditingCommands.Undo.runEditorCommand(null, editor, null); assert.equal(model.getLineContent(1), 'Hello world'); assertCursor(cursor, new Position(1, 12)); - cursorCommand(cursor, H.Undo, {}); + CoreEditingCommands.Undo.runEditorCommand(null, editor, null); assert.equal(model.getLineContent(1), 'Hello'); assertCursor(cursor, new Position(1, 6)); - cursorCommand(cursor, H.Undo, {}); + CoreEditingCommands.Undo.runEditorCommand(null, editor, null); assert.equal(model.getLineContent(1), ''); assertCursor(cursor, new Position(1, 1)); - cursorCommand(cursor, H.Redo, {}); + CoreEditingCommands.Redo.runEditorCommand(null, editor, null); assert.equal(model.getLineContent(1), 'Hello'); assertCursor(cursor, new Position(1, 6)); - cursorCommand(cursor, H.Redo, {}); + CoreEditingCommands.Redo.runEditorCommand(null, editor, null); assert.equal(model.getLineContent(1), 'Hello world'); assertCursor(cursor, new Position(1, 12)); - cursorCommand(cursor, H.Redo, {}); + CoreEditingCommands.Redo.runEditorCommand(null, editor, null); assert.equal(model.getLineContent(1), 'Hello world '); assertCursor(cursor, new Position(1, 13)); - cursorCommand(cursor, H.Redo, {}); + CoreEditingCommands.Redo.runEditorCommand(null, editor, null); assert.equal(model.getLineContent(1), 'Hello world'); assertCursor(cursor, new Position(1, 12)); - cursorCommand(cursor, H.Redo, {}); + CoreEditingCommands.Redo.runEditorCommand(null, editor, null); assert.equal(model.getLineContent(1), 'Hello world'); assertCursor(cursor, new Position(1, 12)); }); @@ -1735,21 +1735,21 @@ suite('Editor Controller - Regression tests', () => { '\t just some text' ].join('\n'), '001'); - cursorCommand(cursor, H.Undo); + CoreEditingCommands.Undo.runEditorCommand(null, editor, null); assert.equal(model.getValue(), [ ' some lines', ' and more lines', ' just some text', ].join('\n'), '002'); - cursorCommand(cursor, H.Undo); + CoreEditingCommands.Undo.runEditorCommand(null, editor, null); assert.equal(model.getValue(), [ 'some lines', 'and more lines', 'just some text', ].join('\n'), '003'); - cursorCommand(cursor, H.Undo); + CoreEditingCommands.Undo.runEditorCommand(null, editor, null); assert.equal(model.getValue(), [ 'some lines', 'and more lines', @@ -1935,10 +1935,8 @@ suite('Editor Controller - Regression tests', () => { }); test('issue #9675: Undo/Redo adds a stop in between CHN Characters', () => { - usingCursor({ - text: [ - ] - }, (model, cursor) => { + withTestCodeEditor([], {}, (editor, cursor) => { + const model = editor.getModel()!; assertCursor(cursor, new Position(1, 1)); // Typing sennsei in Japanese - Hiragana @@ -1957,7 +1955,7 @@ suite('Editor Controller - Regression tests', () => { assert.equal(model.getLineContent(1), 'せんせい'); assertCursor(cursor, new Position(1, 5)); - cursorCommand(cursor, H.Undo); + CoreEditingCommands.Undo.runEditorCommand(null, editor, null); assert.equal(model.getLineContent(1), ''); assertCursor(cursor, new Position(1, 1)); }); @@ -2138,7 +2136,7 @@ suite('Editor Controller - Regression tests', () => { }], () => [new Selection(1, 1, 1, 1)]); assert.equal(model.getValue(EndOfLinePreference.LF), 'Hello world!'); - cursorCommand(cursor, H.Undo, {}); + CoreEditingCommands.Undo.runEditorCommand(null, editor, null); assert.equal(model.getValue(EndOfLinePreference.LF), 'Hello world!'); }); @@ -2229,12 +2227,12 @@ suite('Editor Controller - Regression tests', () => { new Selection(1, 5, 1, 5), ]); - cursorCommand(cursor, H.Undo, null, 'keyboard'); + CoreEditingCommands.Undo.runEditorCommand(null, editor, null); assertCursor(cursor, [ new Selection(1, 4, 1, 4), ]); - cursorCommand(cursor, H.Redo, null, 'keyboard'); + CoreEditingCommands.Redo.runEditorCommand(null, editor, null); assertCursor(cursor, [ new Selection(1, 5, 1, 5), ]); @@ -2263,7 +2261,7 @@ suite('Editor Controller - Regression tests', () => { new Selection(1, 1, 1, 1), ]); - cursorCommand(cursor, H.Undo, null, 'keyboard'); + CoreEditingCommands.Undo.runEditorCommand(null, editor, null); assertCursor(cursor, [ new Selection(1, 1, 1, 1), ]); @@ -2378,49 +2376,49 @@ suite('Editor Controller - Cursor Configuration', () => { CoreNavigationCommands.MoveTo.runCoreEditorCommand(cursor, { position: new Position(2, 1) }); CoreEditingCommands.Tab.runEditorCommand(null, editor, null); assert.equal(model.getLineContent(2), ' My Second Line123'); - cursorCommand(cursor, H.Undo, null, 'keyboard'); + CoreEditingCommands.Undo.runEditorCommand(null, editor, null); // Tab on column 2 assert.equal(model.getLineContent(2), 'My Second Line123'); CoreNavigationCommands.MoveTo.runCoreEditorCommand(cursor, { position: new Position(2, 2) }); CoreEditingCommands.Tab.runEditorCommand(null, editor, null); assert.equal(model.getLineContent(2), 'M y Second Line123'); - cursorCommand(cursor, H.Undo, null, 'keyboard'); + CoreEditingCommands.Undo.runEditorCommand(null, editor, null); // Tab on column 3 assert.equal(model.getLineContent(2), 'My Second Line123'); CoreNavigationCommands.MoveTo.runCoreEditorCommand(cursor, { position: new Position(2, 3) }); CoreEditingCommands.Tab.runEditorCommand(null, editor, null); assert.equal(model.getLineContent(2), 'My Second Line123'); - cursorCommand(cursor, H.Undo, null, 'keyboard'); + CoreEditingCommands.Undo.runEditorCommand(null, editor, null); // Tab on column 4 assert.equal(model.getLineContent(2), 'My Second Line123'); CoreNavigationCommands.MoveTo.runCoreEditorCommand(cursor, { position: new Position(2, 4) }); CoreEditingCommands.Tab.runEditorCommand(null, editor, null); assert.equal(model.getLineContent(2), 'My Second Line123'); - cursorCommand(cursor, H.Undo, null, 'keyboard'); + CoreEditingCommands.Undo.runEditorCommand(null, editor, null); // Tab on column 5 assert.equal(model.getLineContent(2), 'My Second Line123'); CoreNavigationCommands.MoveTo.runCoreEditorCommand(cursor, { position: new Position(2, 5) }); CoreEditingCommands.Tab.runEditorCommand(null, editor, null); assert.equal(model.getLineContent(2), 'My S econd Line123'); - cursorCommand(cursor, H.Undo, null, 'keyboard'); + CoreEditingCommands.Undo.runEditorCommand(null, editor, null); // Tab on column 5 assert.equal(model.getLineContent(2), 'My Second Line123'); CoreNavigationCommands.MoveTo.runCoreEditorCommand(cursor, { position: new Position(2, 5) }); CoreEditingCommands.Tab.runEditorCommand(null, editor, null); assert.equal(model.getLineContent(2), 'My S econd Line123'); - cursorCommand(cursor, H.Undo, null, 'keyboard'); + CoreEditingCommands.Undo.runEditorCommand(null, editor, null); // Tab on column 13 assert.equal(model.getLineContent(2), 'My Second Line123'); CoreNavigationCommands.MoveTo.runCoreEditorCommand(cursor, { position: new Position(2, 13) }); CoreEditingCommands.Tab.runEditorCommand(null, editor, null); assert.equal(model.getLineContent(2), 'My Second Li ne123'); - cursorCommand(cursor, H.Undo, null, 'keyboard'); + CoreEditingCommands.Undo.runEditorCommand(null, editor, null); // Tab on column 14 assert.equal(model.getLineContent(2), 'My Second Line123'); @@ -2774,7 +2772,7 @@ suite('Editor Controller - Cursor Configuration', () => { assert.equal(model.getLineContent(2), 'a '); // Undo DeleteLeft - get us back to original indentation - cursorCommand(cursor, H.Undo, {}); + CoreEditingCommands.Undo.runEditorCommand(null, editor, null); assert.equal(model.getLineContent(2), ' a '); // Nothing is broken when cursor is in (1,1) @@ -2859,22 +2857,22 @@ suite('Editor Controller - Cursor Configuration', () => { CoreEditingCommands.DeleteLeft.runEditorCommand(null, editor, null); assert.equal(model.getValue(EndOfLinePreference.LF), 'x', 'assert10'); - cursorCommand(cursor, H.Undo, {}); + CoreEditingCommands.Undo.runEditorCommand(null, editor, null); assert.equal(model.getValue(EndOfLinePreference.LF), '\nx', 'assert11'); - cursorCommand(cursor, H.Undo, {}); + CoreEditingCommands.Undo.runEditorCommand(null, editor, null); assert.equal(model.getValue(EndOfLinePreference.LF), '\n\ty\nx', 'assert12'); - cursorCommand(cursor, H.Undo, {}); + CoreEditingCommands.Undo.runEditorCommand(null, editor, null); assert.equal(model.getValue(EndOfLinePreference.LF), '\n\ty\n\tx', 'assert13'); - cursorCommand(cursor, H.Redo, {}); + CoreEditingCommands.Redo.runEditorCommand(null, editor, null); assert.equal(model.getValue(EndOfLinePreference.LF), '\n\ty\nx', 'assert14'); - cursorCommand(cursor, H.Redo, {}); + CoreEditingCommands.Redo.runEditorCommand(null, editor, null); assert.equal(model.getValue(EndOfLinePreference.LF), '\nx', 'assert15'); - cursorCommand(cursor, H.Redo, {}); + CoreEditingCommands.Redo.runEditorCommand(null, editor, null); assert.equal(model.getValue(EndOfLinePreference.LF), 'x', 'assert16'); }); @@ -2895,7 +2893,7 @@ suite('Editor Controller - Cursor Configuration', () => { const beforeVersion = model.getVersionId(); const beforeAltVersion = model.getAlternativeVersionId(); cursorCommand(cursor, H.Type, { text: 'Hello' }, 'keyboard'); - cursorCommand(cursor, H.Undo, {}); + CoreEditingCommands.Undo.runEditorCommand(null, editor, null); const afterVersion = model.getVersionId(); const afterAltVersion = model.getAlternativeVersionId(); @@ -4263,7 +4261,7 @@ suite('autoClosingPairs', () => { moveTo(cursor, lineNumber, column); cursorCommand(cursor, H.Type, { text: chr }, 'keyboard'); assert.deepEqual(model.getLineContent(lineNumber), expected, message); - cursorCommand(cursor, H.Undo); + model.undo(); } test('open parens: default', () => { @@ -5347,11 +5345,11 @@ suite('Undo stops', () => { assert.equal(model.getLineContent(1), 'A fir line'); assertCursor(cursor, new Selection(1, 6, 1, 6)); - cursorCommand(cursor, H.Undo, {}); + CoreEditingCommands.Undo.runEditorCommand(null, editor, null); assert.equal(model.getLineContent(1), 'A first line'); assertCursor(cursor, new Selection(1, 8, 1, 8)); - cursorCommand(cursor, H.Undo, {}); + CoreEditingCommands.Undo.runEditorCommand(null, editor, null); assert.equal(model.getLineContent(1), 'A line'); assertCursor(cursor, new Selection(1, 3, 1, 3)); }); @@ -5376,11 +5374,11 @@ suite('Undo stops', () => { assert.equal(model.getLineContent(1), 'A firstine'); assertCursor(cursor, new Selection(1, 8, 1, 8)); - cursorCommand(cursor, H.Undo, {}); + CoreEditingCommands.Undo.runEditorCommand(null, editor, null); assert.equal(model.getLineContent(1), 'A first line'); assertCursor(cursor, new Selection(1, 8, 1, 8)); - cursorCommand(cursor, H.Undo, {}); + CoreEditingCommands.Undo.runEditorCommand(null, editor, null); assert.equal(model.getLineContent(1), 'A line'); assertCursor(cursor, new Selection(1, 3, 1, 3)); }); @@ -5410,11 +5408,11 @@ suite('Undo stops', () => { assert.equal(model.getLineContent(2), 'Second line'); assertCursor(cursor, new Selection(2, 7, 2, 7)); - cursorCommand(cursor, H.Undo, {}); + CoreEditingCommands.Undo.runEditorCommand(null, editor, null); assert.equal(model.getLineContent(2), ' line'); assertCursor(cursor, new Selection(2, 1, 2, 1)); - cursorCommand(cursor, H.Undo, {}); + CoreEditingCommands.Undo.runEditorCommand(null, editor, null); assert.equal(model.getLineContent(2), 'Another line'); assertCursor(cursor, new Selection(2, 8, 2, 8)); }); @@ -5448,11 +5446,11 @@ suite('Undo stops', () => { assert.equal(model.getLineContent(2), ''); assertCursor(cursor, new Selection(2, 1, 2, 1)); - cursorCommand(cursor, H.Undo, {}); + CoreEditingCommands.Undo.runEditorCommand(null, editor, null); assert.equal(model.getLineContent(2), ' line'); assertCursor(cursor, new Selection(2, 1, 2, 1)); - cursorCommand(cursor, H.Undo, {}); + CoreEditingCommands.Undo.runEditorCommand(null, editor, null); assert.equal(model.getLineContent(2), 'Another line'); assertCursor(cursor, new Selection(2, 8, 2, 8)); }); @@ -5479,11 +5477,11 @@ suite('Undo stops', () => { assert.equal(model.getLineContent(2), 'Another text'); assertCursor(cursor, new Selection(2, 13, 2, 13)); - cursorCommand(cursor, H.Undo, {}); + CoreEditingCommands.Undo.runEditorCommand(null, editor, null); assert.equal(model.getLineContent(2), 'Another '); assertCursor(cursor, new Selection(2, 9, 2, 9)); - cursorCommand(cursor, H.Undo, {}); + CoreEditingCommands.Undo.runEditorCommand(null, editor, null); assert.equal(model.getLineContent(2), 'Another line'); assertCursor(cursor, new Selection(2, 9, 2, 9)); }); @@ -5515,11 +5513,11 @@ suite('Undo stops', () => { assert.equal(model.getLineContent(2), 'An'); assertCursor(cursor, new Selection(2, 3, 2, 3)); - cursorCommand(cursor, H.Undo, {}); + CoreEditingCommands.Undo.runEditorCommand(null, editor, null); assert.equal(model.getLineContent(2), 'Another '); assertCursor(cursor, new Selection(2, 9, 2, 9)); - cursorCommand(cursor, H.Undo, {}); + CoreEditingCommands.Undo.runEditorCommand(null, editor, null); assert.equal(model.getLineContent(2), 'Another line'); assertCursor(cursor, new Selection(2, 9, 2, 9)); }); @@ -5539,15 +5537,15 @@ suite('Undo stops', () => { assert.equal(model.getLineContent(1), 'A first and interesting line'); assertCursor(cursor, new Selection(1, 24, 1, 24)); - cursorCommand(cursor, H.Undo, {}); + CoreEditingCommands.Undo.runEditorCommand(null, editor, null); assert.equal(model.getLineContent(1), 'A first and line'); assertCursor(cursor, new Selection(1, 12, 1, 12)); - cursorCommand(cursor, H.Undo, {}); + CoreEditingCommands.Undo.runEditorCommand(null, editor, null); assert.equal(model.getLineContent(1), 'A first line'); assertCursor(cursor, new Selection(1, 8, 1, 8)); - cursorCommand(cursor, H.Undo, {}); + CoreEditingCommands.Undo.runEditorCommand(null, editor, null); assert.equal(model.getLineContent(1), 'A line'); assertCursor(cursor, new Selection(1, 3, 1, 3)); }); diff --git a/src/vs/editor/test/browser/testCodeEditor.ts b/src/vs/editor/test/browser/testCodeEditor.ts index 47a341a2d63..d138d2e396a 100644 --- a/src/vs/editor/test/browser/testCodeEditor.ts +++ b/src/vs/editor/test/browser/testCodeEditor.ts @@ -84,6 +84,7 @@ export function withTestCodeEditor(text: string | string[] | null, options: Test } let editor = createTestCodeEditor(options); + editor.getCursor()!.setHasFocus(true); callback(editor, editor.getCursor()!); editor.dispose(); diff --git a/src/vs/platform/undoRedo/common/undoRedo.ts b/src/vs/platform/undoRedo/common/undoRedo.ts index 711c2058548..75e08d09dcc 100644 --- a/src/vs/platform/undoRedo/common/undoRedo.ts +++ b/src/vs/platform/undoRedo/common/undoRedo.ts @@ -16,7 +16,7 @@ export interface IUndoRedoElement { /** * None, one or multiple resources that this undo/redo element impacts. */ - readonly resources: URI[]; + readonly resources: readonly URI[]; /** * The label of the undo/redo element. @@ -43,7 +43,7 @@ export interface IUndoRedoElement { * Invalidate the edits concerning `resource`. * i.e. the undo/redo stack for that particular resource has been destroyed. */ - invalidate(resource: URI): boolean; + invalidate(resource: URI): void; } export interface IUndoRedoService { diff --git a/src/vs/platform/undoRedo/common/undoRedoService.ts b/src/vs/platform/undoRedo/common/undoRedoService.ts index 60c98764c17..dfa7fb4cea2 100644 --- a/src/vs/platform/undoRedo/common/undoRedoService.ts +++ b/src/vs/platform/undoRedo/common/undoRedoService.ts @@ -7,11 +7,12 @@ import { IUndoRedoService, IUndoRedoElement } from 'vs/platform/undoRedo/common/ import { URI } from 'vs/base/common/uri'; import { getComparisonKey as uriGetComparisonKey } from 'vs/base/common/resources'; import { onUnexpectedError } from 'vs/base/common/errors'; +import { registerSingleton } from 'vs/platform/instantiation/common/extensions'; class StackElement { public readonly actual: IUndoRedoElement; public readonly label: string; - public readonly resources: URI[]; + public readonly resources: readonly URI[]; public readonly strResources: string[]; constructor(actual: IUndoRedoElement) { @@ -179,7 +180,7 @@ export class UndoRedoService implements IUndoRedoService { return false; } - redo(resource: URI): void { + public redo(resource: URI): void { const strResource = uriGetComparisonKey(resource); if (!this._editStacks.has(strResource)) { return; @@ -239,3 +240,5 @@ export class UndoRedoService implements IUndoRedoService { } } } + +registerSingleton(IUndoRedoService, UndoRedoService); diff --git a/src/vs/workbench/services/keybinding/test/electron-browser/keybindingEditing.test.ts b/src/vs/workbench/services/keybinding/test/electron-browser/keybindingEditing.test.ts index 0e60ab8f43f..5c52f1a73b1 100644 --- a/src/vs/workbench/services/keybinding/test/electron-browser/keybindingEditing.test.ts +++ b/src/vs/workbench/services/keybinding/test/electron-browser/keybindingEditing.test.ts @@ -52,6 +52,8 @@ import { ILabelService } from 'vs/platform/label/common/label'; import { LabelService } from 'vs/workbench/services/label/common/labelService'; import { IFilesConfigurationService, FilesConfigurationService } from 'vs/workbench/services/filesConfiguration/common/filesConfigurationService'; import { WorkingCopyFileService, IWorkingCopyFileService } from 'vs/workbench/services/workingCopy/common/workingCopyFileService'; +import { IUndoRedoService } from 'vs/platform/undoRedo/common/undoRedo'; +import { UndoRedoService } from 'vs/platform/undoRedo/common/undoRedoService'; class TestEnvironmentService extends NativeWorkbenchEnvironmentService { @@ -103,6 +105,7 @@ suite('KeybindingsEditing', () => { instantiationService.stub(ILabelService, instantiationService.createInstance(LabelService)); instantiationService.stub(IFilesConfigurationService, instantiationService.createInstance(FilesConfigurationService)); instantiationService.stub(ITextResourcePropertiesService, new TestTextResourcePropertiesService(instantiationService.get(IConfigurationService))); + instantiationService.stub(IUndoRedoService, instantiationService.createInstance(UndoRedoService)); instantiationService.stub(IModelService, instantiationService.createInstance(ModelServiceImpl)); const fileService = new FileService(new NullLogService()); const diskFileSystemProvider = new DiskFileSystemProvider(new NullLogService()); diff --git a/src/vs/workbench/test/browser/parts/editor/editorModel.test.ts b/src/vs/workbench/test/browser/parts/editor/editorModel.test.ts index a754831a0a1..849a327b502 100644 --- a/src/vs/workbench/test/browser/parts/editor/editorModel.test.ts +++ b/src/vs/workbench/test/browser/parts/editor/editorModel.test.ts @@ -18,6 +18,8 @@ import { URI } from 'vs/base/common/uri'; import { createTextBufferFactory } from 'vs/editor/common/model/textModel'; import { ITextResourcePropertiesService } from 'vs/editor/common/services/textResourceConfigurationService'; import { TestTextResourcePropertiesService } from 'vs/workbench/test/browser/workbenchTestServices'; +import { IUndoRedoService } from 'vs/platform/undoRedo/common/undoRedo'; +import { UndoRedoService } from 'vs/platform/undoRedo/common/undoRedoService'; class MyEditorModel extends EditorModel { } class MyTextEditorModel extends BaseTextEditorModel { @@ -72,6 +74,7 @@ suite('Workbench editor model', () => { function stubModelService(instantiationService: TestInstantiationService): IModelService { instantiationService.stub(IConfigurationService, new TestConfigurationService()); instantiationService.stub(ITextResourcePropertiesService, new TestTextResourcePropertiesService(instantiationService.get(IConfigurationService))); + instantiationService.stub(IUndoRedoService, new UndoRedoService()); return instantiationService.createInstance(ModelServiceImpl); } }); diff --git a/src/vs/workbench/test/browser/workbenchTestServices.ts b/src/vs/workbench/test/browser/workbenchTestServices.ts index fb5e9e681da..9e00ead1308 100644 --- a/src/vs/workbench/test/browser/workbenchTestServices.ts +++ b/src/vs/workbench/test/browser/workbenchTestServices.ts @@ -91,6 +91,8 @@ import { IRemotePathService } from 'vs/workbench/services/path/common/remotePath import { Direction } from 'vs/base/browser/ui/grid/grid'; import { IProgressService, IProgressOptions, IProgressWindowOptions, IProgressNotificationOptions, IProgressCompositeOptions, IProgress, IProgressStep, emptyProgress } from 'vs/platform/progress/common/progress'; import { IWorkingCopyFileService, WorkingCopyFileService } from 'vs/workbench/services/workingCopy/common/workingCopyFileService'; +import { UndoRedoService } from 'vs/platform/undoRedo/common/undoRedoService'; +import { IUndoRedoService } from 'vs/platform/undoRedo/common/undoRedo'; export import TestTextResourcePropertiesService = CommonWorkbenchTestServices.TestTextResourcePropertiesService; export import TestContextService = CommonWorkbenchTestServices.TestContextService; @@ -194,6 +196,7 @@ export function workbenchInstantiationService(overrides?: { textFileService?: (i instantiationService.stub(IModeService, instantiationService.createInstance(ModeServiceImpl)); instantiationService.stub(IHistoryService, new TestHistoryService()); instantiationService.stub(ITextResourcePropertiesService, new TestTextResourcePropertiesService(configService)); + instantiationService.stub(IUndoRedoService, instantiationService.createInstance(UndoRedoService)); instantiationService.stub(IModelService, instantiationService.createInstance(ModelServiceImpl)); instantiationService.stub(IFileService, new TestFileService()); instantiationService.stub(IBackupFileService, new TestBackupFileService()); diff --git a/src/vs/workbench/workbench.common.main.ts b/src/vs/workbench/workbench.common.main.ts index b418c5b790e..eae7291b6ff 100644 --- a/src/vs/workbench/workbench.common.main.ts +++ b/src/vs/workbench/workbench.common.main.ts @@ -57,6 +57,7 @@ import 'vs/workbench/browser/parts/views/views'; //#region --- workbench services +import 'vs/platform/undoRedo/common/undoRedoService'; import 'vs/workbench/services/extensions/browser/extensionUrlHandler'; import 'vs/workbench/services/bulkEdit/browser/bulkEditService'; import 'vs/workbench/services/keybinding/common/keybindingEditing';