diff --git a/src/vs/editor/contrib/linesOperations/browser/linesOperations.ts b/src/vs/editor/contrib/linesOperations/browser/linesOperations.ts index 0ca8dd2abfb466..9235ff3b681b97 100644 --- a/src/vs/editor/contrib/linesOperations/browser/linesOperations.ts +++ b/src/vs/editor/contrib/linesOperations/browser/linesOperations.ts @@ -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', @@ -209,7 +264,7 @@ class MoveLinesUpAction extends AbstractMoveLinesAction { } } -class MoveLinesDownAction extends AbstractMoveLinesAction { +export class MoveLinesDownAction extends AbstractMoveLinesAction { constructor() { super(true, { id: 'editor.action.moveLinesDownAction', diff --git a/src/vs/editor/contrib/linesOperations/test/browser/linesOperations.test.ts b/src/vs/editor/contrib/linesOperations/test/browser/linesOperations.test.ts index 199b22c12bac4b..f067349e82f5d7 100644 --- a/src/vs/editor/contrib/linesOperations/test/browser/linesOperations.test.ts +++ b/src/vs/editor/contrib/linesOperations/test/browser/linesOperations.test.ts @@ -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)) { @@ -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(); + }); + }); + }); });