Detailed changes
@@ -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",
@@ -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",
@@ -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",
@@ -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": {
@@ -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)]
@@ -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,
@@ -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 = /* 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_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("√√√ˇ");
+}
@@ -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);
@@ -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 }
@@ -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 }
@@ -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 }
@@ -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]
@@ -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 },
@@ -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 },
@@ -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"] },
@@ -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 }
@@ -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]
@@ -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>,
@@ -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);
+ });
+ });
+ });
+ });
+ }
}
@@ -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
@@ -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) {
@@ -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)
});