Implement toggle-comments

This commit is contained in:
Max Brunsfeld 2021-11-23 14:13:28 -08:00
parent 2e2bce7322
commit f0db748ba1
5 changed files with 198 additions and 7 deletions

View File

@ -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<T: ToOffset>(&self, range: Range<T>) -> 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<T>(&self, range: Range<T>) -> 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)) {

View File

@ -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<Self>) {
// 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::<Point>(cx).collect::<Vec<_>>();
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::<usize>(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);

View File

@ -14,6 +14,7 @@ pub struct LanguageConfig {
pub name: String,
pub path_suffixes: Vec<String>,
pub brackets: Vec<BracketPair>,
pub line_comment: Option<String>,
pub language_server: Option<LanguageServerConfig>,
}
@ -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,

View File

@ -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<T>,

View File

@ -1,5 +1,6 @@
name = "Rust"
path_suffixes = ["rs"]
line_comment = "// "
brackets = [
{ start = "{", end = "}", close = true, newline = true },
{ start = "[", end = "]", close = true, newline = true },