From d9fb8c90d8e3cec81d7915a5db37c00b5d5a6cdb Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Tue, 4 Oct 2022 17:27:03 -0700 Subject: [PATCH] Start work on toggling block comments for HTML --- crates/editor/src/editor.rs | 308 ++++++++++++++++------ crates/language/src/language.rs | 17 +- crates/zed/src/languages/html/config.toml | 2 + 3 files changed, 240 insertions(+), 87 deletions(-) diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index 93a47cf621b406ffa07291b19863e3421dfdc0d0..b2420c1c44ea448685fa43478c1a68b7d9293b9c 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -4487,105 +4487,184 @@ impl Editor { pub fn toggle_comments(&mut self, _: &ToggleComments, cx: &mut ViewContext) { self.transact(cx, |this, cx| { let mut selections = this.selections.all::(cx); - let mut all_selection_lines_are_comments = true; - let mut edit_ranges = Vec::new(); + let mut edits = Vec::new(); + let mut selection_edit_ranges = Vec::new(); let mut last_toggled_row = None; - this.buffer.update(cx, |buffer, cx| { - // TODO: Handle selections that cross excerpts - for selection in &mut selections { - // 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: Arc = if let Some(prefix) = buffer - .language_at(selection.start, cx) - .and_then(|l| l.line_comment_prefix().map(|p| p.into())) - { - prefix + let snapshot = this.buffer.read(cx).read(cx); + let empty_str: Arc = "".into(); + + fn comment_prefix_range( + snapshot: &MultiBufferSnapshot, + row: u32, + comment_prefix: &str, + comment_prefix_whitespace: &str, + ) -> Range { + let start = Point::new(row, snapshot.indent_size_for_line(row).len); + + let mut line_bytes = snapshot + .bytes_in_range(start..snapshot.max_point()) + .flatten() + .copied(); + + // If this line currently begins with the line comment prefix, then record + // the range containing the prefix. + if 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( + start.row, + start.column + comment_prefix.len() as u32 + matching_whitespace_len, + ); + start..end + } else { + start..start + } + } + + fn comment_suffix_range( + snapshot: &MultiBufferSnapshot, + row: u32, + comment_suffix: &str, + comment_suffix_has_leading_space: bool, + ) -> Range { + let end = Point::new(row, snapshot.line_len(row)); + let suffix_start_column = end.column.saturating_sub(comment_suffix.len() as u32); + + let mut line_end_bytes = snapshot + .bytes_in_range(Point::new(end.row, suffix_start_column.saturating_sub(1))..end) + .flatten() + .copied(); + + let leading_space_len = if suffix_start_column > 0 + && line_end_bytes.next() == Some(b' ') + && comment_suffix_has_leading_space + { + 1 + } else { + 0 + }; + + // If this line currently begins with the line comment prefix, then record + // the range containing the prefix. + if line_end_bytes.by_ref().eq(comment_suffix.bytes()) { + let start = Point::new(end.row, suffix_start_column - leading_space_len); + start..end + } else { + end..end + } + } + + // TODO: Handle selections that cross excerpts + for selection in &mut selections { + let language = if let Some(language) = snapshot.language_at(selection.start) { + language + } else { + continue; + }; + + let mut all_selection_lines_are_comments = true; + selection_edit_ranges.clear(); + + // If multiple selections contain a given row, avoid processing that + // row more than once. + let mut start_row = selection.start.row; + if last_toggled_row == Some(start_row) { + start_row += 1; + } + let end_row = + if selection.end.row > selection.start.row && selection.end.column == 0 { + selection.end.row - 1 } else { - return; + selection.end.row }; + last_toggled_row = Some(end_row); + + // If the language has line comments, toggle those. + if let Some(full_comment_prefix) = language.line_comment_prefix() { + // Split the comment prefix's trailing whitespace into a separate string, + // as that portion won't be used for detecting if a line is a comment. let comment_prefix = full_comment_prefix.trim_end_matches(' '); let comment_prefix_whitespace = &full_comment_prefix[comment_prefix.len()..]; - edit_ranges.clear(); - let snapshot = buffer.snapshot(cx); - - 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); - } + for row in start_row..=end_row { if snapshot.is_line_blank(row) { continue; } - let start = Point::new(row, snapshot.indent_size_for_line(row).len); - let mut line_bytes = snapshot - .bytes_in_range(start..snapshot.max_point()) - .flatten() - .copied(); - - // 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 { + let prefix_range = comment_prefix_range( + snapshot.deref(), + row, + comment_prefix, + comment_prefix_whitespace, + ); + if prefix_range.is_empty() { all_selection_lines_are_comments = false; - edit_ranges.push(start..start); } + selection_edit_ranges.push(prefix_range); } - if !edit_ranges.is_empty() { - if all_selection_lines_are_comments { - let empty_str: Arc = "".into(); - buffer.edit( - edit_ranges - .iter() - .cloned() - .map(|range| (range, empty_str.clone())), - None, - cx, - ); - } else { - let min_column = - edit_ranges.iter().map(|r| r.start.column).min().unwrap(); - let edits = edit_ranges.iter().map(|range| { - let position = Point::new(range.start.row, min_column); - (position..position, full_comment_prefix.clone()) - }); - buffer.edit(edits, None, cx); - } + if all_selection_lines_are_comments { + edits.extend( + selection_edit_ranges + .iter() + .cloned() + .map(|range| (range, empty_str.clone())), + ); + } else { + let min_column = selection_edit_ranges + .iter() + .map(|r| r.start.column) + .min() + .unwrap_or(0); + edits.extend(selection_edit_ranges.iter().map(|range| { + let position = Point::new(range.start.row, min_column); + (position..position, full_comment_prefix.clone()) + })); + } + } else if let Some((full_comment_prefix, comment_suffix)) = + language.block_comment_delimiters() + { + let comment_prefix = full_comment_prefix.trim_end_matches(' '); + let comment_prefix_whitespace = &full_comment_prefix[comment_prefix.len()..]; + + let prefix_range = comment_prefix_range( + snapshot.deref(), + start_row, + comment_prefix, + comment_prefix_whitespace, + ); + let suffix_range = comment_suffix_range( + snapshot.deref(), + end_row, + comment_suffix.trim_start_matches(' '), + comment_suffix.starts_with(' '), + ); + + if prefix_range.is_empty() || suffix_range.is_empty() { + edits.push(( + prefix_range.start..prefix_range.start, + full_comment_prefix.clone(), + )); + edits.push((suffix_range.end..suffix_range.end, comment_suffix.clone())); + } else { + edits.push((prefix_range, empty_str.clone())); + edits.push((suffix_range, empty_str.clone())); } + } else { + continue; } + } + + drop(snapshot); + this.buffer.update(cx, |buffer, cx| { + buffer.edit(edits, None, cx); }); let selections = this.selections.all::(cx); @@ -10777,7 +10856,7 @@ mod tests { cx.update(|cx| cx.set_global(Settings::test(cx))); let language = Arc::new(Language::new( LanguageConfig { - line_comment: Some("// ".to_string()), + line_comment: Some("// ".into()), ..Default::default() }, Some(tree_sitter_rust::language()), @@ -10855,6 +10934,67 @@ mod tests { }); } + #[gpui::test] + async fn test_toggle_block_comment(cx: &mut gpui::TestAppContext) { + let mut cx = EditorTestContext::new(cx); + + let html_language = Arc::new( + Language::new( + LanguageConfig { + name: "HTML".into(), + block_comment: Some(("".into())), + ..Default::default() + }, + Some(tree_sitter_html::language()), + ) + .with_injection_query( + r#" + (script_element + (raw_text) @content + (#set! "language" "javascript")) + "#, + ) + .unwrap(), + ); + + let javascript_language = Arc::new(Language::new( + LanguageConfig { + name: "JavaScript".into(), + line_comment: Some("// ".into()), + ..Default::default() + }, + Some(tree_sitter_javascript::language()), + )); + + let registry = Arc::new(LanguageRegistry::test()); + registry.add(html_language.clone()); + registry.add(javascript_language.clone()); + + cx.update_buffer(|buffer, cx| { + buffer.set_language_registry(registry); + buffer.set_language(Some(html_language), cx); + }); + + cx.set_state( + &r#" +

A

ˇ +

B

ˇ +

C

ˇ + "# + .unindent(), + ); + + cx.update_editor(|editor, cx| editor.toggle_comments(&ToggleComments, cx)); + cx.assert_editor_state( + &r#" + + + + "# + .unindent(), + ); + } + #[gpui::test] fn test_editing_disjoint_excerpts(cx: &mut gpui::MutableAppContext) { cx.set_global(Settings::test(cx)); diff --git a/crates/language/src/language.rs b/crates/language/src/language.rs index 59da0909c61ef55d5e1e7451d5fdf791d0012792..c7c5def833c97dc52dc2dab386e0f029c52c9ef7 100644 --- a/crates/language/src/language.rs +++ b/crates/language/src/language.rs @@ -231,7 +231,10 @@ pub struct LanguageConfig { pub decrease_indent_pattern: Option, #[serde(default)] pub autoclose_before: String, - pub line_comment: Option, + #[serde(default)] + pub line_comment: Option>, + #[serde(default)] + pub block_comment: Option<(Arc, Arc)>, } impl Default for LanguageConfig { @@ -245,6 +248,7 @@ impl Default for LanguageConfig { decrease_indent_pattern: Default::default(), autoclose_before: Default::default(), line_comment: Default::default(), + block_comment: Default::default(), } } } @@ -768,8 +772,15 @@ impl Language { self.config.name.clone() } - pub fn line_comment_prefix(&self) -> Option<&str> { - self.config.line_comment.as_deref() + pub fn line_comment_prefix(&self) -> Option<&Arc> { + self.config.line_comment.as_ref() + } + + pub fn block_comment_delimiters(&self) -> Option<(&Arc, &Arc)> { + self.config + .block_comment + .as_ref() + .map(|(start, end)| (start, end)) } pub async fn disk_based_diagnostic_sources(&self) -> &[String] { diff --git a/crates/zed/src/languages/html/config.toml b/crates/zed/src/languages/html/config.toml index 80b33b1243f6fdb0721baa6b51d3c4e57fb68fe3..3e618da25e06f5f37c3106db12bd1d6abf2b1a4c 100644 --- a/crates/zed/src/languages/html/config.toml +++ b/crates/zed/src/languages/html/config.toml @@ -8,3 +8,5 @@ brackets = [ { start = "\"", end = "\"", close = true, newline = false }, { start = "!--", end = " --", close = true, newline = false }, ] + +block_comment = [""] \ No newline at end of file