Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
65 changes: 60 additions & 5 deletions src/vs/editor/contrib/linesOperations/browser/linesOperations.ts
Original file line number Diff line number Diff line change
Expand Up @@ -171,23 +171,78 @@ abstract class AbstractMoveLinesAction extends EditorAction {
}

public run(accessor: ServicesAccessor, editor: ICodeEditor): void {

const model = editor.getModel();
if (!model) {
return;
}

const languageConfigurationService = accessor.get(ILanguageConfigurationService);

const commands: ICommand[] = [];
const selections = editor.getSelections() || [];
const autoIndent = editor.getOption(EditorOption.autoIndent);

selections.sort((a, b) => {
if (this.down) {
if (a.startLineNumber === b.startLineNumber) {
return b.endLineNumber - a.endLineNumber;
} else {
return b.startLineNumber - a.startLineNumber;
}
} else {
if (a.startLineNumber === b.startLineNumber) {
return a.endLineNumber - b.endLineNumber;
} else {
return a.startLineNumber - b.startLineNumber;
}
}
});

const nonIntersectingSelections: Selection[] = [];
const newSelections: Selection[] = [];
const lineCount = model.getLineCount();

let lastStartLineNumber = lineCount;
let lastEndLineNumber = 0;

for (const selection of selections) {
commands.push(new MoveLinesCommand(selection, this.down, autoIndent, languageConfigurationService));
if (this.down && selection.endLineNumber >= lastStartLineNumber || this.down && selection.endLineNumber === lineCount) {
continue;
}
if (!this.down && selection.startLineNumber <= lastEndLineNumber || !this.down && selection.startLineNumber === 1) {
continue;
}

nonIntersectingSelections.push(selection);

const startPosition = selection.getStartPosition();
const startCol = startPosition.column;
const startLine = startPosition.lineNumber;
const newStartLine = this.down ? startLine + 1 : startLine - 1;

const endPosition = selection.getEndPosition();
const endCol = endPosition.column;
const endLine = endPosition.lineNumber;
const newEndLine = this.down ? endLine + 1 : endLine - 1;

newSelections.push(new Selection(newStartLine, startCol, newEndLine, endCol));

lastEndLineNumber = selection.endLineNumber;
lastStartLineNumber = selection.startLineNumber;
}

editor.pushUndoStop();
editor.executeCommands(this.id, commands);
for (const selection of nonIntersectingSelections) {
editor.executeCommands(this.id, [new MoveLinesCommand(selection, this.down, autoIndent, languageConfigurationService)]);
}
if (newSelections) {
editor.setSelections(newSelections);
}
editor.pushUndoStop();
}
}

