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