editor: Add action to toggle block comments (#48752)

Closes #4751

## Testing
- Manually tested by comparing the behaviors with vscode.
- Those requirements are added to unit tests.

Release Notes:

- Added action to toggle block comments

---------

Co-authored-by: ozacod <ozacod@users.noreply.github.com>
This commit is contained in:
ozacod 2026-04-08 22:29:16 +03:00 committed by GitHub
parent 64c69cae9f
commit 525f10a133
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
22 changed files with 770 additions and 3 deletions

View file

@ -529,6 +529,8 @@
"ctrl-k ctrl-b": "editor::BlameHover",
"ctrl-/": ["editor::ToggleComments", { "advance_downwards": false }],
"ctrl-k ctrl-c": ["editor::ToggleComments", { "advance_downwards": false }],
"ctrl-k ctrl-/": "editor::ToggleBlockComments",
"shift-alt-a": "editor::ToggleBlockComments",
"f8": ["editor::GoToDiagnostic", { "severity": { "min": "hint", "max": "error" } }],
"shift-f8": ["editor::GoToPreviousDiagnostic", { "severity": { "min": "hint", "max": "error" } }],
"f2": "editor::Rename",

View file

@ -579,6 +579,8 @@
"cmd-k cmd-i": "editor::Hover",
"cmd-k cmd-b": "editor::BlameHover",
"cmd-/": ["editor::ToggleComments", { "advance_downwards": false }],
"cmd-k cmd-/": "editor::ToggleBlockComments",
"shift-alt-a": "editor::ToggleBlockComments",
"f8": ["editor::GoToDiagnostic", { "severity": { "min": "hint", "max": "error" } }],
"shift-f8": ["editor::GoToPreviousDiagnostic", { "severity": { "min": "hint", "max": "error" } }],
"f2": "editor::Rename",

View file

@ -530,6 +530,8 @@
"ctrl-k ctrl-f": "editor::FormatSelections",
"ctrl-/": ["editor::ToggleComments", { "advance_downwards": false }],
"ctrl-k ctrl-c": ["editor::ToggleComments", { "advance_downwards": false }],
"ctrl-k ctrl-/": "editor::ToggleBlockComments",
"shift-alt-a": "editor::ToggleBlockComments",
"f8": ["editor::GoToDiagnostic", { "severity": { "min": "hint", "max": "error" } }],
"shift-f8": ["editor::GoToPreviousDiagnostic", { "severity": { "min": "hint", "max": "error" } }],
"f2": "editor::Rename",

View file

@ -263,6 +263,7 @@
"] c": "editor::GoToHunk",
"[ c": "editor::GoToPreviousHunk",
"g c": "vim::PushToggleComments",
"g b": "vim::PushToggleBlockComments",
},
},
{
@ -319,6 +320,7 @@
"a": ["vim::PushObject", { "around": true }],
"g shift-r": ["vim::Paste", { "preserve_clipboard": true }],
"g c": "vim::ToggleComments",
"g b": "vim::ToggleBlockComments",
"g q": "vim::Rewrap",
"g w": "vim::Rewrap",
"g ?": "vim::ConvertToRot13",
@ -791,6 +793,12 @@
"c": "vim::CurrentLine",
},
},
{
"context": "vim_operator == gb",
"bindings": {
"c": "vim::CurrentLine",
},
},
{
"context": "vim_operator == gR",
"bindings": {

View file

@ -150,6 +150,12 @@ pub struct ToggleComments {
pub ignore_indent: bool,
}
/// Toggles block comment markers for the selected text.
#[derive(PartialEq, Clone, Deserialize, Default, JsonSchema, Action)]
#[action(namespace = editor)]
#[serde(deny_unknown_fields)]
pub struct ToggleBlockComments;
/// Moves the cursor up by a specified number of lines.
#[derive(PartialEq, Clone, Deserialize, Default, JsonSchema, Action)]
#[action(namespace = editor)]

View file

@ -48,6 +48,8 @@ mod code_completion_tests;
#[cfg(test)]
mod edit_prediction_tests;
#[cfg(test)]
mod editor_block_comment_tests;
#[cfg(test)]
mod editor_tests;
mod signature_help;
#[cfg(any(test, feature = "test-support"))]
@ -16462,6 +16464,197 @@ impl Editor {
Ok(())
}
pub fn toggle_block_comments(
&mut self,
_: &ToggleBlockComments,
window: &mut Window,
cx: &mut Context<Self>,
) {
if self.read_only(cx) {
return;
}
self.hide_mouse_cursor(HideMouseCursorOrigin::TypingAction, cx);
self.transact(window, cx, |this, _window, cx| {
let mut selections = this
.selections
.all::<MultiBufferPoint>(&this.display_snapshot(cx));
let mut edits = Vec::new();
let snapshot = this.buffer.read(cx).read(cx);
let empty_str: Arc<str> = Arc::default();
let mut markers_inserted = Vec::new();
for selection in &mut selections {
let start_point = selection.start;
let end_point = selection.end;
let Some(language) =
snapshot.language_scope_at(Point::new(start_point.row, start_point.column))
else {
continue;
};
let Some(BlockCommentConfig {
start: comment_start,
end: comment_end,
..
}) = language.block_comment()
else {
continue;
};
let prefix_needle = comment_start.trim_end().as_bytes();
let suffix_needle = comment_end.trim_start().as_bytes();
// Collect full lines spanning the selection as the search region
let region_start = Point::new(start_point.row, 0);
let region_end = Point::new(
end_point.row,
snapshot.line_len(MultiBufferRow(end_point.row)),
);
let region_bytes: Vec<u8> = snapshot
.bytes_in_range(region_start..region_end)
.flatten()
.copied()
.collect();
let region_start_offset = snapshot.point_to_offset(region_start);
let start_byte = snapshot.point_to_offset(start_point) - region_start_offset;
let end_byte = snapshot.point_to_offset(end_point) - region_start_offset;
let mut is_commented = false;
let mut prefix_range = start_point..start_point;
let mut suffix_range = end_point..end_point;
// Find rightmost /* at or before the selection end
if let Some(prefix_pos) = region_bytes[..end_byte.min(region_bytes.len())]
.windows(prefix_needle.len())
.rposition(|w| w == prefix_needle)
{
let after_prefix = prefix_pos + prefix_needle.len();
// Find the first */ after that /*
if let Some(suffix_pos) = region_bytes[after_prefix..]
.windows(suffix_needle.len())
.position(|w| w == suffix_needle)
.map(|p| p + after_prefix)
{
let suffix_end = suffix_pos + suffix_needle.len();
// Case 1: /* ... */ surrounds the selection
let markers_surround = prefix_pos <= start_byte
&& suffix_end >= end_byte
&& start_byte < suffix_end;
// Case 2: selection contains /* ... */ (only whitespace padding)
let selection_contains = start_byte <= prefix_pos
&& suffix_end <= end_byte
&& region_bytes[start_byte..prefix_pos]
.iter()
.all(|&b| b.is_ascii_whitespace())
&& region_bytes[suffix_end..end_byte]
.iter()
.all(|&b| b.is_ascii_whitespace());
if markers_surround || selection_contains {
is_commented = true;
let prefix_pt =
snapshot.offset_to_point(region_start_offset + prefix_pos);
let suffix_pt =
snapshot.offset_to_point(region_start_offset + suffix_pos);
prefix_range = prefix_pt
..Point::new(
prefix_pt.row,
prefix_pt.column + prefix_needle.len() as u32,
);
suffix_range = suffix_pt
..Point::new(
suffix_pt.row,
suffix_pt.column + suffix_needle.len() as u32,
);
}
}
}
if is_commented {
// Also remove the space after /* and before */
if snapshot
.bytes_in_range(prefix_range.end..snapshot.max_point())
.flatten()
.next()
== Some(&b' ')
{
prefix_range.end.column += 1;
}
if suffix_range.start.column > 0 {
let before =
Point::new(suffix_range.start.row, suffix_range.start.column - 1);
if snapshot
.bytes_in_range(before..suffix_range.start)
.flatten()
.next()
== Some(&b' ')
{
suffix_range.start.column -= 1;
}
}
edits.push((prefix_range, empty_str.clone()));
edits.push((suffix_range, empty_str.clone()));
} else {
let prefix: Arc<str> = if comment_start.ends_with(' ') {
comment_start.clone()
} else {
format!("{} ", comment_start).into()
};
let suffix: Arc<str> = if comment_end.starts_with(' ') {
comment_end.clone()
} else {
format!(" {}", comment_end).into()
};
edits.push((start_point..start_point, prefix.clone()));
edits.push((end_point..end_point, suffix.clone()));
markers_inserted.push((
selection.id,
prefix.len(),
suffix.len(),
selection.is_empty(),
end_point.row,
));
}
}
drop(snapshot);
this.buffer.update(cx, |buffer, cx| {
buffer.edit(edits, None, cx);
});
let mut selections = this
.selections
.all::<MultiBufferPoint>(&this.display_snapshot(cx));
for selection in &mut selections {
if let Some((_, prefix_len, suffix_len, was_empty, suffix_row)) = markers_inserted
.iter()
.find(|(id, _, _, _, _)| *id == selection.id)
{
if *was_empty {
selection.start.column = selection
.start
.column
.saturating_sub((*prefix_len + *suffix_len) as u32);
} else {
selection.start.column =
selection.start.column.saturating_sub(*prefix_len as u32);
if selection.end.row == *suffix_row {
selection.end.column += *suffix_len as u32;
}
}
}
}
this.change_selections(Default::default(), _window, cx, |s| s.select(selections));
});
}
pub fn toggle_comments(
&mut self,
action: &ToggleComments,

View file

@ -0,0 +1,293 @@
use crate::ToggleBlockComments;
use crate::editor_tests::init_test;
use crate::test::editor_test_context::EditorTestContext;
use gpui::TestAppContext;
use indoc::indoc;
use language::{BlockCommentConfig, Language, LanguageConfig};
use std::sync::Arc;
async fn setup_rust_context(cx: &mut TestAppContext) -> EditorTestContext {
init_test(cx, |_| {});
let mut cx = EditorTestContext::new(cx).await;
let rust_language = Arc::new(Language::new(
LanguageConfig {
name: "Rust".into(),
block_comment: Some(BlockCommentConfig {
start: "/* ".into(),
prefix: "".into(),
end: " */".into(),
tab_size: 0,
}),
..Default::default()
},
Some(tree_sitter_rust::LANGUAGE.into()),
));
cx.language_registry().add(rust_language.clone());
cx.update_buffer(|buffer, cx| {
buffer.set_language(Some(rust_language), cx);
});
cx
}
#[gpui::test]
async fn test_toggle_block_comments(cx: &mut TestAppContext) {
let mut cx = setup_rust_context(cx).await;
cx.set_state(indoc! {"
fn main() {
let x = «1ˇ» + 2;
}
"});
cx.update_editor(|editor, window, cx| {
editor.toggle_block_comments(&ToggleBlockComments, window, cx);
});
cx.assert_editor_state(indoc! {"
fn main() {
let x = «/* 1 */ˇ» + 2;
}
"});
cx.update_editor(|editor, window, cx| {
editor.toggle_block_comments(&ToggleBlockComments, window, cx);
});
cx.assert_editor_state(indoc! {"
fn main() {
let x = «1ˇ» + 2;
}
"});
}
#[gpui::test]
async fn test_toggle_block_comments_with_selection(cx: &mut TestAppContext) {
let mut cx = setup_rust_context(cx).await;
cx.set_state(indoc! {"
fn main() {
«let x = 1 + 2;ˇ»
}
"});
cx.update_editor(|editor, window, cx| {
editor.toggle_block_comments(&ToggleBlockComments, window, cx);
});
cx.assert_editor_state(indoc! {"
fn main() {
«/* let x = 1 + 2; */ˇ»
}
"});
cx.update_editor(|editor, window, cx| {
editor.toggle_block_comments(&ToggleBlockComments, window, cx);
});
cx.assert_editor_state(indoc! {"
fn main() {
«let x = 1 + 2;ˇ»
}
"});
}
#[gpui::test]
async fn test_toggle_block_comments_multiline(cx: &mut TestAppContext) {
let mut cx = setup_rust_context(cx).await;
cx.set_state(indoc! {"
«fn main() {
let x = 1;
}ˇ»
"});
cx.update_editor(|editor, window, cx| {
editor.toggle_block_comments(&ToggleBlockComments, window, cx);
});
cx.assert_editor_state(indoc! {"
«/* fn main() {
let x = 1;
} */ˇ»
"});
cx.update_editor(|editor, window, cx| {
editor.toggle_block_comments(&ToggleBlockComments, window, cx);
});
cx.assert_editor_state(indoc! {"
«fn main() {
let x = 1;
}ˇ»
"});
}
#[gpui::test]
async fn test_toggle_block_comments_cursor_inside(cx: &mut TestAppContext) {
let mut cx = setup_rust_context(cx).await;
cx.set_state(indoc! {"
fn main() {
let x = /**/ + 2;
}
"});
cx.update_editor(|editor, window, cx| {
editor.toggle_block_comments(&ToggleBlockComments, window, cx);
});
cx.assert_editor_state(indoc! {"
fn main() {
let x = 1ˇ + 2;
}
"});
}
#[gpui::test]
async fn test_toggle_block_comments_multiple_cursors(cx: &mut TestAppContext) {
let mut cx = setup_rust_context(cx).await;
cx.set_state(indoc! {"
fn main() {
let x = «1ˇ» + 2;
let y = «3ˇ» + 4;
}
"});
cx.update_editor(|editor, window, cx| {
editor.toggle_block_comments(&ToggleBlockComments, window, cx);
});
cx.assert_editor_state(indoc! {"
fn main() {
let x = «/* 1 */ˇ» + 2;
let y = «/* 3 */ˇ» + 4;
}
"});
cx.update_editor(|editor, window, cx| {
editor.toggle_block_comments(&ToggleBlockComments, window, cx);
});
cx.assert_editor_state(indoc! {"
fn main() {
let x = «1ˇ» + 2;
let y = «3ˇ» + 4;
}
"});
}
#[gpui::test]
async fn test_toggle_block_comments_selection_ending_on_empty_line(cx: &mut TestAppContext) {
let mut cx = setup_rust_context(cx).await;
cx.set_state(indoc! {"
«fn main() {
ˇ»
let x = 1;
}
"});
cx.update_editor(|editor, window, cx| {
editor.toggle_block_comments(&ToggleBlockComments, window, cx);
});
cx.assert_editor_state(indoc! {"
«/* fn main() {
*/ˇ»
let x = 1;
}
"});
cx.update_editor(|editor, window, cx| {
editor.toggle_block_comments(&ToggleBlockComments, window, cx);
});
cx.assert_editor_state(indoc! {"
«fn main() {
ˇ»
let x = 1;
}
"});
}
#[gpui::test]
async fn test_toggle_block_comments_empty_selection_roundtrip(cx: &mut TestAppContext) {
let mut cx = setup_rust_context(cx).await;
cx.set_state(indoc! {"
fn main() {
let x = ˇ1 + 2;
}
"});
cx.update_editor(|editor, window, cx| {
editor.toggle_block_comments(&ToggleBlockComments, window, cx);
});
cx.update_editor(|editor, window, cx| {
editor.toggle_block_comments(&ToggleBlockComments, window, cx);
});
cx.assert_editor_state(indoc! {"
fn main() {
let x = ˇ1 + 2;
}
"});
}
// Multi-byte Unicode characters (√ is 3 bytes in UTF-8) must not cause
// incorrect offset arithmetic or panics.
#[gpui::test]
async fn test_toggle_block_comments_unicode_before_selection(cx: &mut TestAppContext) {
let mut cx = setup_rust_context(cx).await;
cx.set_state("let √ = «42ˇ»;");
cx.update_editor(|editor, window, cx| {
editor.toggle_block_comments(&ToggleBlockComments, window, cx);
});
cx.assert_editor_state("let √ = «/* 42 */ˇ»;");
cx.update_editor(|editor, window, cx| {
editor.toggle_block_comments(&ToggleBlockComments, window, cx);
});
cx.assert_editor_state("let √ = «42ˇ»;");
}
#[gpui::test]
async fn test_toggle_block_comments_unicode_in_selection(cx: &mut TestAppContext) {
let mut cx = setup_rust_context(cx).await;
cx.set_state("«√√√ˇ»");
cx.update_editor(|editor, window, cx| {
editor.toggle_block_comments(&ToggleBlockComments, window, cx);
});
cx.assert_editor_state("«/* √√√ */ˇ»");
cx.update_editor(|editor, window, cx| {
editor.toggle_block_comments(&ToggleBlockComments, window, cx);
});
cx.assert_editor_state("«√√√ˇ»");
}
#[gpui::test]
async fn test_toggle_block_comments_cursor_inside_unicode_comment(cx: &mut TestAppContext) {
let mut cx = setup_rust_context(cx).await;
cx.set_state("/* √√√ˇ */");
cx.update_editor(|editor, window, cx| {
editor.toggle_block_comments(&ToggleBlockComments, window, cx);
});
cx.assert_editor_state("√√√ˇ");
}

View file

@ -397,6 +397,7 @@ impl EditorElement {
editor.find_previous_match(action, window, cx).log_err();
});
register_action(editor, window, Editor::toggle_comments);
register_action(editor, window, Editor::toggle_block_comments);
register_action(editor, window, Editor::select_larger_syntax_node);
register_action(editor, window, Editor::select_smaller_syntax_node);
register_action(editor, window, Editor::select_next_syntax_node);

View file

@ -16,4 +16,5 @@ brackets = [
{ start = "/*", end = " */", close = true, newline = false, not_in = ["string", "comment"] },
]
debuggers = ["CodeLLDB", "GDB"]
block_comment = { start = "/*", prefix = "", end = "*/", tab_size = 1 }
documentation_comment = { start = "/*", prefix = "* ", end = "*/", tab_size = 1 }

View file

@ -18,4 +18,5 @@ brackets = [
{ start = "/*", end = " */", close = true, newline = false, not_in = ["string", "comment"] },
]
debuggers = ["CodeLLDB", "GDB"]
block_comment = { start = "/*", prefix = "", end = "*/", tab_size = 1 }
documentation_comment = { start = "/*", prefix = "* ", end = "*/", tab_size = 1 }

View file

@ -17,4 +17,5 @@ brackets = [
tab_size = 4
hard_tabs = true
debuggers = ["Delve"]
block_comment = { start = "/*", prefix = "", end = "*/", tab_size = 1 }
documentation_comment = { start = "/*", prefix = "* ", end = "*/", tab_size = 1 }

View file

@ -36,7 +36,7 @@ linked_edit_characters = ["."]
[overrides.element]
line_comments = { remove = true }
block_comment = { start = "{/* ", prefix = "", end = "*/}", tab_size = 0 }
block_comment = { start = "{/* ", prefix = "", end = "*/}", tab_size = 1 }
opt_into_language_servers = ["emmet-language-server"]
[overrides.string]

View file

@ -2,6 +2,7 @@ name = "JSONC"
grammar = "jsonc"
path_suffixes = ["jsonc", "bun.lock", "devcontainer.json", "pyrightconfig.json", "tsconfig.json", "luaurc", "swcrc", "babelrc", "eslintrc", "stylelintrc", "jshintrc"]
line_comments = ["// "]
block_comment = { start = "/*", prefix = "", end = "*/", tab_size = 1 }
autoclose_before = ",]}"
brackets = [
{ start = "{", end = "}", close = true, surround = true, newline = true },

View file

@ -3,7 +3,7 @@ grammar = "markdown"
path_suffixes = ["md", "mdx", "mdwn", "mdc", "markdown", "MD"]
modeline_aliases = ["md"]
completion_query_characters = ["-"]
block_comment = { start = "<!--", prefix = "", end = "-->", tab_size = 0 }
block_comment = { start = "<!--", prefix = "", end = "-->", tab_size = 1 }
autoclose_before = ";:.,=}])>"
brackets = [
{ start = "{", end = "}", close = true, newline = true },

View file

@ -4,6 +4,7 @@ path_suffixes = ["py", "pyi", "mpy"]
first_line_pattern = '^#!.*((\bpython[0-9.]*\b)|(\buv run\b))'
modeline_aliases = ["py"]
line_comments = ["# "]
block_comment = { start = "\"\"\"", end = "\"\"\"", prefix = "", tab_size = 1 }
autoclose_before = ";:.,=}])>"
brackets = [
{ start = "f\"", end = "\"", close = true, newline = false, not_in = ["string", "comment"] },

View file

@ -17,4 +17,5 @@ brackets = [
]
collapsed_placeholder = " /* ... */ "
debuggers = ["CodeLLDB", "GDB"]
block_comment = { start = "/*", prefix = "* ", end = "*/", tab_size = 1 }
documentation_comment = { start = "/*", prefix = "* ", end = "*/", tab_size = 1 }

View file

@ -35,7 +35,7 @@ linked_edit_characters = ["."]
[overrides.element]
line_comments = { remove = true }
block_comment = { start = "{/*", prefix = "", end = "*/}", tab_size = 0 }
block_comment = { start = "{/*", prefix = "", end = "*/}", tab_size = 1 }
opt_into_language_servers = ["emmet-language-server"]
[overrides.string]

View file

@ -88,6 +88,8 @@ actions!(
ConvertToRot47,
/// Toggles comments for selected lines.
ToggleComments,
/// Toggles block comments for selected lines.
ToggleBlockComments,
/// Shows the current location in the file.
ShowLocation,
/// Undoes the last change.
@ -125,6 +127,7 @@ pub(crate) fn register(editor: &mut Editor, cx: &mut Context<Vim>) {
Vim::action(editor, cx, Vim::yank_line);
Vim::action(editor, cx, Vim::yank_to_end_of_line);
Vim::action(editor, cx, Vim::toggle_comments);
Vim::action(editor, cx, Vim::toggle_block_comments);
Vim::action(editor, cx, Vim::paste);
Vim::action(editor, cx, Vim::show_location);
@ -463,6 +466,9 @@ impl Vim {
Some(Operator::ToggleComments) => {
self.toggle_comments_motion(motion, times, forced_motion, window, cx)
}
Some(Operator::ToggleBlockComments) => {
self.toggle_block_comments_motion(motion, times, forced_motion, window, cx)
}
Some(Operator::ReplaceWithRegister) => {
self.replace_with_register_motion(motion, times, forced_motion, window, cx)
}
@ -533,6 +539,9 @@ impl Vim {
Some(Operator::ToggleComments) => {
self.toggle_comments_object(object, around, times, window, cx)
}
Some(Operator::ToggleBlockComments) => {
self.toggle_block_comments_object(object, around, times, window, cx)
}
Some(Operator::ReplaceWithRegister) => {
self.replace_with_register_object(object, around, window, cx)
}
@ -985,6 +994,38 @@ impl Vim {
}
}
fn toggle_block_comments(
&mut self,
_: &ToggleBlockComments,
window: &mut Window,
cx: &mut Context<Self>,
) {
self.record_current_action(cx);
self.store_visual_marks(window, cx);
let is_visual_line = self.mode == Mode::VisualLine;
self.update_editor(cx, |vim, editor, cx| {
editor.transact(window, cx, |editor, window, cx| {
let original_positions = vim.save_selection_starts(editor, cx);
if is_visual_line {
editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
s.move_with(&mut |map, selection| {
let start_row = selection.start.to_point(map).row;
let end_row = selection.end.to_point(map).row;
let end_col = map.buffer_snapshot().line_len(MultiBufferRow(end_row));
selection.start = Point::new(start_row, 0).to_display_point(map);
selection.end = Point::new(end_row, end_col).to_display_point(map);
});
});
}
editor.toggle_block_comments(&Default::default(), window, cx);
vim.restore_selection_cursors(editor, window, cx, original_positions);
});
});
if self.mode.is_visual() {
self.switch_mode(Mode::Normal, true, window, cx)
}
}
pub(crate) fn normal_replace(
&mut self,
text: Arc<str>,

View file

@ -71,4 +71,75 @@ impl Vim {
});
});
}
pub fn toggle_block_comments_motion(
&mut self,
motion: Motion,
times: Option<usize>,
forced_motion: bool,
window: &mut Window,
cx: &mut Context<Self>,
) {
self.stop_recording(cx);
self.update_editor(cx, |_, editor, cx| {
let text_layout_details = editor.text_layout_details(window, cx);
editor.transact(window, cx, |editor, window, cx| {
editor.set_clip_at_line_ends(false, cx);
let mut selection_starts: HashMap<_, _> = Default::default();
editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
s.move_with(&mut |map, selection| {
let anchor = map.display_point_to_anchor(selection.head(), Bias::Right);
selection_starts.insert(selection.id, anchor);
motion.expand_selection(
map,
selection,
times,
&text_layout_details,
forced_motion,
);
});
});
editor.toggle_block_comments(&Default::default(), window, cx);
editor.set_clip_at_line_ends(true, cx);
editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
s.move_with(&mut |map, selection| {
let anchor = selection_starts.remove(&selection.id).unwrap();
selection.collapse_to(anchor.to_display_point(map), SelectionGoal::None);
});
});
});
});
}
pub fn toggle_block_comments_object(
&mut self,
object: Object,
around: bool,
times: Option<usize>,
window: &mut Window,
cx: &mut Context<Self>,
) {
self.stop_recording(cx);
self.update_editor(cx, |_, editor, cx| {
editor.transact(window, cx, |editor, window, cx| {
editor.set_clip_at_line_ends(false, cx);
let mut original_positions: HashMap<_, _> = Default::default();
editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
s.move_with(&mut |map, selection| {
let anchor = map.display_point_to_anchor(selection.head(), Bias::Right);
original_positions.insert(selection.id, anchor);
object.expand_selection(map, selection, around, times);
});
});
editor.toggle_block_comments(&Default::default(), window, cx);
editor.set_clip_at_line_ends(true, cx);
editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
s.move_with(&mut |map, selection| {
let anchor = original_positions.remove(&selection.id).unwrap();
selection.collapse_to(anchor.to_display_point(map), SelectionGoal::None);
});
});
});
});
}
}

View file

@ -138,6 +138,7 @@ pub enum Operator {
RecordRegister,
ReplayRegister,
ToggleComments,
ToggleBlockComments,
ReplaceWithRegister,
Exchange,
HelixMatch,
@ -1078,6 +1079,7 @@ impl Operator {
Operator::RecordRegister => "q",
Operator::ReplayRegister => "@",
Operator::ToggleComments => "gc",
Operator::ToggleBlockComments => "gb",
Operator::HelixMatch => "helix_m",
Operator::HelixNext { .. } => "helix_next",
Operator::HelixPrevious { .. } => "helix_previous",
@ -1157,6 +1159,7 @@ impl Operator {
| Operator::ChangeSurrounds { target: None, .. }
| Operator::OppositeCase
| Operator::ToggleComments
| Operator::ToggleBlockComments
| Operator::HelixMatch
| Operator::HelixNext { .. }
| Operator::HelixPrevious { .. } => false,
@ -1180,6 +1183,7 @@ impl Operator {
| Operator::Rot13
| Operator::Rot47
| Operator::ToggleComments
| Operator::ToggleBlockComments
| Operator::ReplaceWithRegister
| Operator::Rewrap
| Operator::ShellCommand

View file

@ -1694,6 +1694,134 @@ async fn test_toggle_comments(cx: &mut gpui::TestAppContext) {
);
}
#[perf]
#[gpui::test]
async fn test_toggle_block_comments(cx: &mut gpui::TestAppContext) {
let mut cx = VimTestContext::new(cx, true).await;
let language = std::sync::Arc::new(language::Language::new(
language::LanguageConfig {
block_comment: Some(language::BlockCommentConfig {
start: "/* ".into(),
prefix: "".into(),
end: " */".into(),
tab_size: 1,
}),
..Default::default()
},
Some(language::tree_sitter_rust::LANGUAGE.into()),
));
cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
// works in normal mode with current-line shorthand
cx.set_state(
indoc! {"
ˇone
two
three
"},
Mode::Normal,
);
cx.simulate_keystrokes("g b c");
cx.assert_state(
indoc! {"
/* ˇone */
two
three
"},
Mode::Normal,
);
// toggle off with cursor inside the comment
cx.simulate_keystrokes("g b c");
cx.assert_state(
indoc! {"
ˇone
two
three
"},
Mode::Normal,
);
// works in visual line mode (wraps full lines)
cx.simulate_keystrokes("shift-v j g b");
cx.assert_state(
indoc! {"
/* ˇone
two */
three
"},
Mode::Normal,
);
// works in visual mode and restores the cursor to the selection start
cx.set_state(
indoc! {"
«oneˇ»
two
three
"},
Mode::Visual,
);
cx.simulate_keystrokes("g b");
cx.assert_state(
indoc! {"
/* ˇone */
two
three
"},
Mode::Normal,
);
// works with multiple visual selections and restores each cursor
cx.set_state(
indoc! {"
«oneˇ» «twoˇ»
three
"},
Mode::Visual,
);
cx.simulate_keystrokes("g b");
cx.assert_state(
indoc! {"
/* ˇone */ /* ˇtwo */
three
"},
Mode::Normal,
);
// works with count
cx.set_state(
indoc! {"
ˇone
two
three
"},
Mode::Normal,
);
cx.simulate_keystrokes("g b 2 j");
cx.assert_state(
indoc! {"
/* ˇone
two
three */
"},
Mode::Normal,
);
// works with motion object
cx.simulate_keystrokes("shift-g");
cx.simulate_keystrokes("g b g g");
cx.assert_state(
indoc! {"
one
two
three
ˇ"},
Mode::Normal,
);
}
#[perf]
#[gpui::test]
async fn test_find_multibyte(cx: &mut gpui::TestAppContext) {

View file

@ -247,6 +247,8 @@ actions!(
PushReplaceWithRegister,
/// Toggles comments.
PushToggleComments,
/// Toggles block comments.
PushToggleBlockComments,
/// Selects (count) next menu item
MenuSelectNext,
/// Selects (count) previous menu item
@ -899,6 +901,14 @@ impl Vim {
vim.push_operator(Operator::ToggleComments, window, cx)
});
Vim::action(
editor,
cx,
|vim, _: &PushToggleBlockComments, window, cx| {
vim.push_operator(Operator::ToggleBlockComments, window, cx)
},
);
Vim::action(editor, cx, |vim, _: &ClearOperators, window, cx| {
vim.clear_operator(window, cx)
});