Merge pull request #17152 from rebornix/MultiCursorState

Compute cursor/s state after edits were applied
This commit is contained in:
Alexandru Dima 2016-12-27 12:05:50 +02:00 committed by GitHub
commit 84e23b5182
5 changed files with 321 additions and 237 deletions

View file

@ -600,7 +600,7 @@ export abstract class CommonCodeEditor extends EventEmitter implements editorCom
return true;
}
public executeEdits(source: string, edits: editorCommon.IIdentifiedSingleEditOperation[]): boolean {
public executeEdits(source: string, edits: editorCommon.IIdentifiedSingleEditOperation[], endCursorState?: Selection[]): boolean {
if (!this.cursor) {
// no view, no cursor
return false;
@ -611,9 +611,13 @@ export abstract class CommonCodeEditor extends EventEmitter implements editorCom
}
this.model.pushEditOperations(this.cursor.getSelections(), edits, () => {
return this.cursor.getSelections();
return endCursorState ? endCursorState : this.cursor.getSelections();
});
if (endCursorState) {
this.cursor.setSelections(source, endCursorState);
}
return true;
}

View file

@ -3939,11 +3939,12 @@ export interface ICommonCodeEditor extends IEditor {
pushUndoStop(): boolean;
/**
* Execute a command on the editor.
* Execute edits on the editor.
* @param source The source of the call.
* @param command The command to execute
* @param edits The edits to execute.
* @param endCursoState Cursor state after the edits were applied.
*/
executeEdits(source: string, edits: IIdentifiedSingleEditOperation[]): boolean;
executeEdits(source: string, edits: IIdentifiedSingleEditOperation[], endCursoState?: Selection[]): boolean;
/**
* Execute multiple (concommitent) commands on the editor.

View file

@ -351,92 +351,10 @@ class InsertLineAfterAction extends HandlerEditorAction {
}
}
@editorAction
export class DeleteAllLeftAction extends EditorAction {
constructor() {
super({
id: 'deleteAllLeft',
label: nls.localize('lines.deleteAllLeft', "Delete All Left"),
alias: 'Delete All Left',
precondition: EditorContextKeys.Writable,
kbOpts: {
kbExpr: EditorContextKeys.TextFocus,
primary: null,
mac: { primary: KeyMod.CtrlCmd | KeyCode.Backspace }
}
});
}
export abstract class AbstractDeleteAllToBoundaryAction extends EditorAction {
public run(accessor: ServicesAccessor, editor: ICommonCodeEditor): void {
let selections: Range[] = editor.getSelections();
selections.sort(Range.compareRangesUsingStarts);
selections = selections.map(selection => {
if (selection.isEmpty()) {
return new Selection(selection.startLineNumber, 1, selection.startLineNumber, selection.startColumn);
} else {
return selection;
}
});
// merge overlapping selections
let effectiveRanges: Range[] = [];
for (let i = 0, count = selections.length - 1; i < count; i++) {
let range = selections[i];
let nextRange = selections[i + 1];
if (Range.intersectRanges(range, nextRange) === null) {
effectiveRanges.push(range);
} else {
selections[i + 1] = Range.plusRange(range, nextRange);
}
}
effectiveRanges.push(selections[selections.length - 1]);
let edits: IIdentifiedSingleEditOperation[] = effectiveRanges.map(range => {
return EditOperation.replace(range, '');
});
editor.executeEdits(this.id, edits);
}
}
@editorAction
export class DeleteAllRightAction extends EditorAction {
constructor() {
super({
id: 'deleteAllRight',
label: nls.localize('lines.deleteAllRight', "Delete All Right"),
alias: 'Delete All Right',
precondition: EditorContextKeys.Writable,
kbOpts: {
kbExpr: EditorContextKeys.TextFocus,
primary: null,
mac: { primary: KeyMod.WinCtrl | KeyCode.KEY_K, secondary: [KeyMod.CtrlCmd | KeyCode.Delete] }
}
});
}
public run(accessor: ServicesAccessor, editor: ICommonCodeEditor): void {
let model = editor.getModel();
let rangesToDelete: Range[] = editor.getSelections().map((sel) => {
if (sel.isEmpty()) {
const maxColumn = model.getLineMaxColumn(sel.startLineNumber);
if (sel.startColumn === maxColumn) {
return new Range(sel.startLineNumber, sel.startColumn, sel.startLineNumber + 1, 1);
} else {
return new Range(sel.startLineNumber, sel.startColumn, sel.startLineNumber, maxColumn);
}
}
return sel;
});
rangesToDelete.sort(Range.compareRangesUsingStarts);
const primaryCursor = editor.getSelection();
let rangesToDelete = this._getRangesToDelete(editor);
// merge overlapping selections
let effectiveRanges: Range[] = [];
@ -453,12 +371,132 @@ export class DeleteAllRightAction extends EditorAction {
effectiveRanges.push(rangesToDelete[rangesToDelete.length - 1]);
let endCursorState = this._getEndCursorState(primaryCursor, effectiveRanges);
let edits: IIdentifiedSingleEditOperation[] = effectiveRanges.map(range => {
endCursorState.push(new Selection(range.startLineNumber, range.startColumn, range.startLineNumber, range.startColumn));
return EditOperation.replace(range, '');
});
editor.executeEdits(this.id, edits);
editor.pushUndoStop();
editor.executeEdits(this.id, edits, endCursorState);
}
/**
* Compute the cursor state after the edit operations were applied.
*/
protected abstract _getEndCursorState(primaryCursor: Range, rangesToDelete: Range[]): Selection[];
protected abstract _getRangesToDelete(editor: ICommonCodeEditor): Range[];
}
@editorAction
export class DeleteAllLeftAction extends AbstractDeleteAllToBoundaryAction {
constructor() {
super({
id: 'deleteAllLeft',
label: nls.localize('lines.deleteAllLeft', "Delete All Left"),
alias: 'Delete All Left',
precondition: EditorContextKeys.Writable,
kbOpts: {
kbExpr: EditorContextKeys.TextFocus,
primary: null,
mac: { primary: KeyMod.CtrlCmd | KeyCode.Backspace }
}
});
}
_getEndCursorState(primaryCursor: Range, rangesToDelete: Range[]): Selection[] {
let endPrimaryCursor: Range;
let endCursorState = [];
for (let i = 0, len = rangesToDelete.length; i < len; i++) {
let range = rangesToDelete[i];
let endCursor = new Selection(rangesToDelete[i].startLineNumber, rangesToDelete[i].startColumn, rangesToDelete[i].startLineNumber, rangesToDelete[i].startColumn);
if (range.intersectRanges(primaryCursor)) {
endPrimaryCursor = endCursor;
} else {
endCursorState.push(endCursor);
}
}
if (endPrimaryCursor) {
endCursorState.unshift(endPrimaryCursor);
}
return endCursorState;
}
_getRangesToDelete(editor: ICommonCodeEditor): Range[] {
let rangesToDelete: Range[] = editor.getSelections();
rangesToDelete.sort(Range.compareRangesUsingStarts);
rangesToDelete = rangesToDelete.map(selection => {
if (selection.isEmpty()) {
return new Range(selection.startLineNumber, 1, selection.startLineNumber, selection.startColumn);
} else {
return selection;
}
});
return rangesToDelete;
}
}
@editorAction
export class DeleteAllRightAction extends AbstractDeleteAllToBoundaryAction {
constructor() {
super({
id: 'deleteAllRight',
label: nls.localize('lines.deleteAllRight', "Delete All Right"),
alias: 'Delete All Right',
precondition: EditorContextKeys.Writable,
kbOpts: {
kbExpr: EditorContextKeys.TextFocus,
primary: null,
mac: { primary: KeyMod.WinCtrl | KeyCode.KEY_K, secondary: [KeyMod.CtrlCmd | KeyCode.Delete] }
}
});
}
_getEndCursorState(primaryCursor: Range, rangesToDelete: Range[]): Selection[] {
let endPrimaryCursor: Range;
let endCursorState = [];
for (let i = 0, len = rangesToDelete.length, offset = 0; i < len; i++) {
let range = rangesToDelete[i];
let endCursor = new Selection(range.startLineNumber - offset, range.startColumn, range.startLineNumber - offset, range.startColumn);
if (range.intersectRanges(primaryCursor)) {
endPrimaryCursor = endCursor;
} else {
endCursorState.push(endCursor);
}
}
if (endPrimaryCursor) {
endCursorState.unshift(endPrimaryCursor);
}
return endCursorState;
}
_getRangesToDelete(editor: ICommonCodeEditor): Range[] {
let model = editor.getModel();
let rangesToDelete: Range[] = editor.getSelections().map((sel) => {
if (sel.isEmpty()) {
const maxColumn = model.getLineMaxColumn(sel.startLineNumber);
if (sel.startColumn === maxColumn) {
return new Range(sel.startLineNumber, sel.startColumn, sel.startLineNumber + 1, 1);
} else {
return new Range(sel.startLineNumber, sel.startColumn, sel.startLineNumber, maxColumn);
}
}
return sel;
});
rangesToDelete.sort(Range.compareRangesUsingStarts);
return rangesToDelete;
}
}
@ -480,7 +518,7 @@ export class JoinLinesAction extends EditorAction {
public run(accessor: ServicesAccessor, editor: ICommonCodeEditor): void {
let selections = editor.getSelections();
let primarySelection = editor.getSelection();
let primaryCursor = editor.getSelection();
selections.sort(Range.compareRangesUsingStarts);
let reducedSelections: Selection[] = [];
@ -488,8 +526,8 @@ export class JoinLinesAction extends EditorAction {
let lastSelection = selections.reduce((previousValue, currentValue) => {
if (previousValue.isEmpty()) {
if (previousValue.endLineNumber === currentValue.startLineNumber) {
if (primarySelection.equalsSelection(previousValue)) {
primarySelection = currentValue;
if (primaryCursor.equalsSelection(previousValue)) {
primaryCursor = currentValue;
}
return currentValue;
}
@ -514,8 +552,8 @@ export class JoinLinesAction extends EditorAction {
let model = editor.getModel();
let edits = [];
let resultSelections = [];
let resultPrimarySelection = primarySelection;
let endCursorState = [];
let endPrimaryCursor = primaryCursor;
let lineOffset = 0;
for (let i = 0, len = reducedSelections.length; i < len; i++) {
@ -594,19 +632,19 @@ export class JoinLinesAction extends EditorAction {
}
}
if (Range.intersectRanges(deleteSelection, primarySelection) !== null) {
resultPrimarySelection = resultSelection;
if (Range.intersectRanges(deleteSelection, primaryCursor) !== null) {
endPrimaryCursor = resultSelection;
} else {
resultSelections.push(resultSelection);
endCursorState.push(resultSelection);
}
}
lineOffset += deleteSelection.endLineNumber - deleteSelection.startLineNumber;
}
editor.executeEdits(this.id, edits);
resultSelections.unshift(resultPrimarySelection);
editor.setSelections(resultSelections);
endCursorState.unshift(endPrimaryCursor);
editor.executeEdits(this.id, edits, endCursorState);
}
}

View file

@ -6,155 +6,160 @@
import * as assert from 'assert';
import { Selection } from 'vs/editor/common/core/selection';
import { Handler } from 'vs/editor/common/editorCommon';
import { withMockCodeEditor } from 'vs/editor/test/common/mocks/mockCodeEditor';
import { DeleteAllLeftAction, JoinLinesAction, TransposeAction, UpperCaseAction, LowerCaseAction, DeleteAllRightAction } from 'vs/editor/contrib/linesOperations/common/linesOperations';
suite('Editor Contrib - Line Operations', () => {
test('delete all left', function () {
withMockCodeEditor(
[
'one',
'two',
'three'
], {}, (editor, cursor) => {
let model = editor.getModel();
let deleteAllLeftAction = new DeleteAllLeftAction();
suite('DeleteAllLeftAction', () => {
test('should delete to the left of the cursor', function () {
withMockCodeEditor(
[
'one',
'two',
'three'
], {}, (editor, cursor) => {
let model = editor.getModel();
let deleteAllLeftAction = new DeleteAllLeftAction();
editor.setSelection(new Selection(1, 2, 1, 2));
deleteAllLeftAction.run(null, editor);
assert.equal(model.getLineContent(1), 'ne', '001');
editor.setSelection(new Selection(1, 2, 1, 2));
deleteAllLeftAction.run(null, editor);
assert.equal(model.getLineContent(1), 'ne', '001');
editor.setSelections([new Selection(2, 2, 2, 2), new Selection(3, 2, 3, 2)]);
deleteAllLeftAction.run(null, editor);
assert.equal(model.getLineContent(2), 'wo', '002');
assert.equal(model.getLineContent(3), 'hree', '003');
});
editor.setSelections([new Selection(2, 2, 2, 2), new Selection(3, 2, 3, 2)]);
deleteAllLeftAction.run(null, editor);
assert.equal(model.getLineContent(2), 'wo', '002');
assert.equal(model.getLineContent(3), 'hree', '003');
});
});
test('should work in multi cursor mode', function () {
withMockCodeEditor(
[
'hello',
'world',
'hello world',
'hello',
'bonjour',
'hola',
'world',
'hello world',
], {}, (editor, cursor) => {
let model = editor.getModel();
let deleteAllLeftAction = new DeleteAllLeftAction();
editor.setSelections([new Selection(1, 2, 1, 2), new Selection(1, 4, 1, 4)]);
deleteAllLeftAction.run(null, editor);
assert.equal(model.getLineContent(1), 'lo', '001');
editor.setSelections([new Selection(2, 2, 2, 2), new Selection(2, 4, 2, 5)]);
deleteAllLeftAction.run(null, editor);
assert.equal(model.getLineContent(2), 'ord', '002');
editor.setSelections([new Selection(3, 2, 3, 5), new Selection(3, 7, 3, 7)]);
deleteAllLeftAction.run(null, editor);
assert.equal(model.getLineContent(3), 'world', '003');
editor.setSelections([new Selection(4, 3, 4, 3), new Selection(4, 5, 5, 4)]);
deleteAllLeftAction.run(null, editor);
assert.equal(model.getLineContent(4), 'lljour', '004');
editor.setSelections([new Selection(5, 3, 6, 3), new Selection(6, 5, 7, 5), new Selection(7, 7, 7, 7)]);
deleteAllLeftAction.run(null, editor);
assert.equal(model.getLineContent(5), 'horlworld', '005');
});
});
});
test('delete all left in multi cursor mode', function () {
withMockCodeEditor(
[
'hello',
'world',
'hello world',
'hello',
'bonjour',
'hola',
'world',
'hello world',
], {}, (editor, cursor) => {
let model = editor.getModel();
let deleteAllLeftAction = new DeleteAllLeftAction();
suite('JoinLinesAction', () => {
test('should join lines and insert space if necessary', function () {
withMockCodeEditor(
[
'hello',
'world',
'hello ',
'world',
'hello ',
' world',
'hello ',
' world',
'',
'',
'hello world'
], {}, (editor, cursor) => {
let model = editor.getModel();
let joinLinesAction = new JoinLinesAction();
editor.setSelections([new Selection(1, 2, 1, 2), new Selection(1, 4, 1, 4)]);
deleteAllLeftAction.run(null, editor);
assert.equal(model.getLineContent(1), 'lo', '001');
editor.setSelection(new Selection(1, 2, 1, 2));
joinLinesAction.run(null, editor);
assert.equal(model.getLineContent(1), 'hello world', '001');
assert.deepEqual(editor.getSelection().toString(), new Selection(1, 6, 1, 6).toString(), '002');
editor.setSelections([new Selection(2, 2, 2, 2), new Selection(2, 4, 2, 5)]);
deleteAllLeftAction.run(null, editor);
assert.equal(model.getLineContent(2), 'ord', '002');
editor.setSelection(new Selection(2, 2, 2, 2));
joinLinesAction.run(null, editor);
assert.equal(model.getLineContent(2), 'hello world', '003');
assert.deepEqual(editor.getSelection().toString(), new Selection(2, 7, 2, 7).toString(), '004');
editor.setSelections([new Selection(3, 2, 3, 5), new Selection(3, 7, 3, 7)]);
deleteAllLeftAction.run(null, editor);
assert.equal(model.getLineContent(3), 'world', '003');
editor.setSelection(new Selection(3, 2, 3, 2));
joinLinesAction.run(null, editor);
assert.equal(model.getLineContent(3), 'hello world', '005');
assert.deepEqual(editor.getSelection().toString(), new Selection(3, 7, 3, 7).toString(), '006');
editor.setSelections([new Selection(4, 3, 4, 3), new Selection(4, 5, 5, 4)]);
deleteAllLeftAction.run(null, editor);
assert.equal(model.getLineContent(4), 'lljour', '004');
editor.setSelection(new Selection(4, 2, 5, 3));
joinLinesAction.run(null, editor);
assert.equal(model.getLineContent(4), 'hello world', '007');
assert.deepEqual(editor.getSelection().toString(), new Selection(4, 2, 4, 8).toString(), '008');
editor.setSelections([new Selection(5, 3, 6, 3), new Selection(6, 5, 7, 5), new Selection(7, 7, 7, 7)]);
deleteAllLeftAction.run(null, editor);
assert.equal(model.getLineContent(5), 'horlworld', '005');
});
});
editor.setSelection(new Selection(5, 1, 7, 3));
joinLinesAction.run(null, editor);
assert.equal(model.getLineContent(5), 'hello world', '009');
assert.deepEqual(editor.getSelection().toString(), new Selection(5, 1, 5, 3).toString(), '010');
});
});
test('join lines', function () {
withMockCodeEditor(
[
'hello',
'world',
'hello ',
'world',
'hello ',
' world',
'hello ',
' world',
'',
'',
'hello world'
], {}, (editor, cursor) => {
let model = editor.getModel();
let joinLinesAction = new JoinLinesAction();
test('should work in multi cursor mode', function () {
withMockCodeEditor(
[
'hello',
'world',
'hello ',
'world',
'hello ',
' world',
'hello ',
' world',
'',
'',
'hello world'
], {}, (editor, cursor) => {
let model = editor.getModel();
let joinLinesAction = new JoinLinesAction();
editor.setSelection(new Selection(1, 2, 1, 2));
joinLinesAction.run(null, editor);
assert.equal(model.getLineContent(1), 'hello world', '001');
assert.deepEqual(editor.getSelection().toString(), new Selection(1, 6, 1, 6).toString(), '002');
editor.setSelections([
/** primary cursor */
new Selection(5, 2, 5, 2),
new Selection(1, 2, 1, 2),
new Selection(3, 2, 4, 2),
new Selection(5, 4, 6, 3),
new Selection(7, 5, 8, 4),
new Selection(10, 1, 10, 1)
]);
editor.setSelection(new Selection(2, 2, 2, 2));
joinLinesAction.run(null, editor);
assert.equal(model.getLineContent(2), 'hello world', '003');
assert.deepEqual(editor.getSelection().toString(), new Selection(2, 7, 2, 7).toString(), '004');
joinLinesAction.run(null, editor);
assert.equal(model.getLinesContent().join('\n'), 'hello world\nhello world\nhello world\nhello world\n\nhello world', '001');
assert.deepEqual(editor.getSelections().toString(), [
/** primary cursor */
new Selection(3, 4, 3, 8),
new Selection(1, 6, 1, 6),
new Selection(2, 2, 2, 8),
new Selection(4, 5, 4, 9),
new Selection(6, 1, 6, 1)
].toString(), '002');
editor.setSelection(new Selection(3, 2, 3, 2));
joinLinesAction.run(null, editor);
assert.equal(model.getLineContent(3), 'hello world', '005');
assert.deepEqual(editor.getSelection().toString(), new Selection(3, 7, 3, 7).toString(), '006');
editor.setSelection(new Selection(4, 2, 5, 3));
joinLinesAction.run(null, editor);
assert.equal(model.getLineContent(4), 'hello world', '007');
assert.deepEqual(editor.getSelection().toString(), new Selection(4, 2, 4, 8).toString(), '008');
editor.setSelection(new Selection(5, 1, 7, 3));
joinLinesAction.run(null, editor);
assert.equal(model.getLineContent(5), 'hello world', '009');
assert.deepEqual(editor.getSelection().toString(), new Selection(5, 1, 5, 3).toString(), '010');
});
});
test('join lines in multi cursor mode', function () {
withMockCodeEditor(
[
'hello',
'world',
'hello ',
'world',
'hello ',
' world',
'hello ',
' world',
'',
'',
'hello world'
], {}, (editor, cursor) => {
let model = editor.getModel();
let joinLinesAction = new JoinLinesAction();
editor.setSelections([
/** primary cursor */
new Selection(5, 2, 5, 2),
new Selection(1, 2, 1, 2),
new Selection(3, 2, 4, 2),
new Selection(5, 4, 6, 3),
new Selection(7, 5, 8, 4),
new Selection(10, 1, 10, 1)
]);
joinLinesAction.run(null, editor);
assert.equal(model.getLinesContent().join('\n'), 'hello world\nhello world\nhello world\nhello world\n\nhello world', '001');
assert.deepEqual(editor.getSelections().toString(), [
/** primary cursor */
new Selection(3, 4, 3, 8),
new Selection(1, 6, 1, 6),
new Selection(2, 2, 2, 8),
new Selection(4, 5, 4, 9),
new Selection(6, 1, 6, 1)
].toString(), '002');
/** primary cursor */
assert.deepEqual(editor.getSelection().toString(), new Selection(3, 4, 3, 8).toString(), '003');
});
assert.deepEqual(editor.getSelection().toString(), new Selection(3, 4, 3, 8).toString(), '003');
});
});
});
test('transpose', function () {
@ -447,5 +452,40 @@ suite('Editor Contrib - Line Operations', () => {
]);
});
});
test('should work with undo/redo', () => {
withMockCodeEditor([
'hello',
'there',
'world'
], {}, (editor, cursor) => {
const model = editor.getModel();
const action = new DeleteAllRightAction();
editor.setSelections([
new Selection(1, 3, 1, 3),
new Selection(1, 6, 1, 6),
new Selection(3, 4, 3, 4),
]);
action.run(null, editor);
assert.deepEqual(model.getLinesContent(), ['hethere', 'wor']);
assert.deepEqual(editor.getSelections(), [
new Selection(1, 3, 1, 3),
new Selection(2, 4, 2, 4)
]);
cursor.trigger('tests', Handler.Undo, {});
assert.deepEqual(editor.getSelections(), [
new Selection(1, 3, 1, 3),
new Selection(1, 6, 1, 6),
new Selection(3, 4, 3, 4)
]);
cursor.trigger('tests', Handler.Redo, {});
assert.deepEqual(editor.getSelections(), [
new Selection(1, 3, 1, 3),
new Selection(2, 4, 2, 4)
]);
});
});
});
});

7
src/vs/monaco.d.ts vendored
View file

@ -3163,11 +3163,12 @@ declare module monaco.editor {
*/
pushUndoStop(): boolean;
/**
* Execute a command on the editor.
* Execute edits on the editor.
* @param source The source of the call.
* @param command The command to execute
* @param edits The edits to execute.
* @param endCursoState Cursor state after the edits were applied.
*/
executeEdits(source: string, edits: IIdentifiedSingleEditOperation[]): boolean;
executeEdits(source: string, edits: IIdentifiedSingleEditOperation[], endCursoState?: Selection[]): boolean;
/**
* Execute multiple (concommitent) commands on the editor.
* @param source The source of the call.