html: Open extra newline between opening and closing HTML tags (#25130)
Closes #12064 It feels a bit strange to use `brackets` for this but it seems to work without unintended consequences from my testing so far. Release Notes: - N/A --------- Co-authored-by: Marshall Bowers <git@maxdeviant.com>
This commit is contained in:
parent
528da6eb26
commit
1429363218
7 changed files with 199 additions and 62 deletions
|
@ -101,10 +101,10 @@ use language::{
|
|||
language_settings::{
|
||||
self, all_language_settings, language_settings, InlayHintSettings, RewrapBehavior,
|
||||
},
|
||||
point_from_lsp, text_diff_with_options, AutoindentMode, BracketPair, Buffer, Capability,
|
||||
CharKind, CodeLabel, CursorShape, Diagnostic, DiffOptions, DiskState, EditPredictionsMode,
|
||||
EditPreview, HighlightedText, IndentKind, IndentSize, Language, OffsetRangeExt, Point,
|
||||
Selection, SelectionGoal, TextObject, TransactionId, TreeSitterOptions,
|
||||
point_from_lsp, text_diff_with_options, AutoindentMode, BracketMatch, BracketPair, Buffer,
|
||||
Capability, CharKind, CodeLabel, CursorShape, Diagnostic, DiffOptions, DiskState,
|
||||
EditPredictionsMode, EditPreview, HighlightedText, IndentKind, IndentSize, Language,
|
||||
OffsetRangeExt, Point, Selection, SelectionGoal, TextObject, TransactionId, TreeSitterOptions,
|
||||
};
|
||||
use language::{point_to_lsp, BufferRow, CharClassifier, Runnable, RunnableRange};
|
||||
use linked_editing_ranges::refresh_linked_ranges;
|
||||
|
@ -3153,35 +3153,9 @@ impl Editor {
|
|||
let (comment_delimiter, insert_extra_newline) = if let Some(language) =
|
||||
&language_scope
|
||||
{
|
||||
let leading_whitespace_len = buffer
|
||||
.reversed_chars_at(start)
|
||||
.take_while(|c| c.is_whitespace() && *c != '\n')
|
||||
.map(|c| c.len_utf8())
|
||||
.sum::<usize>();
|
||||
|
||||
let trailing_whitespace_len = buffer
|
||||
.chars_at(end)
|
||||
.take_while(|c| c.is_whitespace() && *c != '\n')
|
||||
.map(|c| c.len_utf8())
|
||||
.sum::<usize>();
|
||||
|
||||
let insert_extra_newline =
|
||||
language.brackets().any(|(pair, enabled)| {
|
||||
let pair_start = pair.start.trim_end();
|
||||
let pair_end = pair.end.trim_start();
|
||||
|
||||
enabled
|
||||
&& pair.newline
|
||||
&& buffer.contains_str_at(
|
||||
end + trailing_whitespace_len,
|
||||
pair_end,
|
||||
)
|
||||
&& buffer.contains_str_at(
|
||||
(start - leading_whitespace_len)
|
||||
.saturating_sub(pair_start.len()),
|
||||
pair_start,
|
||||
)
|
||||
});
|
||||
insert_extra_newline_brackets(&buffer, start..end, language)
|
||||
|| insert_extra_newline_tree_sitter(&buffer, start..end);
|
||||
|
||||
// Comment extension on newline is allowed only for cursor selections
|
||||
let comment_delimiter = maybe!({
|
||||
|
@ -15088,6 +15062,72 @@ impl Editor {
|
|||
}
|
||||
}
|
||||
|
||||
fn insert_extra_newline_brackets(
|
||||
buffer: &MultiBufferSnapshot,
|
||||
range: Range<usize>,
|
||||
language: &language::LanguageScope,
|
||||
) -> bool {
|
||||
let leading_whitespace_len = buffer
|
||||
.reversed_chars_at(range.start)
|
||||
.take_while(|c| c.is_whitespace() && *c != '\n')
|
||||
.map(|c| c.len_utf8())
|
||||
.sum::<usize>();
|
||||
let trailing_whitespace_len = buffer
|
||||
.chars_at(range.end)
|
||||
.take_while(|c| c.is_whitespace() && *c != '\n')
|
||||
.map(|c| c.len_utf8())
|
||||
.sum::<usize>();
|
||||
let range = range.start - leading_whitespace_len..range.end + trailing_whitespace_len;
|
||||
|
||||
language.brackets().any(|(pair, enabled)| {
|
||||
let pair_start = pair.start.trim_end();
|
||||
let pair_end = pair.end.trim_start();
|
||||
|
||||
enabled
|
||||
&& pair.newline
|
||||
&& buffer.contains_str_at(range.end, pair_end)
|
||||
&& buffer.contains_str_at(range.start.saturating_sub(pair_start.len()), pair_start)
|
||||
})
|
||||
}
|
||||
|
||||
fn insert_extra_newline_tree_sitter(buffer: &MultiBufferSnapshot, range: Range<usize>) -> bool {
|
||||
let (buffer, range) = match buffer.range_to_buffer_ranges(range).as_slice() {
|
||||
[(buffer, range, _)] => (*buffer, range.clone()),
|
||||
_ => return false,
|
||||
};
|
||||
let pair = {
|
||||
let mut result: Option<BracketMatch> = None;
|
||||
|
||||
for pair in buffer
|
||||
.all_bracket_ranges(range.clone())
|
||||
.filter(move |pair| {
|
||||
pair.open_range.start <= range.start && pair.close_range.end >= range.end
|
||||
})
|
||||
{
|
||||
let len = pair.close_range.end - pair.open_range.start;
|
||||
|
||||
if let Some(existing) = &result {
|
||||
let existing_len = existing.close_range.end - existing.open_range.start;
|
||||
if len > existing_len {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
result = Some(pair);
|
||||
}
|
||||
|
||||
result
|
||||
};
|
||||
let Some(pair) = pair else {
|
||||
return false;
|
||||
};
|
||||
pair.newline_only
|
||||
&& buffer
|
||||
.chars_for_range(pair.open_range.end..range.start)
|
||||
.chain(buffer.chars_for_range(range.end..pair.close_range.start))
|
||||
.all(|c| c.is_whitespace() && c != '\n')
|
||||
}
|
||||
|
||||
fn get_uncommitted_diff_for_buffer(
|
||||
project: &Entity<Project>,
|
||||
buffers: impl IntoIterator<Item = Entity<Buffer>>,
|
||||
|
|
|
@ -15934,6 +15934,60 @@ async fn test_rename_without_prepare(cx: &mut gpui::TestAppContext) {
|
|||
"});
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_tree_sitter_brackets_newline_insertion(cx: &mut TestAppContext) {
|
||||
init_test(cx, |_| {});
|
||||
let mut cx = EditorTestContext::new(cx).await;
|
||||
|
||||
let language = Arc::new(
|
||||
Language::new(
|
||||
LanguageConfig::default(),
|
||||
Some(tree_sitter_html::LANGUAGE.into()),
|
||||
)
|
||||
.with_brackets_query(
|
||||
r#"
|
||||
("<" @open "/>" @close)
|
||||
("</" @open ">" @close)
|
||||
("<" @open ">" @close)
|
||||
("\"" @open "\"" @close)
|
||||
((element (start_tag) @open (end_tag) @close) (#set! newline.only))
|
||||
"#,
|
||||
)
|
||||
.unwrap(),
|
||||
);
|
||||
cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
|
||||
|
||||
cx.set_state(indoc! {"
|
||||
<span>ˇ</span>
|
||||
"});
|
||||
cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
|
||||
cx.assert_editor_state(indoc! {"
|
||||
<span>
|
||||
ˇ
|
||||
</span>
|
||||
"});
|
||||
|
||||
cx.set_state(indoc! {"
|
||||
<span><span></span>ˇ</span>
|
||||
"});
|
||||
cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
|
||||
cx.assert_editor_state(indoc! {"
|
||||
<span><span></span>
|
||||
ˇ</span>
|
||||
"});
|
||||
|
||||
cx.set_state(indoc! {"
|
||||
<span>ˇ
|
||||
</span>
|
||||
"});
|
||||
cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
|
||||
cx.assert_editor_state(indoc! {"
|
||||
<span>
|
||||
ˇ
|
||||
</span>
|
||||
"});
|
||||
}
|
||||
|
||||
fn empty_range(row: usize, column: usize) -> Range<DisplayPoint> {
|
||||
let point = DisplayPoint::new(DisplayRow(row as u32), column as u32);
|
||||
point..point
|
||||
|
|
|
@ -782,6 +782,13 @@ impl EditPreview {
|
|||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub struct BracketMatch {
|
||||
pub open_range: Range<usize>,
|
||||
pub close_range: Range<usize>,
|
||||
pub newline_only: bool,
|
||||
}
|
||||
|
||||
impl Buffer {
|
||||
/// Create a new buffer with the given base text.
|
||||
pub fn local<T: Into<String>>(base_text: T, cx: &Context<Self>) -> Self {
|
||||
|
@ -3556,15 +3563,10 @@ impl BufferSnapshot {
|
|||
self.syntax.matches(range, self, query)
|
||||
}
|
||||
|
||||
/// Returns bracket range pairs overlapping or adjacent to `range`
|
||||
pub fn bracket_ranges<T: ToOffset>(
|
||||
pub fn all_bracket_ranges(
|
||||
&self,
|
||||
range: Range<T>,
|
||||
) -> impl Iterator<Item = (Range<usize>, Range<usize>)> + '_ {
|
||||
// Find bracket pairs that *inclusively* contain the given range.
|
||||
let range = range.start.to_offset(self).saturating_sub(1)
|
||||
..self.len().min(range.end.to_offset(self) + 1);
|
||||
|
||||
range: Range<usize>,
|
||||
) -> impl Iterator<Item = BracketMatch> + '_ {
|
||||
let mut matches = self.syntax.matches(range.clone(), &self.text, |grammar| {
|
||||
grammar.brackets_config.as_ref().map(|c| &c.query)
|
||||
});
|
||||
|
@ -3579,6 +3581,7 @@ impl BufferSnapshot {
|
|||
let mut open = None;
|
||||
let mut close = None;
|
||||
let config = &configs[mat.grammar_index];
|
||||
let pattern = &config.patterns[mat.pattern_index];
|
||||
for capture in mat.captures {
|
||||
if capture.index == config.open_capture_ix {
|
||||
open = Some(capture.node.byte_range());
|
||||
|
@ -3589,21 +3592,37 @@ impl BufferSnapshot {
|
|||
|
||||
matches.advance();
|
||||
|
||||
let Some((open, close)) = open.zip(close) else {
|
||||
let Some((open_range, close_range)) = open.zip(close) else {
|
||||
continue;
|
||||
};
|
||||
|
||||
let bracket_range = open.start..=close.end;
|
||||
let bracket_range = open_range.start..=close_range.end;
|
||||
if !bracket_range.overlaps(&range) {
|
||||
continue;
|
||||
}
|
||||
|
||||
return Some((open, close));
|
||||
return Some(BracketMatch {
|
||||
open_range,
|
||||
close_range,
|
||||
newline_only: pattern.newline_only,
|
||||
});
|
||||
}
|
||||
None
|
||||
})
|
||||
}
|
||||
|
||||
/// Returns bracket range pairs overlapping or adjacent to `range`
|
||||
pub fn bracket_ranges<T: ToOffset>(
|
||||
&self,
|
||||
range: Range<T>,
|
||||
) -> impl Iterator<Item = BracketMatch> + '_ {
|
||||
// Find bracket pairs that *inclusively* contain the given range.
|
||||
let range = range.start.to_offset(self).saturating_sub(1)
|
||||
..self.len().min(range.end.to_offset(self) + 1);
|
||||
self.all_bracket_ranges(range)
|
||||
.filter(|pair| !pair.newline_only)
|
||||
}
|
||||
|
||||
pub fn text_object_ranges<T: ToOffset>(
|
||||
&self,
|
||||
range: Range<T>,
|
||||
|
@ -3674,11 +3693,12 @@ impl BufferSnapshot {
|
|||
pub fn enclosing_bracket_ranges<T: ToOffset>(
|
||||
&self,
|
||||
range: Range<T>,
|
||||
) -> impl Iterator<Item = (Range<usize>, Range<usize>)> + '_ {
|
||||
) -> impl Iterator<Item = BracketMatch> + '_ {
|
||||
let range = range.start.to_offset(self)..range.end.to_offset(self);
|
||||
|
||||
self.bracket_ranges(range.clone())
|
||||
.filter(move |(open, close)| open.start <= range.start && close.end >= range.end)
|
||||
self.bracket_ranges(range.clone()).filter(move |pair| {
|
||||
pair.open_range.start <= range.start && pair.close_range.end >= range.end
|
||||
})
|
||||
}
|
||||
|
||||
/// Returns the smallest enclosing bracket ranges containing the given range or None if no brackets contain range
|
||||
|
@ -3694,14 +3714,14 @@ impl BufferSnapshot {
|
|||
// Get the ranges of the innermost pair of brackets.
|
||||
let mut result: Option<(Range<usize>, Range<usize>)> = None;
|
||||
|
||||
for (open, close) in self.enclosing_bracket_ranges(range.clone()) {
|
||||
for pair in self.enclosing_bracket_ranges(range.clone()) {
|
||||
if let Some(range_filter) = range_filter {
|
||||
if !range_filter(open.clone(), close.clone()) {
|
||||
if !range_filter(pair.open_range.clone(), pair.close_range.clone()) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
let len = close.end - open.start;
|
||||
let len = pair.close_range.end - pair.open_range.start;
|
||||
|
||||
if let Some((existing_open, existing_close)) = &result {
|
||||
let existing_len = existing_close.end - existing_open.start;
|
||||
|
@ -3710,7 +3730,7 @@ impl BufferSnapshot {
|
|||
}
|
||||
}
|
||||
|
||||
result = Some((open, close));
|
||||
result = Some((pair.open_range, pair.close_range));
|
||||
}
|
||||
|
||||
result
|
||||
|
|
|
@ -3401,7 +3401,10 @@ fn assert_bracket_pairs(
|
|||
.collect::<Vec<_>>();
|
||||
|
||||
assert_set_eq!(
|
||||
buffer.bracket_ranges(selection_range).collect::<Vec<_>>(),
|
||||
buffer
|
||||
.bracket_ranges(selection_range)
|
||||
.map(|pair| (pair.open_range, pair.close_range))
|
||||
.collect::<Vec<_>>(),
|
||||
bracket_pairs
|
||||
);
|
||||
}
|
||||
|
|
|
@ -918,7 +918,7 @@ pub struct Grammar {
|
|||
pub ts_language: tree_sitter::Language,
|
||||
pub(crate) error_query: Option<Query>,
|
||||
pub(crate) highlights_query: Option<Query>,
|
||||
pub(crate) brackets_config: Option<BracketConfig>,
|
||||
pub(crate) brackets_config: Option<BracketsConfig>,
|
||||
pub(crate) redactions_config: Option<RedactionConfig>,
|
||||
pub(crate) runnable_config: Option<RunnableConfig>,
|
||||
pub(crate) indents_config: Option<IndentConfig>,
|
||||
|
@ -1039,10 +1039,16 @@ struct InjectionPatternConfig {
|
|||
combined: bool,
|
||||
}
|
||||
|
||||
struct BracketConfig {
|
||||
struct BracketsConfig {
|
||||
query: Query,
|
||||
open_capture_ix: u32,
|
||||
close_capture_ix: u32,
|
||||
patterns: Vec<BracketsPatternConfig>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Default)]
|
||||
struct BracketsPatternConfig {
|
||||
newline_only: bool,
|
||||
}
|
||||
|
||||
impl Language {
|
||||
|
@ -1284,11 +1290,24 @@ impl Language {
|
|||
("close", &mut close_capture_ix),
|
||||
],
|
||||
);
|
||||
let patterns = (0..query.pattern_count())
|
||||
.map(|ix| {
|
||||
let mut config = BracketsPatternConfig::default();
|
||||
for setting in query.property_settings(ix) {
|
||||
match setting.key.as_ref() {
|
||||
"newline.only" => config.newline_only = true,
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
config
|
||||
})
|
||||
.collect();
|
||||
if let Some((open_capture_ix, close_capture_ix)) = open_capture_ix.zip(close_capture_ix) {
|
||||
grammar.brackets_config = Some(BracketConfig {
|
||||
grammar.brackets_config = Some(BracketsConfig {
|
||||
query,
|
||||
open_capture_ix,
|
||||
close_capture_ix,
|
||||
patterns,
|
||||
});
|
||||
}
|
||||
Ok(self)
|
||||
|
|
|
@ -5156,11 +5156,11 @@ impl MultiBufferSnapshot {
|
|||
excerpt
|
||||
.buffer()
|
||||
.enclosing_bracket_ranges(excerpt.map_range_to_buffer(range))
|
||||
.filter_map(move |(open, close)| {
|
||||
if excerpt.contains_buffer_range(open.start..close.end) {
|
||||
.filter_map(move |pair| {
|
||||
if excerpt.contains_buffer_range(pair.open_range.start..pair.close_range.end) {
|
||||
Some((
|
||||
excerpt.map_range_from_buffer(open),
|
||||
excerpt.map_range_from_buffer(close),
|
||||
excerpt.map_range_from_buffer(pair.open_range),
|
||||
excerpt.map_range_from_buffer(pair.close_range),
|
||||
))
|
||||
} else {
|
||||
None
|
||||
|
@ -5207,12 +5207,12 @@ impl MultiBufferSnapshot {
|
|||
excerpt
|
||||
.buffer()
|
||||
.bracket_ranges(excerpt.map_range_to_buffer(range))
|
||||
.filter_map(move |(start_bracket_range, close_bracket_range)| {
|
||||
let buffer_range = start_bracket_range.start..close_bracket_range.end;
|
||||
.filter_map(move |pair| {
|
||||
let buffer_range = pair.open_range.start..pair.close_range.end;
|
||||
if excerpt.contains_buffer_range(buffer_range) {
|
||||
Some((
|
||||
excerpt.map_range_from_buffer(start_bracket_range),
|
||||
excerpt.map_range_from_buffer(close_bracket_range),
|
||||
excerpt.map_range_from_buffer(pair.open_range),
|
||||
excerpt.map_range_from_buffer(pair.close_range),
|
||||
))
|
||||
} else {
|
||||
None
|
||||
|
|
|
@ -2,3 +2,4 @@
|
|||
("</" @open ">" @close)
|
||||
("<" @open ">" @close)
|
||||
("\"" @open "\"" @close)
|
||||
((element (start_tag) @open (end_tag) @close) (#set! newline.only))
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue