Start work on toggling block comments for HTML
This commit is contained in:
parent
218ba81013
commit
d9fb8c90d8
3 changed files with 240 additions and 87 deletions
|
@ -4487,105 +4487,184 @@ impl Editor {
|
||||||
pub fn toggle_comments(&mut self, _: &ToggleComments, cx: &mut ViewContext<Self>) {
|
pub fn toggle_comments(&mut self, _: &ToggleComments, cx: &mut ViewContext<Self>) {
|
||||||
self.transact(cx, |this, cx| {
|
self.transact(cx, |this, cx| {
|
||||||
let mut selections = this.selections.all::<Point>(cx);
|
let mut selections = this.selections.all::<Point>(cx);
|
||||||
let mut all_selection_lines_are_comments = true;
|
let mut edits = Vec::new();
|
||||||
let mut edit_ranges = Vec::new();
|
let mut selection_edit_ranges = Vec::new();
|
||||||
let mut last_toggled_row = None;
|
let mut last_toggled_row = None;
|
||||||
this.buffer.update(cx, |buffer, cx| {
|
let snapshot = this.buffer.read(cx).read(cx);
|
||||||
// TODO: Handle selections that cross excerpts
|
let empty_str: Arc<str> = "".into();
|
||||||
for selection in &mut selections {
|
|
||||||
// Get the line comment prefix. Split its trailing whitespace into a separate string,
|
fn comment_prefix_range(
|
||||||
// as that portion won't be used for detecting if a line is a comment.
|
snapshot: &MultiBufferSnapshot,
|
||||||
let full_comment_prefix: Arc<str> = if let Some(prefix) = buffer
|
row: u32,
|
||||||
.language_at(selection.start, cx)
|
comment_prefix: &str,
|
||||||
.and_then(|l| l.line_comment_prefix().map(|p| p.into()))
|
comment_prefix_whitespace: &str,
|
||||||
{
|
) -> Range<Point> {
|
||||||
prefix
|
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<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 {
|
} 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 = full_comment_prefix.trim_end_matches(' ');
|
||||||
let comment_prefix_whitespace = &full_comment_prefix[comment_prefix.len()..];
|
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) {
|
if snapshot.is_line_blank(row) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
let start = Point::new(row, snapshot.indent_size_for_line(row).len);
|
let prefix_range = comment_prefix_range(
|
||||||
let mut line_bytes = snapshot
|
snapshot.deref(),
|
||||||
.bytes_in_range(start..snapshot.max_point())
|
row,
|
||||||
.flatten()
|
comment_prefix,
|
||||||
.copied();
|
comment_prefix_whitespace,
|
||||||
|
);
|
||||||
// If this line currently begins with the line comment prefix, then record
|
if prefix_range.is_empty() {
|
||||||
// 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;
|
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 {
|
||||||
if all_selection_lines_are_comments {
|
edits.extend(
|
||||||
let empty_str: Arc<str> = "".into();
|
selection_edit_ranges
|
||||||
buffer.edit(
|
.iter()
|
||||||
edit_ranges
|
.cloned()
|
||||||
.iter()
|
.map(|range| (range, empty_str.clone())),
|
||||||
.cloned()
|
);
|
||||||
.map(|range| (range, empty_str.clone())),
|
} else {
|
||||||
None,
|
let min_column = selection_edit_ranges
|
||||||
cx,
|
.iter()
|
||||||
);
|
.map(|r| r.start.column)
|
||||||
} else {
|
.min()
|
||||||
let min_column =
|
.unwrap_or(0);
|
||||||
edit_ranges.iter().map(|r| r.start.column).min().unwrap();
|
edits.extend(selection_edit_ranges.iter().map(|range| {
|
||||||
let edits = edit_ranges.iter().map(|range| {
|
let position = Point::new(range.start.row, min_column);
|
||||||
let position = Point::new(range.start.row, min_column);
|
(position..position, full_comment_prefix.clone())
|
||||||
(position..position, full_comment_prefix.clone())
|
}));
|
||||||
});
|
|
||||||
buffer.edit(edits, None, cx);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
} 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);
|
let selections = this.selections.all::<usize>(cx);
|
||||||
|
@ -10777,7 +10856,7 @@ mod tests {
|
||||||
cx.update(|cx| cx.set_global(Settings::test(cx)));
|
cx.update(|cx| cx.set_global(Settings::test(cx)));
|
||||||
let language = Arc::new(Language::new(
|
let language = Arc::new(Language::new(
|
||||||
LanguageConfig {
|
LanguageConfig {
|
||||||
line_comment: Some("// ".to_string()),
|
line_comment: Some("// ".into()),
|
||||||
..Default::default()
|
..Default::default()
|
||||||
},
|
},
|
||||||
Some(tree_sitter_rust::language()),
|
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]
|
#[gpui::test]
|
||||||
fn test_editing_disjoint_excerpts(cx: &mut gpui::MutableAppContext) {
|
fn test_editing_disjoint_excerpts(cx: &mut gpui::MutableAppContext) {
|
||||||
cx.set_global(Settings::test(cx));
|
cx.set_global(Settings::test(cx));
|
||||||
|
|
|
@ -231,7 +231,10 @@ pub struct LanguageConfig {
|
||||||
pub decrease_indent_pattern: Option<Regex>,
|
pub decrease_indent_pattern: Option<Regex>,
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub autoclose_before: String,
|
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 {
|
impl Default for LanguageConfig {
|
||||||
|
@ -245,6 +248,7 @@ impl Default for LanguageConfig {
|
||||||
decrease_indent_pattern: Default::default(),
|
decrease_indent_pattern: Default::default(),
|
||||||
autoclose_before: Default::default(),
|
autoclose_before: Default::default(),
|
||||||
line_comment: Default::default(),
|
line_comment: Default::default(),
|
||||||
|
block_comment: Default::default(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -768,8 +772,15 @@ impl Language {
|
||||||
self.config.name.clone()
|
self.config.name.clone()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn line_comment_prefix(&self) -> Option<&str> {
|
pub fn line_comment_prefix(&self) -> Option<&Arc<str>> {
|
||||||
self.config.line_comment.as_deref()
|
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] {
|
pub async fn disk_based_diagnostic_sources(&self) -> &[String] {
|
||||||
|
|
|
@ -8,3 +8,5 @@ brackets = [
|
||||||
{ start = "\"", end = "\"", close = true, newline = false },
|
{ start = "\"", end = "\"", close = true, newline = false },
|
||||||
{ start = "!--", end = " --", close = true, newline = false },
|
{ start = "!--", end = " --", close = true, newline = false },
|
||||||
]
|
]
|
||||||
|
|
||||||
|
block_comment = ["<!-- ", " -->"]
|
Loading…
Add table
Add a link
Reference in a new issue