Skip to content

feat: Support multi cursor line moves #68695

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 13 commits into
base: main
Choose a base branch
from

Conversation

2mas
Copy link

@2mas 2mas commented Feb 14, 2019

Fixes #68694 moving multiple consequitive lines

@msftclas
Copy link

msftclas commented Feb 14, 2019

CLA assistant check
All CLA requirements met.

@2mas
Copy link
Author

2mas commented Feb 14, 2019

Needs some more work, lines get scrambled

@2mas 2mas closed this Feb 14, 2019
@2mas 2mas reopened this Feb 14, 2019
@kieferrm kieferrm requested a review from rebornix February 15, 2019 03:31
@2mas
Copy link
Author

2mas commented Feb 16, 2019

Perhaps a comment about why selections needs to be reversed and executed individually should be added for clarity, tell me if I can assist more

@2mas
Copy link
Author

2mas commented Feb 28, 2019

Latest commit covers scenario when selection-direction goes upwards. Reversing the processing of selections should only occur when lines are moved in the same direction as the selections are made.

Also had to make the selections distinct by line. Keeping the selections can probably be solved too since their column-positions wont be changed. If you're interested in this approach in this PR I can give that a try aswell

temp

@2mas
Copy link
Author

2mas commented Mar 1, 2019

Decided to implement the stashing of selections anyway, also added a block when top/end of doc is reached. I'm probably done by now. :)

@2mas
Copy link
Author

2mas commented Mar 28, 2019

@rebornix any status on this? Just curious

@rebornix
Copy link
Member

@2mas let's shoot for next week (beginning of next iteration). BTW, I'd love to see some tests as well but I can do that if the time doesn't work for you.

@rebornix rebornix added this to the On Deck milestone Mar 28, 2019
@2mas
Copy link
Author

2mas commented Mar 31, 2019

Awesome, I’ll see if I can come up with something (limited available time I’m afraid).

@2mas
Copy link
Author

2mas commented Apr 9, 2019

Added a few testcases and resolved a conflict with master, should be good to test-drive now.

@rebornix rebornix added editor-autoindent Editor auto indentation issues feature-request Request for new features or functionality labels Oct 8, 2020
@joaomoreno joaomoreno changed the base branch from master to main February 15, 2021 08:51
@vivodi
Copy link

vivodi commented Dec 29, 2024

Is this still relevant?

@2mas
Copy link
Author

2mas commented Jan 1, 2025

Is this still relevant?

It seems so, the issue is still there in the editor and keeps annoying users apparently. - #11809 (comment)

No idea about the state of the code in my PR today though, spent some time on it years ago to make it good to testdrive and merge but it could have fallen short in prioritization since its probably not impacting the majority of the users, or there is something else about the solution that stops it

@rebornix
Copy link
Member

This change requires comprehensive testing before merging. At the same time I'm not sure if we are investing more in the textmate indentation rules. I'll leave this to @aiday-mar to decide.

@rebornix rebornix assigned aiday-mar and unassigned rebornix Jan 21, 2025
@aiday-mar aiday-mar requested review from aiday-mar and removed request for rebornix May 7, 2025 10:05
Copy link
Contributor

@aiday-mar aiday-mar left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hi thank you for making this PR and apologies for the delay in reviewing this PR. I think this PR would be a very valuable contribution. I have looked at it, and I think the approach is overall correct but I have several questions as well as changes I think we should make before merging. It would be good to also test this PR on many different corner cases to make sure it does not introduce bugs. I will do that too. There is also a merge conflict which needs to be resolved first.


