diff --git a/assets/keymaps/default-linux.json b/assets/keymaps/default-linux.json index 190f915d86f1c3f5a69449fbc17ca456800f6d52..fb305fe768931dd6f52f1b5d890ad6771b7b5cac 100644 --- a/assets/keymaps/default-linux.json +++ b/assets/keymaps/default-linux.json @@ -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", diff --git a/assets/keymaps/default-macos.json b/assets/keymaps/default-macos.json index e1119be12980e49538bfbfe81d0834b61d0181f3..5fb408640b2c5083f4d3379bf927178c96bed4b6 100644 --- a/assets/keymaps/default-macos.json +++ b/assets/keymaps/default-macos.json @@ -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", diff --git a/assets/keymaps/default-windows.json b/assets/keymaps/default-windows.json index 0736e49162d8186990c2af62f971245e1fc825fe..34d161577ee315857becf7c9e3c9353402e56876 100644 --- a/assets/keymaps/default-windows.json +++ b/assets/keymaps/default-windows.json @@ -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", diff --git a/assets/keymaps/vim.json b/assets/keymaps/vim.json index efc375795ee70c57c372aa8c56352bcae5f8e8f7..cbfea5d7fddc3ccdc41bab0167f550ad5e53172a 100644 --- a/assets/keymaps/vim.json +++ b/assets/keymaps/vim.json @@ -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": { diff --git a/crates/editor/src/actions.rs b/crates/editor/src/actions.rs index f4b4c69679ebd4ab0f9080cd7d110fd4e87259c4..077152bff0e5494d04c62a7874f7b2ffea28488c 100644 --- a/crates/editor/src/actions.rs +++ b/crates/editor/src/actions.rs @@ -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)] diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index e6f597de7ff9138b226cd2474353ef8c2ce16ebb..c9c2688f80edc14e879ae50adb654d3cf2c9ae8a 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -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, + ) { + 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::(&this.display_snapshot(cx)); + let mut edits = Vec::new(); + let snapshot = this.buffer.read(cx).read(cx); + let empty_str: Arc = 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 = 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 = if comment_start.ends_with(' ') { + comment_start.clone() + } else { + format!("{} ", comment_start).into() + }; + let suffix: Arc = 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::(&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, diff --git a/crates/editor/src/editor_block_comment_tests.rs b/crates/editor/src/editor_block_comment_tests.rs new file mode 100644 index 0000000000000000000000000000000000000000..09b7a49c9389cdd0a7e0b76a3f98636f5301972d --- /dev/null +++ b/crates/editor/src/editor_block_comment_tests.rs @@ -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("√√√ˇ"); +} diff --git a/crates/editor/src/element.rs b/crates/editor/src/element.rs index 512fbb8855aa11d8c540065a55eb296919012821..3e41aaceb6955653bfedfd5bb8464ff6b4b66353 100644 --- a/crates/editor/src/element.rs +++ b/crates/editor/src/element.rs @@ -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); diff --git a/crates/grammars/src/c/config.toml b/crates/grammars/src/c/config.toml index a3b55f4f2d4fe3bfb19100e5877661c5841126a9..9977bb90373bc82e8aed7209c64b770cae125dce 100644 --- a/crates/grammars/src/c/config.toml +++ b/crates/grammars/src/c/config.toml @@ -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 } diff --git a/crates/grammars/src/cpp/config.toml b/crates/grammars/src/cpp/config.toml index 138d4a78e45f153eaa2eeb72a91654416154ed33..09672742c5b4b9c275f4d3f352311b84a7ac06f7 100644 --- a/crates/grammars/src/cpp/config.toml +++ b/crates/grammars/src/cpp/config.toml @@ -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 } diff --git a/crates/grammars/src/go/config.toml b/crates/grammars/src/go/config.toml index 36f885d75fe623eb16b306f0481ac7677ab0d35b..3fff3411f22f7b65bc5ab1e0587295ac7d41cb01 100644 --- a/crates/grammars/src/go/config.toml +++ b/crates/grammars/src/go/config.toml @@ -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 } diff --git a/crates/grammars/src/javascript/config.toml b/crates/grammars/src/javascript/config.toml index 118024494a7b8f98bcff9354fd3d27f4fc1dcfc4..66956f1ed60fb23383b842103a555c01ab00c21f 100644 --- a/crates/grammars/src/javascript/config.toml +++ b/crates/grammars/src/javascript/config.toml @@ -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] diff --git a/crates/grammars/src/jsonc/config.toml b/crates/grammars/src/jsonc/config.toml index acc0c2e13d5b0ad2df5e45e500be36c53e7c5857..3d9811a042e13e613cd1159b09dfebbcc6937d6b 100644 --- a/crates/grammars/src/jsonc/config.toml +++ b/crates/grammars/src/jsonc/config.toml @@ -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 }, diff --git a/crates/grammars/src/markdown/config.toml b/crates/grammars/src/markdown/config.toml index 27dd1821e414fb8e068c3c1975ec6189d80c0350..46c4147020905b9f440a36b342af8004fb3462cc 100644 --- a/crates/grammars/src/markdown/config.toml +++ b/crates/grammars/src/markdown/config.toml @@ -3,7 +3,7 @@ grammar = "markdown" path_suffixes = ["md", "mdx", "mdwn", "mdc", "markdown", "MD"] modeline_aliases = ["md"] completion_query_characters = ["-"] -block_comment = { start = "", tab_size = 0 } +block_comment = { start = "", tab_size = 1 } autoclose_before = ";:.,=}])>" brackets = [ { start = "{", end = "}", close = true, newline = true }, diff --git a/crates/grammars/src/python/config.toml b/crates/grammars/src/python/config.toml index 0c2072393bf6cc1db6b152d80779cd7c81af1a7e..2d88e46ddb492392a4d23e8fd6bc793dd742854a 100644 --- a/crates/grammars/src/python/config.toml +++ b/crates/grammars/src/python/config.toml @@ -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"] }, diff --git a/crates/grammars/src/rust/config.toml b/crates/grammars/src/rust/config.toml index f739b370f4b5c3fe7bc53f4818ffabedfa1bbd0b..6c5763fa642648d6aaf7c3a89792866af9fa08ee 100644 --- a/crates/grammars/src/rust/config.toml +++ b/crates/grammars/src/rust/config.toml @@ -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 } diff --git a/crates/grammars/src/tsx/config.toml b/crates/grammars/src/tsx/config.toml index 42438fdf890a98f319244332f384f574e02c2904..efc180f5cf322116088c6dbe835bf4e399f23f3c 100644 --- a/crates/grammars/src/tsx/config.toml +++ b/crates/grammars/src/tsx/config.toml @@ -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] diff --git a/crates/vim/src/normal.rs b/crates/vim/src/normal.rs index b54a0262744afddbefbd3d4ce5a737dfe3ee7502..5aad101e08632b119cb86036cb1b8557c82cd702 100644 --- a/crates/vim/src/normal.rs +++ b/crates/vim/src/normal.rs @@ -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::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.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, diff --git a/crates/vim/src/normal/toggle_comments.rs b/crates/vim/src/normal/toggle_comments.rs index 5ee6e9f5078e49ef35c589a1f3be37fb81a37afc..2cac1dd09f07086cbf9b31007cf7c9aeb3334d78 100644 --- a/crates/vim/src/normal/toggle_comments.rs +++ b/crates/vim/src/normal/toggle_comments.rs @@ -71,4 +71,75 @@ impl Vim { }); }); } + + pub fn toggle_block_comments_motion( + &mut self, + motion: Motion, + times: Option, + forced_motion: bool, + window: &mut Window, + cx: &mut Context, + ) { + 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, + window: &mut Window, + cx: &mut Context, + ) { + 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); + }); + }); + }); + }); + } } diff --git a/crates/vim/src/state.rs b/crates/vim/src/state.rs index 9e9b42d31900e0ceb160df4ad4dd3ce3a530e155..661853930c97cdfe6455e10bfec404b3ade2f231 100644 --- a/crates/vim/src/state.rs +++ b/crates/vim/src/state.rs @@ -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 diff --git a/crates/vim/src/test.rs b/crates/vim/src/test.rs index 961729e0e24a66a624e30ca7c72bfe5f13e10bca..7d4b469d3b94d5fd9636b5eb6a588b57eaf771c7 100644 --- a/crates/vim/src/test.rs +++ b/crates/vim/src/test.rs @@ -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) { diff --git a/crates/vim/src/vim.rs b/crates/vim/src/vim.rs index a66111cae1576744c4c51d717984d67c12fc8235..917641423f89c6da82576ae512cd01bea82ce4ab 100644 --- a/crates/vim/src/vim.rs +++ b/crates/vim/src/vim.rs @@ -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) });