class MoveLinesUpAction extends AbstractMoveLinesAction {
export class MoveLinesUpAction extends AbstractMoveLinesAction {
constructor() {
super(false, {
id: 'editor.action.moveLinesUpAction',
Expand All @@ -209,7 +264,7 @@ class MoveLinesUpAction extends AbstractMoveLinesAction {
}
}

class MoveLinesDownAction extends AbstractMoveLinesAction {
export class MoveLinesDownAction extends AbstractMoveLinesAction {
constructor() {
super(true, {
id: 'editor.action.moveLinesDownAction',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,10 @@ import { Selection } from '../../../../common/core/selection.js';
import { Handler } from '../../../../common/editorCommon.js';
import { ITextModel } from '../../../../common/model.js';
import { ViewModel } from '../../../../common/viewModel/viewModelImpl.js';
import { CamelCaseAction, PascalCaseAction, DeleteAllLeftAction, DeleteAllRightAction, DeleteDuplicateLinesAction, DeleteLinesAction, IndentLinesAction, InsertLineAfterAction, InsertLineBeforeAction, JoinLinesAction, KebabCaseAction, LowerCaseAction, SnakeCaseAction, SortLinesAscendingAction, SortLinesDescendingAction, TitleCaseAction, TransposeAction, UpperCaseAction } from '../../browser/linesOperations.js';
import { withTestCodeEditor } from '../../../../test/browser/testCodeEditor.js';
import { CamelCaseAction, PascalCaseAction, DeleteAllLeftAction, DeleteAllRightAction, DeleteDuplicateLinesAction, DeleteLinesAction, IndentLinesAction, InsertLineAfterAction, InsertLineBeforeAction, JoinLinesAction, KebabCaseAction, LowerCaseAction, SnakeCaseAction, SortLinesAscendingAction, SortLinesDescendingAction, TitleCaseAction, TransposeAction, UpperCaseAction, MoveLinesUpAction, MoveLinesDownAction } from '../../browser/linesOperations.js';
import { createCodeEditorServices, withTestCodeEditor } from '../../../../test/browser/testCodeEditor.js';
import { createTextModel } from '../../../../test/common/testTextModel.js';
import { DisposableStore } from '../../../../../base/common/lifecycle.js';

function assertSelection(editor: ICodeEditor, expected: Selection | Selection[]): void {
if (!Array.isArray(expected)) {
Expand Down Expand Up @@ -1640,4 +1641,232 @@ suite('Editor Contrib - Line Operations', () => {
]
);
});

// Multi cursor/selections line move actions
suite('Issue #68694 support to move multiple lines through alt+arrow', () => {

test('Move lines down', () => {
const disposables = new DisposableStore();
const instantiationService = createCodeEditorServices(disposables);

const TEXT = [
'Line 1',
'Line 2',
'Line 3',
'Line 4',
'Line 5'
];
withTestCodeEditor(TEXT, {}, (editor) => {
editor.setSelections([
new Selection(1, 1, 1, 1),
new Selection(2, 1, 2, 1),
]);

const moveLinesDownAction = new MoveLinesDownAction();
moveLinesDownAction.run(instantiationService, editor);

const model = editor.getModel()!;

assert.deepStrictEqual(model.getLinesContent(), [
'Line 3',
'Line 1',
'Line 2',
'Line 4',
'Line 5'
]);

assert.deepStrictEqual(
editor.getSelections()!.toString(), [
new Selection(2, 1, 2, 1),
new Selection(3, 1, 3, 1),
].toString());
});

disposables.dispose();
});

test('Move lines up', () => {
const disposables = new DisposableStore();
const instantiationService = createCodeEditorServices(disposables);

const TEXT = [
'Line 1',
'Line 2',
'Line 3',
'Line 4',
'Line 5'
];
withTestCodeEditor(TEXT, {}, (editor) => {
editor.setSelections([
new Selection(3, 1, 3, 1),
new Selection(4, 1, 4, 1),
]);

const moveLinesUpAction = new MoveLinesUpAction();
moveLinesUpAction.run(instantiationService, editor);

const model = editor.getModel()!;

assert.deepStrictEqual(model.getLinesContent(), [
'Line 1',
'Line 3',
'Line 4',
'Line 2',
'Line 5'
]);

assert.deepStrictEqual(
editor.getSelections()!.toString(), [
new Selection(2, 1, 2, 1),
new Selection(3, 1, 3, 1),
].toString());

disposables.dispose();
});
});

test('Combining up and down', () => {
const disposables = new DisposableStore();
const instantiationService = createCodeEditorServices(disposables);

const TEXT = [
'Line 1',
'Line 2',
'Line 3',
'Line 4',
'Line 5'
];
withTestCodeEditor(TEXT, {}, (editor) => {
editor.setSelections([
new Selection(1, 1, 1, 1),
new Selection(2, 1, 2, 1),
]);

const moveLinesUpAction = new MoveLinesUpAction();
const moveLinesDownAction = new MoveLinesDownAction();

const model = editor.getModel()!;

moveLinesUpAction.run(instantiationService, editor); // no change since selections are already at the beginning of the file = no-op

assert.deepStrictEqual(model.getLinesContent(), [
'Line 1',
'Line 2',
'Line 3',
'Line 4',
'Line 5'
]);

moveLinesDownAction.run(instantiationService, editor);

assert.deepStrictEqual(model.getLinesContent(), [
'Line 3',
'Line 1',
'Line 2',
'Line 4',
'Line 5'
]);

moveLinesDownAction.run(instantiationService, editor);

assert.deepStrictEqual(model.getLinesContent(), [
'Line 3',
'Line 4',
'Line 1',
'Line 2',
'Line 5'
]);

assert.deepStrictEqual(
editor.getSelections()!.toString(), [
new Selection(3, 1, 3, 1),
new Selection(4, 1, 4, 1),
].toString());

disposables.dispose();
});
});

test('Move non ascending selections down', () => {
const disposables = new DisposableStore();
const instantiationService = createCodeEditorServices(disposables);

const TEXT = [
'Line 1',
'Line 2',
'Line 3',
'Line 4',
'Line 5'
];
withTestCodeEditor(TEXT, {}, (editor) => {
editor.setSelections([
new Selection(4, 1, 4, 1),
new Selection(3, 1, 3, 1),
]);

const moveLinesDownAction = new MoveLinesDownAction();
moveLinesDownAction.run(instantiationService, editor);

const model = editor.getModel()!;

assert.deepStrictEqual(model.getLinesContent(), [
'Line 1',
'Line 2',
'Line 5',
'Line 3',
'Line 4'
]);

assert.deepStrictEqual(
editor.getSelections()!.toString(), [
new Selection(5, 1, 5, 1),
new Selection(4, 1, 4, 1),
].toString());

disposables.dispose();
});
});

test('Move multiple selections per line', () => {
const disposables = new DisposableStore();
const instantiationService = createCodeEditorServices(disposables);

const TEXT = [
'Line 1',
'Line 2',
'Line 3',
'Line 4',
'Line 5'
];
withTestCodeEditor(TEXT, {}, (editor) => {
editor.setSelections([
new Selection(3, 1, 3, 1),
new Selection(4, 1, 4, 1),
new Selection(3, 2, 3, 2),
]);

const moveLinesDownAction = new MoveLinesDownAction();
moveLinesDownAction.run(instantiationService, editor);

const model = editor.getModel()!;

assert.deepStrictEqual(model.getLinesContent(), [
'Line 1',
'Line 2',
'Line 5',
'Line 3',
'Line 4'
]);

assert.deepStrictEqual(
editor.getSelections()!.toString(), [
new Selection(4, 1, 4, 1),
new Selection(5, 1, 5, 1),
new Selection(4, 2, 4, 2),
].toString());

disposables.dispose();
});
});
});
});
Loading