editor.pushUndoStop();
} else {
editor.pushUndoStop();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could we unify this part of the code between the two cases? We could construct the commands array entirely first and then call executeCommands on this commands array. Then after this, if we have moved multiple lines, we can set the selections.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This didn't work out for me. For some reason it just doesn't process all of the commands


for (const selection of selections) {
commands.push(new MoveLinesCommand(selection, this.down, autoIndent));
if (movingMultipleLines) {
// If we are at the beginning/end of document and multiple lines are moved, abort.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we need to do this check for whether the selection is at the beginning or end of the document? The other case when we are not moving multiple lines does not do such a check, and it would be good to unify as much as possible the code. Instead of doing the check, when we calculate the new selections, if one of the selections is on the first line and we move up, the new selection should be the same as the old selection, because as I assume, the operation would be a no-op in this case.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is actually one thing that would be best if you decide on probably, we abort the whole thing here to keep the "format" of the selections, for example if we have these two lines selected on top of the document and move up (line 5 & 6), it will be a no-op currently and we maintain one line between them.

image

If we remove this block, it will continue with the second selection and move it up, this may be a good thing to do:
image

And if we move it up again it will replace the first selection (since the first one is processed first and stays in place/no-op, the second one will be processed afterwards and take its place, putting the first one below). To prevent this there could be a check for an existing selection on the line above/below and no-op in that case. I'll see if I can come up with something clean and easy for this
image

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this behavior is okay. We could just not treat this case when the selection is at the beginning or at the end and wait to see what users think.

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

// Work only with one selection per line
Copy link
Contributor

@aiday-mar aiday-mar Jun 5, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's put this filter operation before the for loop above, so as not to loop over uneccessary selections in the for loop.

Also instead of doing a filter operation followed by a map operation followed by an indexOf operation which is O(n^2) where n is the number of selections could we just loop once over the selections, keep track of the endLineNumber of the last processed selection and if the current selection does not match this endLineNumber add the current selection to a pre-created selections array. This will allow us to reduce the complexity to O(n).

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Great catch, will fix

return arr.map(sel => sel['endLineNumber']).indexOf(s['endLineNumber']) === idx;
});

selectionDirectionDown = selections[0].endLineNumber < selections[selections.length - 1].endLineNumber;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am not sure I understand this piece of code. Why do we calculate the selectionDirectionDown. What does it mean?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We need to know if the selection direction is down or up (Add Next Occurrence vs Add Previous Occurrence) and if we are initiating a down-move, then we need to process the selections in reversed order from the bottom and up, otherwise they would replace each other. I will try to simplify and clarify this with a comment

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In that case is not sufficient to just check if we are initiating a downwards movement? I am not exactly sure, but it seems to me selections are not necessarily ordered from top to bottom.

Could you give a concrete example for what this code solves?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, if I do this:
image

And select some lines with Add Previous Occurrence:
image

And move down:
image

vs

image

Add Previous Occurrence:
image

move down:
image

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's because "line 3" gets processed first -> moved to row 6, then the next selection is processed on row 6, which is now "line 3" -> moved to row 7, next selection gets processed on row 7, which is "line 3" -> ends up on row 8. So this is why process order must be right :)

if (movingMultipleLines) {
// Stash selections while processing, set new selections +/- line-change only
for (const selection of selections) {
let startCol = selection.getStartPosition().column;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

To unify the code between the two cases, you can just calculate the new selections array in both cases (whether there are multiple selections or not). And then at the very end set the new selections in all cases. This will help avoid these if/else branching.

@aiday-mar aiday-mar changed the title Fixes #68694 moving multiple consequitive lines Allowing to move multiple consecutive lines Jun 5, 2025
@2mas
Copy link
Author

2mas commented Jun 10, 2025

Hi thank you for making this PR and apologies for the delay in reviewing this PR. I think this PR would be a very valuable contribution. I have looked at it, and I think the approach is overall correct but I have several questions as well as changes I think we should make before merging. It would be good to also test this PR on many different corner cases to make sure it does not introduce bugs. I will do that too. There is also a merge conflict which needs to be resolved first.

Hi! Nice to see that you find this useful, let me get back with resolved conflicts and some adjustments and clarifications.

I have some issues running this project locally though, but I managed to get the web-editor working at least, but tests are only running half-way for some reason, perhaps I can rely on CI anyway, I'll do my best effort

@aiday-mar
Copy link
Contributor

What issues are you seeing while running this project locally? You should be able to compile as well as run the tests and test with OSS

2mas added 7 commits June 10, 2025 13:28
Correcting multiple move-lines when having made selections in reversed order
… line. Applies stash after line movement to keep previous

selections.

Aborting when moving multiple lines and the line is currently at top or end of document
@2mas
Copy link
Author

2mas commented Jun 10, 2025

What issues are you seeing while running this project locally? You should be able to compile as well as run the tests and test with OSS

Firstly tried running test.sh and code.sh in WSL when cloning into devcontainer which failed, can't remember what was logged. I got closest to full experience with running directly on Windows with node v22.15.1, python 3.13.0 and VS 2022 + workload and individual components installed as per docs, I get these types of issues when running locally in Windows:

  • npm install
  • npm run watch

.\scripts\code.bat actually worked on some more attempts now, seems to be some intermittent error previously
.\scripts\test.bat doesn't seem to run all tests, it prints this at the end:

Found --crash-reporter-directory argument. Setting crashDumps directory to be 'C:\Projects\vscode\.build\crashes'
[20020:0610/140701.484:INFO:CONSOLE(2)] "The vm module of Node.js is unsupported in Electron's renderer process due to incompatibilities with the Blink rendering engine. Crashes are likely and avoiding the module is highly recommended. This module may be removed in a future release.", source: node:electron/js2c/renderer_init (2)
[20020:0610/140701.797:INFO:CONSOLE(2)] "Platform browser has already been set. Overwriting the platform with [object Object].", source: C:\Projects\vscode\node_modules\@vscode\vscode-languagedetection\dist\lib\index.js (2)
(node:30204) [DEP0040] DeprecationWarning: The `punycode` module is deprecated. Please use a userland alternative instead.
(Use `Code - OSS --trace-deprecation ...` to show where the warning was created)
(node:30204) [DEP0025] DeprecationWarning: sys is deprecated. Use util instead.
(node:30204) ExperimentalWarning: WASI is an experimental feature and might change at any time

...

322 passing (2s)
  2 pending
  2 failing

...

2) "after each" hook for "English File menu renders mnemonics":
      + expected - actual

      -false
      +true
  AssertionError [ERR_ASSERTION]: Error: Unexpected console output in test run. Please ensure no console.[log|error|info|warn] usage in tests or runtime errors.
      at Context.<anonymous> (renderer.js:291:11)

errorlevel: 1

@2mas 2mas force-pushed the fix-multiple-consecutive-line-movements branch from 493b319 to 076ad04 Compare June 10, 2025 12:09
@aiday-mar
Copy link
Contributor

What exactly is the error you see when running npm i and npm run watch on Windows? This could be happening because you need to merge main and recompile first.

@2mas
Copy link
Author

2mas commented Jun 12, 2025

I have pushed some updates and covered some edge cases as well, for example if one puts cursors on multiple lines with no order, we will still sort them and apply the move only once per line.

There are some conditions in the code that look a bit complex, tried to explain as comments. Also, I'm not able to run tests locally and I can see from CI that something is wrong, maybe you can assist me with this?

@2mas 2mas changed the title Allowing to move multiple consecutive lines feat: Support multi cursor line moves Jun 13, 2025
@2mas
Copy link
Author

2mas commented Jun 13, 2025

PR title updated to better reflect what this does, previous one was not so good since multiple lines can be moved within a single selection, and this PR solves more than that

@aiday-mar
Copy link
Contributor

Hi thanks for this PR. Today I am a bit busy, but I will make sure to look at this soon.

@2mas
Copy link
Author

2mas commented Jun 13, 2025

Hi thanks for this PR. Today I am a bit busy, but I will make sure to look at this soon.

No stress at all. I got the test runner working now (not the .bat, but the integrated runner in vscode started working) so test suite is now repaired

@aiday-mar aiday-mar enabled auto-merge (squash) June 17, 2025 10:58
@aiday-mar
Copy link
Contributor

Hi thanks for the PR. I pushed some polish changes. I need to discuss this PR with my team, but hopefully this can get merged in.

@2mas
Copy link
Author

2mas commented Jun 17, 2025

Hi! Order of the initial selections are now lost since the list is already sorted from beginning, but since it requires an extra loop for it, it may not be super important, up to you. Probably its the cause for failing tests.

However the continue doesn't work that well, I'll show a small example

image
Moving down gets them unordered and selections are lost
image

Would it be better to keep the return here, otherwise the nearest neighbour doesn't know that the top/bottom line was a no-op and takes its place?

--
I had some uncommitted code to remove the movingMultipleLines branching btw, it wasn't actually necessary since there was a return to abort the operation. I'll add it here if you find something useful to pick from it (also covers the case with multiple cursors on a single row only, they will disappear as it now):

public run(accessor: ServicesAccessor, editor: ICodeEditor): void {
		const languageConfigurationService = accessor.get(ILanguageConfigurationService);
		const autoIndent = editor.getOption(EditorOption.autoIndent);

		const selections = editor.getSelections() || [];
		const distinctSelectionsPerLine: Selection[] = [];
		const selectionsAfterLineMove: Selection[] = [];

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

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

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

		selections.sort((a, b) => a.endLineNumber - b.endLineNumber);
		let lastLineNumber = 0;

		for (const selection of selections) {
			if (lastLineNumber !== selection.endLineNumber) {
				distinctSelectionsPerLine.push(selection);
				lastLineNumber = selection.endLineNumber;
			}
		}

		if (this.down) {
			distinctSelectionsPerLine.reverse();
		}

		const model = editor.getModel();
		const lineCount = model !== null ? model.getLineCount() : 0;

		editor.pushUndoStop();

		for (const selection of distinctSelectionsPerLine) {
			if ((selection.startLineNumber === 1 && !this.down) || (selection.endLineNumber === lineCount && this.down)) {
				editor.pushUndoStop();
				return;
			}

			editor.executeCommand(this.id, new MoveLinesCommand(selection, this.down, autoIndent, languageConfigurationService));
		}

		editor.setSelections(selectionsAfterLineMove);
		editor.pushUndoStop();
	}

@aiday-mar
Copy link
Contributor

Hi! Thanks I will have a look again at your comment later this week!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
changes-requested editor-autoindent Editor auto indentation issues feature-request Request for new features or functionality
Projects
None yet
Development

Successfully merging this pull request may close these issues.

support to move multiple lines through alt+arrow
5 participants