-
-
Notifications
You must be signed in to change notification settings - Fork 22.2k
Add Multi-caret support to TextEdit #61902
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
Conversation
3e84740
to
c211042
Compare
a7e00e8
to
be8ed21
Compare
Editor PR review meeting: Needs review/testing, really nice and wanted feature though |
d3f709e
to
6f72a94
Compare
Thanks for making this! I haven't really used mutli-carets on any IDE and I don't have any stylistic prejudices. Also since it's an initial work, I'll not discuss usability. So here's just a good ol' dump of bugs instead:
|
Rebased and thanks for testing, hopefully fixed most of those issues. Regarding |
editor/code_editor.cpp
Outdated
Vector<int> cursor_columns; | ||
for (int c = 0; c < text_editor->get_caret_count(); c++) { | ||
cursor_columns.push_back(text_editor->get_caret_column(c)); | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Same.
scene/gui/code_edit.cpp
Outdated
remove_text(get_caret_line(), get_caret_column(), get_caret_line(), get_caret_column() + 1); | ||
// Remove the old character if in overtype mode and no selection. | ||
if (is_overtype_mode_enabled() && !had_selection) { | ||
/* Make sure we don't try and remove empty space. */ |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
/* Make sure we don't try and remove empty space. */ | |
// Make sure we don't try and remove empty space. |
scene/gui/code_edit.cpp
Outdated
/* This value informs us by how much we changed selection position by indenting right. */ | ||
/* Default is 1 for tab indentation. */ |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
/* This value informs us by how much we changed selection position by indenting right. */ | |
/* Default is 1 for tab indentation. */ | |
// This value informs us by how much we changed selection position by indenting right. | |
// Default is 1 for tab indentation. |
This comment style is just used everywhere in the codebase.
Same below.
scene/gui/text_edit.cpp
Outdated
set_caret_line(caret.line - 1); | ||
set_caret_column(text[caret.line].length()); | ||
for (int i = 0; i < carets.size(); i++) { | ||
// Handle selection |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
// Handle selection | |
// Handle selection. |
Could as well fix the comments if they are being moved.
Ah right
-1 for all carets This should be mentioned somewhere in the docs, probably in the class description or |
scene/gui/text_edit.cpp
Outdated
String clipboard = text[cl]; | ||
DisplayServer::get_singleton()->clipboard_set(clipboard); | ||
set_caret_column(0); | ||
// Check for overlaping carrets |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
// Check for overlaping carrets | |
// Check for overlaping carrots |
jk
@@ -42,6 +42,14 @@ class TextEdit : public Control { | |||
GDCLASS(TextEdit, Control); | |||
|
|||
public: | |||
/* Edit Actions. */ |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Unnecessary comment.
...although I see there are similar comments below :/
It only repeats enum name.
@@ -361,19 +377,39 @@ class TextEdit : public Control { | |||
int _get_char_pos_for_line(int p_px, int p_line, int p_wrap_index = 0) const; | |||
|
|||
/* Caret. */ |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think this comment is out of place now, no?
I checked through the code and it looks ok, I think. ~80% is just moving things around and adding caret index. Can't say much about the feature implementation, because it's rather complex, but I assume it's good, because it works xdd I left some comments, mostly some useless bikeshedding about comments. We use From more important stuff, there is this weird paste behavior I mentioned above. Although I used VS Code as reference, so not sure how it works in other editors. #61902 (comment) is rather important too, because the PR just adds I don't use multiple carets much, but I think a nice-to-have feature would be rectangular selection: |
Rider has the same behaviour as VS code, I think it is the expected behaviour. rider64_KWbv4gh2aZ.mp4 |
Pasting should now work like |
Thanks! |
It's an edge case, but might need some extra indication (I do not know what exactly will be appropriate here):
Screen.Recording.2022-10-06.at.10.04.07.mov |
This PR implements the initial support for multiple carets in
TextEdit
. AdjustedCodeEdit
and the Editor to support them.This will most likely be the last major feature that I have time to make for 4.0.
Will look at adding multi-caret tests in a separate PR(s), as those will take a little while as this change is quite large.
For the initial support currently it's pretty bare bones, it's enabled by default. You can add an additional caret by
Alt + LeftClick
. Typing will insert at all carets etc. Caret '0' is the main caret, the viewport will always follow this caret unless selecting via the mouse with a secondary caret. Each caret also has it's own selection.I've tried to make the behaviour when using multiple carets intuitive based on VSCode, SublimeText and QTCreator, but even between them there are a lot of differences and bugs. Not sure on performance with a lot of carets due to the checks etc but happy to optimise after.
In terms of API changes most of the viewport, caret and edit methods now take in a caret index, the caret index is one of the following:
-1
for all carets0
for the main caret1-x
for the restEach method is defaulted as appropriate so existing code should still work (as seen in current tests).
Each method will only accept
-1
where it makes sense. For examplehas_selection(-1)
will returntrue
if any caret has a selection. Whereasget_caret_line
does not have a strong use case for supporting-1
, so is defaulted to0
.Some utility methods have been exposed for handing multiple carets, they are as follows:
-
add_caret
- Adds a caret.-
remove_caret
- Removes a caret.-
get_caret_count
- Gets the number of carets.-
remove_secondary_carets
- Removes all additional carets.-
merge_overlapping_carets
- Merges any carets and selections that are overlapping. Note this is not called when a caret changes position but after "actions", so if using the API it is possible to get into a state where carets do overlap.-
get_caret_index_edit_order
- Returns a list of caret indexes in their edit order, this done from bottom to top. Edit order refers to the way action such asinsert_text_at_caret
are applied.-
adjust_carets_after_edit
- Should be called after editing a caret to reposition the carets affected by the edit such as adding a new line will need to push all carets and selection after that point down a line. This assumes edits are applied in edit order.Do due a typing now having the potential to consist of multiple edits, the undo redo system would put each caret as a separate step. Using
[begin|end]_complex_operation
around this would force it into a single step. However this would only make each letter when typing a step, the typing session would not be. We needed a way to have a "hanging" complex op, this comes in the form of an "action".Calling
start_action
will end the current action if it is not the same, before starting the new one.end_action
marks that the any changes from the action are over, however the action does not end until the timeout or another action is started. This can be done via directly callingstart_action
,[begin|end]_complex_operation
or via a edit that triggers and undo redo step outside of the[start|end]_action
calls.There are currently four supported actions, intended as follow:
-
None
- No current action.-
Typing
- Normal typing-
Backspace
- A backwards delete typically using the backspace-
Delete
- A forwards delete typically using the delete key.This fixes some old issues where typing when the caret had a selection would be treated as an entire step, so completion would not work etc. Similarly, pressing the delete key would create an undo step for each letter, now they are treated like typing where multiple deletes are a single action.
Final notes on the undo redo, the system used to use a best guess approach to determine where the caret went, it will now restore the carets exactly as they were. One thing to watch out for, is many carets can spam the undo stack as it becomes difficult to merge them into a single item, as they move about due to other edits, this may cause the undo stack to fill up pretty fast.
Lastly, in order to get this is work, moved the viewport variables outside of the caret and gave them some slightly better names, and currently only the main caret is restored on launch.
closes godotengine/godot-proposals/issues/245
closes godotengine/godot-proposals/issues/3455