From f0db748ba10fe6986caf41d14ccd5da088e443f6 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Tue, 23 Nov 2021 14:13:28 -0800 Subject: [PATCH] Implement toggle-comments --- crates/buffer/src/lib.rs | 16 ++- crates/editor/src/lib.rs | 178 ++++++++++++++++++++++++++ crates/language/src/language.rs | 5 + crates/language/src/lib.rs | 5 - crates/zed/languages/rust/config.toml | 1 + 5 files changed, 198 insertions(+), 7 deletions(-) diff --git a/crates/buffer/src/lib.rs b/crates/buffer/src/lib.rs index b700d76773..6d72571c1c 100644 --- a/crates/buffer/src/lib.rs +++ b/crates/buffer/src/lib.rs @@ -564,6 +564,10 @@ impl Buffer { self.content().line_len(row) } + pub fn is_line_blank(&self, row: u32) -> bool { + self.content().is_line_blank(row) + } + pub fn max_point(&self) -> Point { self.visible_text.max_point() } @@ -1557,6 +1561,10 @@ impl Snapshot { self.content().line_len(row) } + pub fn is_line_blank(&self, row: u32) -> bool { + self.content().is_line_blank(row) + } + pub fn indent_column_for_line(&self, row: u32) -> u32 { self.content().indent_column_for_line(row) } @@ -1574,8 +1582,7 @@ impl Snapshot { } pub fn text_for_range(&self, range: Range) -> Chunks { - let range = range.start.to_offset(self)..range.end.to_offset(self); - self.visible_text.chunks_in_range(range) + self.content().text_for_range(range) } pub fn text_summary_for_range(&self, range: Range) -> TextSummary @@ -1725,6 +1732,11 @@ impl<'a> Content<'a> { (row_end_offset - row_start_offset) as u32 } + fn is_line_blank(&self, row: u32) -> bool { + self.text_for_range(Point::new(row, 0)..Point::new(row, self.line_len(row))) + .all(|chunk| chunk.matches(|c: char| !c.is_whitespace()).next().is_none()) + } + pub fn indent_column_for_line(&self, row: u32) -> u32 { let mut result = 0; for c in self.chars_at(Point::new(row, 0)) { diff --git a/crates/editor/src/lib.rs b/crates/editor/src/lib.rs index b54ea6989d..b59ccf1c55 100644 --- a/crates/editor/src/lib.rs +++ b/crates/editor/src/lib.rs @@ -83,6 +83,7 @@ action!(SelectLine); action!(SplitSelectionIntoLines); action!(AddSelectionAbove); action!(AddSelectionBelow); +action!(ToggleComments); action!(SelectLargerSyntaxNode); action!(SelectSmallerSyntaxNode); action!(MoveToEnclosingBracket); @@ -184,6 +185,7 @@ pub fn init(cx: &mut MutableAppContext) { Binding::new("cmd-ctrl-p", AddSelectionAbove, Some("Editor")), Binding::new("cmd-alt-down", AddSelectionBelow, Some("Editor")), Binding::new("cmd-ctrl-n", AddSelectionBelow, Some("Editor")), + Binding::new("cmd-/", ToggleComments, Some("Editor")), Binding::new("alt-up", SelectLargerSyntaxNode, Some("Editor")), Binding::new("ctrl-w", SelectLargerSyntaxNode, Some("Editor")), Binding::new("alt-down", SelectSmallerSyntaxNode, Some("Editor")), @@ -244,6 +246,7 @@ pub fn init(cx: &mut MutableAppContext) { cx.add_action(Editor::split_selection_into_lines); cx.add_action(Editor::add_selection_above); cx.add_action(Editor::add_selection_below); + cx.add_action(Editor::toggle_comments); cx.add_action(Editor::select_larger_syntax_node); cx.add_action(Editor::select_smaller_syntax_node); cx.add_action(Editor::move_to_enclosing_bracket); @@ -2127,6 +2130,96 @@ impl Editor { } } + pub fn toggle_comments(&mut self, _: &ToggleComments, cx: &mut ViewContext) { + // Get the line comment prefix. Split its trailing whitespace into a separate string, + // as that portion won't be used for detecting if a line is a comment. + let full_comment_prefix = + if let Some(prefix) = self.language(cx).and_then(|l| l.line_comment_prefix()) { + prefix.to_string() + } else { + return; + }; + let comment_prefix = full_comment_prefix.trim_end_matches(' '); + let comment_prefix_whitespace = &full_comment_prefix[comment_prefix.len()..]; + + self.start_transaction(cx); + let mut selections = self.selections::(cx).collect::>(); + let mut all_selection_lines_are_comments = true; + let mut edit_ranges = Vec::new(); + let mut last_toggled_row = None; + self.buffer.update(cx, |buffer, cx| { + for selection in &mut selections { + edit_ranges.clear(); + + let end_row = + if selection.end.row > selection.start.row && selection.end.column == 0 { + selection.end.row + } else { + selection.end.row + 1 + }; + + for row in selection.start.row..end_row { + // If multiple selections contain a given row, avoid processing that + // row more than once. + if last_toggled_row == Some(row) { + continue; + } else { + last_toggled_row = Some(row); + } + + if buffer.is_line_blank(row) { + continue; + } + + let start = Point::new(row, buffer.indent_column_for_line(row)); + let mut line_bytes = buffer.bytes_at(start); + + // If this line currently begins with the line comment prefix, then record + // the range containing the prefix. + if all_selection_lines_are_comments + && line_bytes + .by_ref() + .take(comment_prefix.len()) + .eq(comment_prefix.bytes()) + { + // Include any whitespace that matches the comment prefix. + let matching_whitespace_len = line_bytes + .zip(comment_prefix_whitespace.bytes()) + .take_while(|(a, b)| a == b) + .count() as u32; + let end = Point::new( + row, + start.column + comment_prefix.len() as u32 + matching_whitespace_len, + ); + edit_ranges.push(start..end); + } + // If this line does not begin with the line comment prefix, then record + // the position where the prefix should be inserted. + else { + all_selection_lines_are_comments = false; + edit_ranges.push(start..start); + } + } + + if !edit_ranges.is_empty() { + if all_selection_lines_are_comments { + buffer.edit(edit_ranges.iter().cloned(), "", cx); + } else { + let min_column = edit_ranges.iter().map(|r| r.start.column).min().unwrap(); + let edit_ranges = edit_ranges.iter().map(|range| { + let position = Point::new(range.start.row, min_column); + position..position + }); + buffer.edit(edit_ranges, &full_comment_prefix, cx); + } + } + } + }); + + self.update_selections(self.selections::(cx).collect(), true, cx); + self.end_transaction(cx); + } + pub fn select_larger_syntax_node( &mut self, _: &SelectLargerSyntaxNode, @@ -4890,6 +4983,91 @@ mod tests { }); } + #[gpui::test] + async fn test_toggle_comment(mut cx: gpui::TestAppContext) { + let settings = cx.read(EditorSettings::test); + let language = Some(Arc::new(Language::new( + LanguageConfig { + line_comment: Some("// ".to_string()), + ..Default::default() + }, + tree_sitter_rust::language(), + ))); + + let text = " + fn a() { + //b(); + // c(); + // d(); + } + " + .unindent(); + + let buffer = cx.add_model(|cx| Buffer::new(0, text, cx).with_language(language, None, cx)); + let (_, view) = cx.add_window(|cx| build_editor(buffer, settings, cx)); + + view.update(&mut cx, |editor, cx| { + // If multiple selections intersect a line, the line is only + // toggled once. + editor + .select_display_ranges( + &[ + DisplayPoint::new(1, 3)..DisplayPoint::new(2, 3), + DisplayPoint::new(3, 5)..DisplayPoint::new(3, 6), + ], + cx, + ) + .unwrap(); + editor.toggle_comments(&ToggleComments, cx); + assert_eq!( + editor.text(cx), + " + fn a() { + b(); + c(); + d(); + } + " + .unindent() + ); + + // The comment prefix is inserted at the same column for every line + // in a selection. + editor + .select_display_ranges(&[DisplayPoint::new(1, 3)..DisplayPoint::new(3, 6)], cx) + .unwrap(); + editor.toggle_comments(&ToggleComments, cx); + assert_eq!( + editor.text(cx), + " + fn a() { + // b(); + // c(); + // d(); + } + " + .unindent() + ); + + // If a selection ends at the beginning of a line, that line is not toggled. + editor + .select_display_ranges(&[DisplayPoint::new(2, 0)..DisplayPoint::new(3, 0)], cx) + .unwrap(); + editor.toggle_comments(&ToggleComments, cx); + assert_eq!( + editor.text(cx), + " + fn a() { + // b(); + c(); + // d(); + } + " + .unindent() + ); + }); + } + #[gpui::test] async fn test_extra_newline_insertion(mut cx: gpui::TestAppContext) { let settings = cx.read(EditorSettings::test); diff --git a/crates/language/src/language.rs b/crates/language/src/language.rs index 1f94996123..e80f34116c 100644 --- a/crates/language/src/language.rs +++ b/crates/language/src/language.rs @@ -14,6 +14,7 @@ pub struct LanguageConfig { pub name: String, pub path_suffixes: Vec, pub brackets: Vec, + pub line_comment: Option, pub language_server: Option, } @@ -115,6 +116,10 @@ impl Language { self.config.name.as_str() } + pub fn line_comment_prefix(&self) -> Option<&str> { + self.config.line_comment.as_deref() + } + pub fn start_server( &self, root_path: &Path, diff --git a/crates/language/src/lib.rs b/crates/language/src/lib.rs index 749c51a4b4..4c3fa9a515 100644 --- a/crates/language/src/lib.rs +++ b/crates/language/src/lib.rs @@ -1654,11 +1654,6 @@ impl Snapshot { None } - fn is_line_blank(&self, row: u32) -> bool { - self.text_for_range(Point::new(row, 0)..Point::new(row, self.line_len(row))) - .all(|chunk| chunk.matches(|c: char| !c.is_whitespace()).next().is_none()) - } - pub fn chunks<'a, T: ToOffset>( &'a self, range: Range, diff --git a/crates/zed/languages/rust/config.toml b/crates/zed/languages/rust/config.toml index 5719164008..655a264e6c 100644 --- a/crates/zed/languages/rust/config.toml +++ b/crates/zed/languages/rust/config.toml @@ -1,5 +1,6 @@ name = "Rust" path_suffixes = ["rs"] +line_comment = "// " brackets = [ { start = "{", end = "}", close = true, newline = true }, { start = "[", end = "]", close = true, newline = true },