Start work on toggling block comments for HTML

This commit is contained in:
Max Brunsfeld 2022-10-04 17:27:03 -07:00
parent 218ba81013
commit d9fb8c90d8
3 changed files with 240 additions and 87 deletions

View file

@ -4487,48 +4487,20 @@ impl Editor {
pub fn toggle_comments(&mut self, _: &ToggleComments, cx: &mut ViewContext<Self>) {
self.transact(cx, |this, cx| {
let mut selections = this.selections.all::<Point>(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<str> = if let Some(prefix) = buffer
.language_at(selection.start, cx)
.and_then(|l| l.line_comment_prefix().map(|p| p.into()))
{
prefix
} else {
return;
};
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);
}
if snapshot.is_line_blank(row) {
continue;
}
let snapshot = this.buffer.read(cx).read(cx);
let empty_str: Arc<str> = "".into();
fn comment_prefix_range(
snapshot: &MultiBufferSnapshot,
row: u32,
comment_prefix: &str,
comment_prefix_whitespace: &str,
) -> Range<Point> {
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()
@ -4536,8 +4508,7 @@ impl Editor {
// 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
if line_bytes
.by_ref()
.take(comment_prefix.len())
.eq(comment_prefix.bytes())
@ -4546,46 +4517,154 @@ impl Editor {
let matching_whitespace_len = line_bytes
.zip(comment_prefix_whitespace.bytes())
.take_while(|(a, b)| a == b)
.count()
as u32;
.count() as u32;
let end = Point::new(
row,
start.column
+ comment_prefix.len() as u32
+ matching_whitespace_len,
start.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);
start..end
} else {
start..start
}
}
if !edit_ranges.is_empty() {
fn comment_suffix_range(
snapshot: &MultiBufferSnapshot,
row: u32,
comment_suffix: &str,
comment_suffix_has_leading_space: bool,
) -> Range<Point> {
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 {
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()..];
for row in start_row..=end_row {
if snapshot.is_line_blank(row) {
continue;
}
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;
}
selection_edit_ranges.push(prefix_range);
}
if all_selection_lines_are_comments {
let empty_str: Arc<str> = "".into();
buffer.edit(
edit_ranges
edits.extend(
selection_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 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::<usize>(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(), " -->".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#"
<p>A</p>ˇ
<p>B</p>ˇ
<p>C</p>ˇ
"#
.unindent(),
);
cx.update_editor(|editor, cx| editor.toggle_comments(&ToggleComments, cx));
cx.assert_editor_state(
&r#"
<!-- <p>A</p>ˇ -->
<!-- <p>B</p>ˇ -->
<!-- <p>C</p>ˇ -->
"#
.unindent(),
);
}
#[gpui::test]
fn test_editing_disjoint_excerpts(cx: &mut gpui::MutableAppContext) {
cx.set_global(Settings::test(cx));

View file

@ -231,7 +231,10 @@ pub struct LanguageConfig {
pub decrease_indent_pattern: Option<Regex>,
#[serde(default)]
pub autoclose_before: String,
pub line_comment: Option<String>,
#[serde(default)]
pub line_comment: Option<Arc<str>>,
#[serde(default)]
pub block_comment: Option<(Arc<str>, Arc<str>)>,
}
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<str>> {
self.config.line_comment.as_ref()
}
pub fn block_comment_delimiters(&self) -> Option<(&Arc<str>, &Arc<str>)> {
self.config
.block_comment
.as_ref()
.map(|(start, end)| (start, end))
}
pub async fn disk_based_diagnostic_sources(&self) -> &[String] {

View file

@ -8,3 +8,5 @@ brackets = [
{ start = "\"", end = "\"", close = true, newline = false },
{ start = "!--", end = " --", close = true, newline = false },
]
block_comment = ["<!-- ", " -->"]