提交 c8ba3022 编写于 作者: A Alex Dima

Fixes #8205: Make emmet edits undoable in one step

上级 2550eb7d
...@@ -595,7 +595,7 @@ export abstract class CommonCodeEditor extends EventEmitter implements IActionPr ...@@ -595,7 +595,7 @@ export abstract class CommonCodeEditor extends EventEmitter implements IActionPr
this.cursor.trigger(source, editorCommon.Handler.ExecuteCommand, command); this.cursor.trigger(source, editorCommon.Handler.ExecuteCommand, command);
} }
public executeEdits(source: string, edits: editorCommon.IIdentifiedSingleEditOperation[]): boolean { public pushUndoStop(): boolean {
if (!this.cursor) { if (!this.cursor) {
// no view, no cursor // no view, no cursor
return false; return false;
...@@ -605,10 +605,23 @@ export abstract class CommonCodeEditor extends EventEmitter implements IActionPr ...@@ -605,10 +605,23 @@ export abstract class CommonCodeEditor extends EventEmitter implements IActionPr
return false; return false;
} }
this.model.pushStackElement(); this.model.pushStackElement();
return true;
}
public executeEdits(source: string, edits: editorCommon.IIdentifiedSingleEditOperation[]): boolean {
if (!this.cursor) {
// no view, no cursor
return false;
}
if (this._configuration.editor.readOnly) {
// read only editor => sorry!
return false;
}
this.model.pushEditOperations(this.cursor.getSelections(), edits, () => { this.model.pushEditOperations(this.cursor.getSelections(), edits, () => {
return this.cursor.getSelections(); return this.cursor.getSelections();
}); });
this.model.pushStackElement();
return true; return true;
} }
......
...@@ -3925,6 +3925,11 @@ export interface ICommonCodeEditor extends IEditor { ...@@ -3925,6 +3925,11 @@ export interface ICommonCodeEditor extends IEditor {
*/ */
executeCommand(source: string, command: ICommand): void; executeCommand(source: string, command: ICommand): void;
/**
* Push an "undo stop" in the undo-redo stack.
*/
pushUndoStop(): boolean;
/** /**
* Execute a command on the editor. * Execute a command on the editor.
* @param source The source of the call. * @param source The source of the call.
......
...@@ -65,8 +65,10 @@ suite('FindController', () => { ...@@ -65,8 +65,10 @@ suite('FindController', () => {
assert.deepEqual(fromRange(editor.getSelection()), [1, 1, 1, 4]); assert.deepEqual(fromRange(editor.getSelection()), [1, 1, 1, 4]);
// I hit delete to remove it and change the text to XYZ. // I hit delete to remove it and change the text to XYZ.
editor.pushUndoStop();
editor.executeEdits('test', [EditOperation.delete(new Range(1, 1, 1, 4))]); editor.executeEdits('test', [EditOperation.delete(new Range(1, 1, 1, 4))]);
editor.executeEdits('test', [EditOperation.insert(new Position(1, 1), 'XYZ')]); editor.executeEdits('test', [EditOperation.insert(new Position(1, 1), 'XYZ')]);
editor.pushUndoStop();
// At this point the text editor looks like this: // At this point the text editor looks like this:
// XYZ // XYZ
......
...@@ -717,6 +717,10 @@ class InsertSnippetController { ...@@ -717,6 +717,10 @@ class InsertSnippetController {
export interface ISnippetController extends editorCommon.IEditorContribution { export interface ISnippetController extends editorCommon.IEditorContribution {
run(snippet: CodeSnippet, overwriteBefore: number, overwriteAfter: number, stripPrefix?:boolean): void; run(snippet: CodeSnippet, overwriteBefore: number, overwriteAfter: number, stripPrefix?:boolean): void;
/**
* Inserts once `snippet` at the start of `replaceRange`, after deleting `replaceRange`.
*/
runWithReplaceRange(snippet: CodeSnippet, replaceRange:Range, undoStops:boolean): void;
jumpToNextPlaceholder(): void; jumpToNextPlaceholder(): void;
jumpToPrevPlaceholder(): void; jumpToPrevPlaceholder(): void;
acceptSnippet(): void; acceptSnippet(): void;
...@@ -727,6 +731,11 @@ export function getSnippetController(editor: editorCommon.ICommonCodeEditor): IS ...@@ -727,6 +731,11 @@ export function getSnippetController(editor: editorCommon.ICommonCodeEditor): IS
return <ISnippetController>editor.getContribution(SnippetController.ID); return <ISnippetController>editor.getContribution(SnippetController.ID);
} }
interface IPreparedSnippet {
typeRange: Range;
adaptedSnippet: ICodeSnippet;
}
class SnippetController implements ISnippetController { class SnippetController implements ISnippetController {
public static ID = 'editor.contrib.snippetController'; public static ID = 'editor.contrib.snippetController';
...@@ -753,15 +762,31 @@ class SnippetController implements ISnippetController { ...@@ -753,15 +762,31 @@ class SnippetController implements ISnippetController {
} }
public run(snippet:CodeSnippet, overwriteBefore:number, overwriteAfter:number, stripPrefix?:boolean): void { public run(snippet:CodeSnippet, overwriteBefore:number, overwriteAfter:number, stripPrefix?:boolean): void {
this._runAndRestoreController(() => {
if (snippet.placeHolders.length === 0) {
// No placeholders => execute for all editor selections
this._runForAllSelections(snippet, overwriteBefore, overwriteAfter, stripPrefix);
} else {
let prepared = SnippetController._prepareSnippet(this._editor, this._editor.getSelection(), snippet, overwriteBefore, overwriteAfter, stripPrefix);
this._runPreparedSnippetForPrimarySelection(prepared, true);
}
});
}
public runWithReplaceRange(snippet: CodeSnippet, replaceRange:Range, undoStops:boolean): void {
this._runAndRestoreController(() => {
this._runPreparedSnippetForPrimarySelection({
typeRange: replaceRange,
adaptedSnippet: SnippetController._getAdaptedSnippet(this._editor.getModel(), snippet, replaceRange)
}, undoStops);
});
}
private _runAndRestoreController(callback:()=>void): void {
let prevController = this._currentController; let prevController = this._currentController;
this._currentController = null; this._currentController = null;
if (snippet.placeHolders.length === 0) { callback();
// No placeholders => execute for all editor selections
this._runForAllSelections(snippet, overwriteBefore, overwriteAfter, stripPrefix);
} else {
this._runForPrimarySelection(snippet, overwriteBefore, overwriteAfter, stripPrefix);
}
if (!this._currentController) { if (!this._currentController) {
// we didn't end up in snippet mode again => restore previous controller // we didn't end up in snippet mode again => restore previous controller
...@@ -801,16 +826,21 @@ class SnippetController implements ISnippetController { ...@@ -801,16 +826,21 @@ class SnippetController implements ISnippetController {
} }
} }
private _runForPrimarySelection(snippet: CodeSnippet, overwriteBefore: number, overwriteAfter: number, stripPrefix?:boolean): void { private _runPreparedSnippetForPrimarySelection(prepared: IPreparedSnippet, undoStops:boolean): void {
let initialAlternativeVersionId = this._editor.getModel().getAlternativeVersionId(); let initialAlternativeVersionId = this._editor.getModel().getAlternativeVersionId();
let edits: editorCommon.IIdentifiedSingleEditOperation[] = []; let edits: editorCommon.IIdentifiedSingleEditOperation[] = [];
let prepared = SnippetController._prepareSnippet(this._editor, this._editor.getSelection(), snippet, overwriteBefore, overwriteAfter, stripPrefix);
SnippetController._addCommandForSnippet(this._editor.getModel(), prepared.adaptedSnippet, prepared.typeRange, edits); SnippetController._addCommandForSnippet(this._editor.getModel(), prepared.adaptedSnippet, prepared.typeRange, edits);
if (edits.length > 0) { if (edits.length > 0) {
if (undoStops) {
this._editor.pushUndoStop();
}
this._editor.executeEdits('editor.contrib.insertSnippetHelper', edits); this._editor.executeEdits('editor.contrib.insertSnippetHelper', edits);
if (undoStops) {
this._editor.pushUndoStop();
}
} }
let cursorOnly = SnippetController._getSnippetCursorOnly(prepared.adaptedSnippet); let cursorOnly = SnippetController._getSnippetCursorOnly(prepared.adaptedSnippet);
...@@ -834,7 +864,9 @@ class SnippetController implements ISnippetController { ...@@ -834,7 +864,9 @@ class SnippetController implements ISnippetController {
} }
if (edits.length > 0) { if (edits.length > 0) {
this._editor.pushUndoStop();
this._editor.executeEdits('editor.contrib.insertSnippetHelper', edits); this._editor.executeEdits('editor.contrib.insertSnippetHelper', edits);
this._editor.pushUndoStop();
} }
} }
......
...@@ -3126,6 +3126,10 @@ declare module monaco.editor { ...@@ -3126,6 +3126,10 @@ declare module monaco.editor {
* @param command The command to execute * @param command The command to execute
*/ */
executeCommand(source: string, command: ICommand): void; executeCommand(source: string, command: ICommand): void;
/**
* Push an "undo stop" in the undo-redo stack.
*/
pushUndoStop(): boolean;
/** /**
* Execute a command on the editor. * Execute a command on the editor.
* @param source The source of the call. * @param source The source of the call.
......
...@@ -329,7 +329,11 @@ export class MainThreadTextEditor { ...@@ -329,7 +329,11 @@ export class MainThreadTextEditor {
forceMoveMarkers: edit.forceMoveMarkers forceMoveMarkers: edit.forceMoveMarkers
}; };
}); });
return this._codeEditor.executeEdits('MainThreadTextEditor', transformedEdits) || true;
this._codeEditor.pushUndoStop();
this._codeEditor.executeEdits('MainThreadTextEditor', transformedEdits);
this._codeEditor.pushUndoStop();
return true;
} }
console.warn('applyEdits on invisible editor'); console.warn('applyEdits on invisible editor');
......
...@@ -24,7 +24,7 @@ class ExpandAbbreviationAction extends BasicEmmetEditorAction { ...@@ -24,7 +24,7 @@ class ExpandAbbreviationAction extends BasicEmmetEditorAction {
super(descriptor, editor, configurationService, 'expand_abbreviation'); super(descriptor, editor, configurationService, 'expand_abbreviation');
} }
public noExpansionOccurred(actionId?: string): void { protected noExpansionOccurred(): void {
// forward the tab key back to the editor // forward the tab key back to the editor
this.editor.trigger('emmet', editorCommon.Handler.Tab, {}); this.editor.trigger('emmet', editorCommon.Handler.Tab, {});
} }
......
...@@ -9,18 +9,19 @@ import {IPosition, ICommonCodeEditor} from 'vs/editor/common/editorCommon'; ...@@ -9,18 +9,19 @@ import {IPosition, ICommonCodeEditor} from 'vs/editor/common/editorCommon';
import strings = require('vs/base/common/strings'); import strings = require('vs/base/common/strings');
import snippets = require('vs/editor/contrib/snippet/common/snippet'); import snippets = require('vs/editor/contrib/snippet/common/snippet');
import {Range} from 'vs/editor/common/core/range'; import {Range} from 'vs/editor/common/core/range';
import {ReplaceCommand} from 'vs/editor/common/commands/replaceCommand';
import emmet = require('emmet'); import emmet = require('emmet');
export class EditorAccessor implements emmet.Editor { export class EditorAccessor implements emmet.Editor {
editor: ICommonCodeEditor; editor: ICommonCodeEditor;
private _hasMadeEdits: boolean;
emmetSupportedModes = ['html', 'razor', 'css', 'less', 'sass', 'scss', 'stylus', 'xml', 'xsl', 'jade', 'handlebars', 'ejs', 'hbs', 'jsx', 'tsx', 'erb', 'php', 'twig']; emmetSupportedModes = ['html', 'razor', 'css', 'less', 'sass', 'scss', 'stylus', 'xml', 'xsl', 'jade', 'handlebars', 'ejs', 'hbs', 'jsx', 'tsx', 'erb', 'php', 'twig'];
constructor(editor: ICommonCodeEditor) { constructor(editor: ICommonCodeEditor) {
this.editor = editor; this.editor = editor;
this._hasMadeEdits = false;
} }
public isEmmetEnabledMode(): boolean { public isEmmetEnabledMode(): boolean {
...@@ -58,6 +59,10 @@ export class EditorAccessor implements emmet.Editor { ...@@ -58,6 +59,10 @@ export class EditorAccessor implements emmet.Editor {
return this.editor.getModel().getLineContent(selectionStart.lineNumber); return this.editor.getModel().getLineContent(selectionStart.lineNumber);
} }
public onBeforeEmmetAction(): void {
this._hasMadeEdits = false;
}
public replaceContent(value: string, start: number, end: number, no_indent: boolean): void { public replaceContent(value: string, start: number, end: number, no_indent: boolean): void {
//console.log('value', value); //console.log('value', value);
let startPosition = this.getPositionFromOffset(start); let startPosition = this.getPositionFromOffset(start);
...@@ -74,15 +79,23 @@ export class EditorAccessor implements emmet.Editor { ...@@ -74,15 +79,23 @@ export class EditorAccessor implements emmet.Editor {
} }
} }
// shift column by +1 since they are 1 based // If this is the first edit in this "transaction", push an undo stop before them
let range = new Range(startPosition.lineNumber, startPosition.column, endPosition.lineNumber, endPosition.column); if (!this._hasMadeEdits) {
this._hasMadeEdits = true;
let command = new ReplaceCommand(range, ''); this.editor.pushUndoStop();
this.editor.executeCommand('emmet', command); }
let range = new Range(startPosition.lineNumber, startPosition.column, endPosition.lineNumber, endPosition.column);
let snippet = snippets.CodeSnippet.convertExternalSnippet(value, snippets.ExternalSnippetType.EmmetSnippet); let snippet = snippets.CodeSnippet.convertExternalSnippet(value, snippets.ExternalSnippetType.EmmetSnippet);
let codeSnippet = new snippets.CodeSnippet(snippet); let codeSnippet = new snippets.CodeSnippet(snippet);
snippets.getSnippetController(this.editor).run(codeSnippet, 0, 0, false); snippets.getSnippetController(this.editor).runWithReplaceRange(codeSnippet, range, false);
}
public onAfterEmmetAction(): void {
// If there were any edits in this "transaction", push an undo stop after them
if (this._hasMadeEdits) {
this.editor.pushUndoStop();
}
} }
public getContent(): string { public getContent(): string {
......
...@@ -136,4 +136,5 @@ declare module 'emmet' { ...@@ -136,4 +136,5 @@ declare module 'emmet' {
*/ */
export function run(action: string, editor: Editor): boolean; export function run(action: string, editor: Editor): boolean;
export function file(fileAccessor:any): void;
} }
...@@ -13,6 +13,7 @@ import {Behaviour} from 'vs/editor/common/editorActionEnablement'; ...@@ -13,6 +13,7 @@ import {Behaviour} from 'vs/editor/common/editorActionEnablement';
import {EditorAccessor} from 'vs/workbench/parts/emmet/node/editorAccessor'; import {EditorAccessor} from 'vs/workbench/parts/emmet/node/editorAccessor';
import * as fileAccessor from 'vs/workbench/parts/emmet/node/fileAccessor'; import * as fileAccessor from 'vs/workbench/parts/emmet/node/fileAccessor';
import {IConfigurationService} from 'vs/platform/configuration/common/configuration'; import {IConfigurationService} from 'vs/platform/configuration/common/configuration';
import * as emmet from 'emmet';
interface IEmmetConfiguration { interface IEmmetConfiguration {
emmet: { emmet: {
...@@ -28,7 +29,7 @@ export abstract class EmmetEditorAction extends EditorAction { ...@@ -28,7 +29,7 @@ export abstract class EmmetEditorAction extends EditorAction {
private configurationService: IConfigurationService = null; private configurationService: IConfigurationService = null;
constructor(descriptor: IEditorActionDescriptorData, editor: ICommonCodeEditor, configurationService: IConfigurationService) { constructor(descriptor: IEditorActionDescriptorData, editor: ICommonCodeEditor, configurationService: IConfigurationService) {
super(descriptor, editor, Behaviour.TextFocus); super(descriptor, editor, Behaviour.Writeable);
this.editorAccessor = new EditorAccessor(editor); this.editorAccessor = new EditorAccessor(editor);
this.configurationService = configurationService; this.configurationService = configurationService;
} }
...@@ -66,25 +67,37 @@ export abstract class EmmetEditorAction extends EditorAction { ...@@ -66,25 +67,37 @@ export abstract class EmmetEditorAction extends EditorAction {
} }
public run(): TPromise<boolean> { public run(): TPromise<boolean> {
return new TPromise((c, e) => { if (!this.editorAccessor.isEmmetEnabledMode()) {
require(['emmet'], (_emmet) => { this.noExpansionOccurred();
_emmet.file(fileAccessor); return ;
}
try {
if (!this.editorAccessor.isEmmetEnabledMode()) { return this._withEmmet().then((_emmet) => {
this.noExpansionOccurred(); _emmet.file(fileAccessor);
return; this._withEmmetPreferences(_emmet, () => {
} this.editorAccessor.onBeforeEmmetAction();
this.updateEmmetPreferences(_emmet); this.runEmmetAction(_emmet);
this.runEmmetAction(_emmet); this.editorAccessor.onAfterEmmetAction();
} catch (err) { });
//
} finally { return true;
this.resetEmmetPreferences(_emmet); });
} }
}, e);
private _withEmmet(): TPromise<typeof emmet> {
return new TPromise<typeof emmet>((c, e) => {
require(['emmet'], c, e);
}); });
} }
private _withEmmetPreferences(_emmet:typeof emmet, callback:() => void): void {
try {
this.updateEmmetPreferences(_emmet);
callback();
} finally {
this.resetEmmetPreferences(_emmet);
}
}
} }
export class BasicEmmetEditorAction extends EmmetEditorAction { export class BasicEmmetEditorAction extends EmmetEditorAction {
......
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册