Add support for auto-closing of JSX tags (#25681)
Closes #4271 Implemented by kicking of a task on the main thread at the end of `Editor::handle_input` which waits for the buffer to be re-parsed before checking if JSX tag completion possible based on the recent edits, and if it is then it spawns a task on the background thread to generate the edits to be auto-applied to the buffer Release Notes: - Added support for auto-closing of JSX tags --------- Co-authored-by: Cole Miller <cole@zed.dev> Co-authored-by: Max Brunsfeld <max@zed.dev> Co-authored-by: Marshall Bowers <git@maxdeviant.com> Co-authored-by: Mikayla <mikayla@zed.dev> Co-authored-by: Peter Tripp <peter@zed.dev>
This commit is contained in:
parent
05df3d1bd6
commit
ff25fa24e7
15 changed files with 1207 additions and 149 deletions
1
Cargo.lock
generated
1
Cargo.lock
generated
|
@ -4154,6 +4154,7 @@ dependencies = [
|
|||
"inline_completion",
|
||||
"itertools 0.14.0",
|
||||
"language",
|
||||
"languages",
|
||||
"linkify",
|
||||
"log",
|
||||
"lsp",
|
||||
|
|
|
@ -1298,6 +1298,11 @@
|
|||
// "semi": false,
|
||||
// "singleQuote": true
|
||||
},
|
||||
// Settings for auto-closing of JSX tags.
|
||||
"jsx_tag_auto_close": {
|
||||
// // Whether to auto-close JSX tags.
|
||||
// "enabled": true
|
||||
},
|
||||
// LSP Specific settings.
|
||||
"lsp": {
|
||||
// Specify the LSP name as a key here.
|
||||
|
|
|
@ -94,6 +94,7 @@ ctor.workspace = true
|
|||
env_logger.workspace = true
|
||||
gpui = { workspace = true, features = ["test-support"] }
|
||||
language = { workspace = true, features = ["test-support"] }
|
||||
languages = {workspace = true, features = ["test-support"] }
|
||||
lsp = { workspace = true, features = ["test-support"] }
|
||||
multi_buffer = { workspace = true, features = ["test-support"] }
|
||||
project = { workspace = true, features = ["test-support"] }
|
||||
|
|
|
@ -28,6 +28,7 @@ mod hover_popover;
|
|||
mod indent_guides;
|
||||
mod inlay_hint_cache;
|
||||
pub mod items;
|
||||
mod jsx_tag_auto_close;
|
||||
mod linked_editing_ranges;
|
||||
mod lsp_ext;
|
||||
mod mouse_context_menu;
|
||||
|
@ -724,6 +725,7 @@ pub struct Editor {
|
|||
use_autoclose: bool,
|
||||
use_auto_surround: bool,
|
||||
auto_replace_emoji_shortcode: bool,
|
||||
jsx_tag_auto_close_enabled_in_any_buffer: bool,
|
||||
show_git_blame_gutter: bool,
|
||||
show_git_blame_inline: bool,
|
||||
show_git_blame_inline_delay_task: Option<Task<()>>,
|
||||
|
@ -1410,6 +1412,7 @@ impl Editor {
|
|||
use_autoclose: true,
|
||||
use_auto_surround: true,
|
||||
auto_replace_emoji_shortcode: false,
|
||||
jsx_tag_auto_close_enabled_in_any_buffer: false,
|
||||
leader_peer_id: None,
|
||||
remote_id: None,
|
||||
hover_state: Default::default(),
|
||||
|
@ -1493,6 +1496,7 @@ impl Editor {
|
|||
|
||||
this.end_selection(window, cx);
|
||||
this.scroll_manager.show_scrollbar(window, cx);
|
||||
jsx_tag_auto_close::refresh_enabled_in_any_buffer(&mut this, &buffer, cx);
|
||||
|
||||
if mode == EditorMode::Full {
|
||||
let should_auto_hide_scrollbars = cx.should_auto_hide_scrollbars();
|
||||
|
@ -3101,6 +3105,9 @@ impl Editor {
|
|||
drop(snapshot);
|
||||
|
||||
self.transact(window, cx, |this, window, cx| {
|
||||
let initial_buffer_versions =
|
||||
jsx_tag_auto_close::construct_initial_buffer_versions_map(this, &edits, cx);
|
||||
|
||||
this.buffer.update(cx, |buffer, cx| {
|
||||
buffer.edit(edits, this.autoindent_mode.clone(), cx);
|
||||
});
|
||||
|
@ -3188,6 +3195,7 @@ impl Editor {
|
|||
this.trigger_completion_on_input(&text, trigger_in_words, window, cx);
|
||||
linked_editing_ranges::refresh_linked_ranges(this, window, cx);
|
||||
this.refresh_inline_completion(true, false, window, cx);
|
||||
jsx_tag_auto_close::handle_from(this, initial_buffer_versions, window, cx);
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -15393,6 +15401,7 @@ impl Editor {
|
|||
let buffer = self.buffer.read(cx);
|
||||
self.registered_buffers
|
||||
.retain(|buffer_id, _| buffer.buffer(*buffer_id).is_some());
|
||||
jsx_tag_auto_close::refresh_enabled_in_any_buffer(self, multibuffer, cx);
|
||||
cx.emit(EditorEvent::ExcerptsRemoved { ids: ids.clone() })
|
||||
}
|
||||
multi_buffer::Event::ExcerptsEdited {
|
||||
|
@ -15412,6 +15421,7 @@ impl Editor {
|
|||
}
|
||||
multi_buffer::Event::Reparsed(buffer_id) => {
|
||||
self.tasks_update_task = Some(self.refresh_runnables(window, cx));
|
||||
jsx_tag_auto_close::refresh_enabled_in_any_buffer(self, multibuffer, cx);
|
||||
|
||||
cx.emit(EditorEvent::Reparsed(*buffer_id));
|
||||
}
|
||||
|
@ -15420,6 +15430,7 @@ impl Editor {
|
|||
}
|
||||
multi_buffer::Event::LanguageChanged(buffer_id) => {
|
||||
linked_editing_ranges::refresh_linked_ranges(self, window, cx);
|
||||
jsx_tag_auto_close::refresh_enabled_in_any_buffer(self, multibuffer, cx);
|
||||
cx.emit(EditorEvent::Reparsed(*buffer_id));
|
||||
cx.notify();
|
||||
}
|
||||
|
|
|
@ -16779,6 +16779,245 @@ async fn test_tree_sitter_brackets_newline_insertion(cx: &mut TestAppContext) {
|
|||
"});
|
||||
}
|
||||
|
||||
mod autoclose_tags {
|
||||
use super::*;
|
||||
use language::language_settings::JsxTagAutoCloseSettings;
|
||||
use languages::language;
|
||||
|
||||
async fn test_setup(cx: &mut TestAppContext) -> EditorTestContext {
|
||||
init_test(cx, |settings| {
|
||||
settings.defaults.jsx_tag_auto_close = Some(JsxTagAutoCloseSettings { enabled: true });
|
||||
});
|
||||
|
||||
let mut cx = EditorTestContext::new(cx).await;
|
||||
cx.update_buffer(|buffer, cx| {
|
||||
let language = language("tsx", tree_sitter_typescript::LANGUAGE_TSX.into());
|
||||
|
||||
buffer.set_language(Some(language), cx)
|
||||
});
|
||||
|
||||
cx
|
||||
}
|
||||
|
||||
macro_rules! check {
|
||||
($name:ident, $initial:literal + $input:literal => $expected:expr) => {
|
||||
#[gpui::test]
|
||||
async fn $name(cx: &mut TestAppContext) {
|
||||
let mut cx = test_setup(cx).await;
|
||||
cx.set_state($initial);
|
||||
cx.run_until_parked();
|
||||
|
||||
cx.update_editor(|editor, window, cx| {
|
||||
editor.handle_input($input, window, cx);
|
||||
});
|
||||
cx.run_until_parked();
|
||||
cx.assert_editor_state($expected);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
check!(
|
||||
test_basic,
|
||||
"<divˇ" + ">" => "<div>ˇ</div>"
|
||||
);
|
||||
|
||||
check!(
|
||||
test_basic_nested,
|
||||
"<div><divˇ</div>" + ">" => "<div><div>ˇ</div></div>"
|
||||
);
|
||||
|
||||
check!(
|
||||
test_basic_ignore_already_closed,
|
||||
"<div><divˇ</div></div>" + ">" => "<div><div>ˇ</div></div>"
|
||||
);
|
||||
|
||||
check!(
|
||||
test_doesnt_autoclose_closing_tag,
|
||||
"</divˇ" + ">" => "</div>ˇ"
|
||||
);
|
||||
|
||||
check!(
|
||||
test_jsx_attr,
|
||||
"<div attr={</div>}ˇ" + ">" => "<div attr={</div>}>ˇ</div>"
|
||||
);
|
||||
|
||||
check!(
|
||||
test_ignores_closing_tags_in_expr_block,
|
||||
"<div><divˇ{</div>}</div>" + ">" => "<div><div>ˇ</div>{</div>}</div>"
|
||||
);
|
||||
|
||||
check!(
|
||||
test_doesnt_autoclose_on_gt_in_expr,
|
||||
"<div attr={1 ˇ" + ">" => "<div attr={1 >ˇ"
|
||||
);
|
||||
|
||||
check!(
|
||||
test_ignores_closing_tags_with_different_tag_names,
|
||||
"<div><divˇ</div></span>" + ">" => "<div><div>ˇ</div></div></span>"
|
||||
);
|
||||
|
||||
check!(
|
||||
test_autocloses_in_jsx_expression,
|
||||
"<div>{<divˇ}</div>" + ">" => "<div>{<div>ˇ</div>}</div>"
|
||||
);
|
||||
|
||||
check!(
|
||||
test_doesnt_autoclose_already_closed_in_jsx_expression,
|
||||
"<div>{<divˇ</div>}</div>" + ">" => "<div>{<div>ˇ</div>}</div>"
|
||||
);
|
||||
|
||||
check!(
|
||||
test_autocloses_fragment,
|
||||
"<ˇ" + ">" => "<>ˇ</>"
|
||||
);
|
||||
|
||||
check!(
|
||||
test_does_not_include_type_argument_in_autoclose_tag_name,
|
||||
"<Component<T> attr={boolean_value}ˇ" + ">" => "<Component<T> attr={boolean_value}>ˇ</Component>"
|
||||
);
|
||||
|
||||
check!(
|
||||
test_does_not_autoclose_doctype,
|
||||
"<!DOCTYPE htmlˇ" + ">" => "<!DOCTYPE html>ˇ"
|
||||
);
|
||||
|
||||
check!(
|
||||
test_does_not_autoclose_comment,
|
||||
"<!-- comment --ˇ" + ">" => "<!-- comment -->ˇ"
|
||||
);
|
||||
|
||||
check!(
|
||||
test_multi_cursor_autoclose_same_tag,
|
||||
r#"
|
||||
<divˇ
|
||||
<divˇ
|
||||
"#
|
||||
+ ">" =>
|
||||
r#"
|
||||
<div>ˇ</div>
|
||||
<div>ˇ</div>
|
||||
"#
|
||||
);
|
||||
|
||||
check!(
|
||||
test_multi_cursor_autoclose_different_tags,
|
||||
r#"
|
||||
<divˇ
|
||||
<spanˇ
|
||||
"#
|
||||
+ ">" =>
|
||||
r#"
|
||||
<div>ˇ</div>
|
||||
<span>ˇ</span>
|
||||
"#
|
||||
);
|
||||
|
||||
check!(
|
||||
test_multi_cursor_autoclose_some_dont_autoclose_others,
|
||||
r#"
|
||||
<divˇ
|
||||
<div /ˇ
|
||||
<spanˇ</span>
|
||||
<!DOCTYPE htmlˇ
|
||||
</headˇ
|
||||
<Component<T>ˇ
|
||||
ˇ
|
||||
"#
|
||||
+ ">" =>
|
||||
r#"
|
||||
<div>ˇ</div>
|
||||
<div />ˇ
|
||||
<span>ˇ</span>
|
||||
<!DOCTYPE html>ˇ
|
||||
</head>ˇ
|
||||
<Component<T>>ˇ</Component>
|
||||
>ˇ
|
||||
"#
|
||||
);
|
||||
|
||||
check!(
|
||||
test_doesnt_mess_up_trailing_text,
|
||||
"<divˇfoobar" + ">" => "<div>ˇ</div>foobar"
|
||||
);
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_multibuffer(cx: &mut TestAppContext) {
|
||||
init_test(cx, |settings| {
|
||||
settings.defaults.jsx_tag_auto_close = Some(JsxTagAutoCloseSettings { enabled: true });
|
||||
});
|
||||
|
||||
let buffer_a = cx.new(|cx| {
|
||||
let mut buf = language::Buffer::local("<div", cx);
|
||||
buf.set_language(
|
||||
Some(language("tsx", tree_sitter_typescript::LANGUAGE_TSX.into())),
|
||||
cx,
|
||||
);
|
||||
buf
|
||||
});
|
||||
let buffer_b = cx.new(|cx| {
|
||||
let mut buf = language::Buffer::local("<pre", cx);
|
||||
buf.set_language(
|
||||
Some(language("tsx", tree_sitter_typescript::LANGUAGE_TSX.into())),
|
||||
cx,
|
||||
);
|
||||
buf
|
||||
});
|
||||
let buffer_c = cx.new(|cx| {
|
||||
let buf = language::Buffer::local("<span", cx);
|
||||
buf
|
||||
});
|
||||
let buffer = cx.new(|cx| {
|
||||
let mut buf = MultiBuffer::new(language::Capability::ReadWrite);
|
||||
buf.push_excerpts(
|
||||
buffer_a,
|
||||
[ExcerptRange {
|
||||
context: text::Anchor::MIN..text::Anchor::MAX,
|
||||
primary: None,
|
||||
}],
|
||||
cx,
|
||||
);
|
||||
buf.push_excerpts(
|
||||
buffer_b,
|
||||
[ExcerptRange {
|
||||
context: text::Anchor::MIN..text::Anchor::MAX,
|
||||
primary: None,
|
||||
}],
|
||||
cx,
|
||||
);
|
||||
buf.push_excerpts(
|
||||
buffer_c,
|
||||
[ExcerptRange {
|
||||
context: text::Anchor::MIN..text::Anchor::MAX,
|
||||
primary: None,
|
||||
}],
|
||||
cx,
|
||||
);
|
||||
buf
|
||||
});
|
||||
let editor = cx.add_window(|window, cx| build_editor(buffer.clone(), window, cx));
|
||||
|
||||
let mut cx = EditorTestContext::for_editor(editor, cx).await;
|
||||
|
||||
cx.update_editor(|editor, window, cx| {
|
||||
editor.change_selections(None, window, cx, |selections| {
|
||||
selections.select(vec![
|
||||
Selection::from_offset(4),
|
||||
Selection::from_offset(9),
|
||||
Selection::from_offset(15),
|
||||
])
|
||||
})
|
||||
});
|
||||
cx.run_until_parked();
|
||||
|
||||
cx.update_editor(|editor, window, cx| {
|
||||
editor.handle_input(">", window, cx);
|
||||
});
|
||||
cx.run_until_parked();
|
||||
|
||||
cx.assert_editor_state("<div>ˇ</div>\n<pre>ˇ</pre>\n<span>ˇ");
|
||||
}
|
||||
}
|
||||
|
||||
fn empty_range(row: usize, column: usize) -> Range<DisplayPoint> {
|
||||
let point = DisplayPoint::new(DisplayRow(row as u32), column as u32);
|
||||
point..point
|
||||
|
|
616
crates/editor/src/jsx_tag_auto_close.rs
Normal file
616
crates/editor/src/jsx_tag_auto_close.rs
Normal file
|
@ -0,0 +1,616 @@
|
|||
use anyhow::{anyhow, Context as _, Result};
|
||||
use collections::HashMap;
|
||||
use gpui::{Context, Entity, Window};
|
||||
use multi_buffer::{MultiBuffer, ToOffset};
|
||||
use std::ops::Range;
|
||||
use util::ResultExt as _;
|
||||
|
||||
use language::{BufferSnapshot, JsxTagAutoCloseConfig, Node};
|
||||
use text::{Anchor, OffsetRangeExt as _};
|
||||
|
||||
use crate::Editor;
|
||||
|
||||
pub struct JsxTagCompletionState {
|
||||
edit_index: usize,
|
||||
open_tag_range: Range<usize>,
|
||||
}
|
||||
|
||||
/// Index of the named child within an open or close tag
|
||||
/// that corresponds to the tag name
|
||||
/// Note that this is not configurable, i.e. we assume the first
|
||||
/// named child of a tag node is the tag name
|
||||
const TS_NODE_TAG_NAME_CHILD_INDEX: usize = 0;
|
||||
|
||||
/// Maximum number of parent elements to walk back when checking if an open tag
|
||||
/// is already closed.
|
||||
///
|
||||
/// See the comment in `generate_auto_close_edits` for more details
|
||||
const ALREADY_CLOSED_PARENT_ELEMENT_WALK_BACK_LIMIT: usize = 2;
|
||||
|
||||
pub(crate) fn should_auto_close(
|
||||
buffer: &BufferSnapshot,
|
||||
edited_ranges: &[Range<usize>],
|
||||
config: &JsxTagAutoCloseConfig,
|
||||
) -> Option<Vec<JsxTagCompletionState>> {
|
||||
let mut to_auto_edit = vec![];
|
||||
for (index, edited_range) in edited_ranges.iter().enumerate() {
|
||||
let text = buffer
|
||||
.text_for_range(edited_range.clone())
|
||||
.collect::<String>();
|
||||
if !text.ends_with(">") {
|
||||
continue;
|
||||
}
|
||||
let Some(layer) = buffer.smallest_syntax_layer_containing(edited_range.clone()) else {
|
||||
continue;
|
||||
};
|
||||
let Some(node) = layer
|
||||
.node()
|
||||
.named_descendant_for_byte_range(edited_range.start, edited_range.end)
|
||||
else {
|
||||
continue;
|
||||
};
|
||||
let mut jsx_open_tag_node = node;
|
||||
if node.grammar_name() != config.open_tag_node_name {
|
||||
if let Some(parent) = node.parent() {
|
||||
if parent.grammar_name() == config.open_tag_node_name {
|
||||
jsx_open_tag_node = parent;
|
||||
}
|
||||
}
|
||||
}
|
||||
if jsx_open_tag_node.grammar_name() != config.open_tag_node_name {
|
||||
continue;
|
||||
}
|
||||
|
||||
let first_two_chars: Option<[char; 2]> = {
|
||||
let mut chars = buffer
|
||||
.text_for_range(jsx_open_tag_node.byte_range())
|
||||
.flat_map(|chunk| chunk.chars());
|
||||
if let (Some(c1), Some(c2)) = (chars.next(), chars.next()) {
|
||||
Some([c1, c2])
|
||||
} else {
|
||||
None
|
||||
}
|
||||
};
|
||||
if let Some(chars) = first_two_chars {
|
||||
if chars[0] != '<' {
|
||||
continue;
|
||||
}
|
||||
if chars[1] == '!' || chars[1] == '/' {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
to_auto_edit.push(JsxTagCompletionState {
|
||||
edit_index: index,
|
||||
open_tag_range: jsx_open_tag_node.byte_range(),
|
||||
});
|
||||
}
|
||||
if to_auto_edit.is_empty() {
|
||||
return None;
|
||||
} else {
|
||||
return Some(to_auto_edit);
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn generate_auto_close_edits(
|
||||
buffer: &BufferSnapshot,
|
||||
ranges: &[Range<usize>],
|
||||
config: &JsxTagAutoCloseConfig,
|
||||
state: Vec<JsxTagCompletionState>,
|
||||
) -> Result<Vec<(Range<Anchor>, String)>> {
|
||||
let mut edits = Vec::with_capacity(state.len());
|
||||
for auto_edit in state {
|
||||
let edited_range = ranges[auto_edit.edit_index].clone();
|
||||
let Some(layer) = buffer.smallest_syntax_layer_containing(edited_range.clone()) else {
|
||||
continue;
|
||||
};
|
||||
let layer_root_node = layer.node();
|
||||
let Some(open_tag) = layer_root_node.descendant_for_byte_range(
|
||||
auto_edit.open_tag_range.start,
|
||||
auto_edit.open_tag_range.end,
|
||||
) else {
|
||||
continue;
|
||||
};
|
||||
assert!(open_tag.kind() == config.open_tag_node_name);
|
||||
let tag_name = open_tag
|
||||
.named_child(TS_NODE_TAG_NAME_CHILD_INDEX)
|
||||
.filter(|node| node.kind() == config.tag_name_node_name)
|
||||
.map_or("".to_string(), |node| {
|
||||
buffer.text_for_range(node.byte_range()).collect::<String>()
|
||||
});
|
||||
|
||||
/*
|
||||
* Naive check to see if the tag is already closed
|
||||
* Essentially all we do is count the number of open and close tags
|
||||
* with the same tag name as the open tag just entered by the user
|
||||
* The search is limited to some scope determined by
|
||||
* `ALREADY_CLOSED_PARENT_ELEMENT_WALK_BACK_LIMIT`
|
||||
*
|
||||
* The limit is preferable to walking up the tree until we find a non-tag node,
|
||||
* and then checking the entire tree, as this is unnecessarily expensive, and
|
||||
* risks false positives
|
||||
* eg. a `</div>` tag without a corresponding opening tag exists 25 lines away
|
||||
* and the user typed in `<div>`, intuitively we still want to auto-close it because
|
||||
* the other `</div>` tag is almost certainly not supposed to be the closing tag for the
|
||||
* current element
|
||||
*
|
||||
* We have to walk up the tree some amount because tree-sitters error correction is not
|
||||
* designed to handle this case, and usually does not represent the tree structure
|
||||
* in the way we might expect,
|
||||
*
|
||||
* We half to walk up the tree until we hit an element with a different open tag name (`doing_deep_search == true`)
|
||||
* because tree-sitter may pair the new open tag with the root of the tree's closing tag leaving the
|
||||
* root's opening tag unclosed.
|
||||
* e.g
|
||||
* ```
|
||||
* <div>
|
||||
* <div>|cursor here|
|
||||
* </div>
|
||||
* ```
|
||||
* in Astro/vue/svelte tree-sitter represented the tree as
|
||||
* (
|
||||
* (jsx_element
|
||||
* (jsx_opening_element
|
||||
* "<div>")
|
||||
* )
|
||||
* (jsx_element
|
||||
* (jsx_opening_element
|
||||
* "<div>") // <- cursor is here
|
||||
* (jsx_closing_element
|
||||
* "</div>")
|
||||
* )
|
||||
* )
|
||||
* so if we only walked to the first `jsx_element` node,
|
||||
* we would mistakenly identify the div entered by the
|
||||
* user as already being closed, despite this clearly
|
||||
* being false
|
||||
*
|
||||
* The errors with the tree-sitter tree caused by error correction,
|
||||
* are also why the naive algorithm was chosen, as the alternative
|
||||
* approach would be to maintain or construct a full parse tree (like tree-sitter)
|
||||
* that better represents errors in a way that we can simply check
|
||||
* the enclosing scope of the entered tag for a closing tag
|
||||
* This is far more complex and expensive, and was deemed impractical
|
||||
* given that the naive algorithm is sufficient in the majority of cases.
|
||||
*/
|
||||
{
|
||||
let tag_node_name_equals = |node: &Node, tag_name_node_name: &str, name: &str| {
|
||||
let is_empty = name.len() == 0;
|
||||
if let Some(node_name) = node.named_child(TS_NODE_TAG_NAME_CHILD_INDEX) {
|
||||
if node_name.kind() != tag_name_node_name {
|
||||
return is_empty;
|
||||
}
|
||||
let range = node_name.byte_range();
|
||||
return buffer.text_for_range(range).equals_str(name);
|
||||
}
|
||||
return is_empty;
|
||||
};
|
||||
|
||||
let tree_root_node = {
|
||||
let mut ancestors = Vec::with_capacity(
|
||||
// estimate of max, not based on any data,
|
||||
// but trying to avoid excessive reallocation
|
||||
16,
|
||||
);
|
||||
ancestors.push(layer_root_node);
|
||||
let mut cur = layer_root_node;
|
||||
// walk down the tree until we hit the open tag
|
||||
// note: this is what node.parent() does internally
|
||||
while let Some(descendant) = cur.child_with_descendant(open_tag) {
|
||||
if descendant == open_tag {
|
||||
break;
|
||||
}
|
||||
ancestors.push(descendant);
|
||||
cur = descendant;
|
||||
}
|
||||
|
||||
assert!(ancestors.len() > 0);
|
||||
|
||||
let mut tree_root_node = open_tag;
|
||||
|
||||
let mut parent_element_node_count = 0;
|
||||
let mut doing_deep_search = false;
|
||||
|
||||
for &ancestor in ancestors.iter().rev() {
|
||||
tree_root_node = ancestor;
|
||||
let is_element = ancestor.kind() == config.jsx_element_node_name;
|
||||
let is_error = ancestor.is_error();
|
||||
if is_error || !is_element {
|
||||
break;
|
||||
}
|
||||
if is_element {
|
||||
let is_first = parent_element_node_count == 0;
|
||||
if !is_first {
|
||||
let has_open_tag_with_same_tag_name = ancestor
|
||||
.named_child(0)
|
||||
.filter(|n| n.kind() == config.open_tag_node_name)
|
||||
.map_or(false, |element_open_tag_node| {
|
||||
tag_node_name_equals(
|
||||
&element_open_tag_node,
|
||||
&config.tag_name_node_name,
|
||||
&tag_name,
|
||||
)
|
||||
});
|
||||
if has_open_tag_with_same_tag_name {
|
||||
doing_deep_search = true;
|
||||
} else if doing_deep_search {
|
||||
break;
|
||||
}
|
||||
}
|
||||
parent_element_node_count += 1;
|
||||
if !doing_deep_search
|
||||
&& parent_element_node_count
|
||||
>= ALREADY_CLOSED_PARENT_ELEMENT_WALK_BACK_LIMIT
|
||||
{
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
tree_root_node
|
||||
};
|
||||
|
||||
let mut unclosed_open_tag_count: i32 = 0;
|
||||
|
||||
let mut cursor = layer_root_node.walk();
|
||||
|
||||
let mut stack = Vec::with_capacity(tree_root_node.descendant_count());
|
||||
stack.extend(tree_root_node.children(&mut cursor));
|
||||
|
||||
let mut has_erroneous_close_tag = false;
|
||||
let mut erroneous_close_tag_node_name = "";
|
||||
let mut erroneous_close_tag_name_node_name = "";
|
||||
if let Some(name) = config.erroneous_close_tag_node_name.as_deref() {
|
||||
has_erroneous_close_tag = true;
|
||||
erroneous_close_tag_node_name = name;
|
||||
erroneous_close_tag_name_node_name = config
|
||||
.erroneous_close_tag_name_node_name
|
||||
.as_deref()
|
||||
.unwrap_or(&config.tag_name_node_name);
|
||||
}
|
||||
|
||||
let is_after_open_tag = |node: &Node| {
|
||||
return node.start_byte() < open_tag.start_byte()
|
||||
&& node.end_byte() < open_tag.start_byte();
|
||||
};
|
||||
|
||||
// perf: use cursor for more efficient traversal
|
||||
// if child -> go to child
|
||||
// else if next sibling -> go to next sibling
|
||||
// else -> go to parent
|
||||
// if parent == tree_root_node -> break
|
||||
while let Some(node) = stack.pop() {
|
||||
let kind = node.kind();
|
||||
if kind == config.open_tag_node_name {
|
||||
if tag_node_name_equals(&node, &config.tag_name_node_name, &tag_name) {
|
||||
unclosed_open_tag_count += 1;
|
||||
}
|
||||
} else if kind == config.close_tag_node_name {
|
||||
if tag_node_name_equals(&node, &config.tag_name_node_name, &tag_name) {
|
||||
unclosed_open_tag_count -= 1;
|
||||
}
|
||||
} else if has_erroneous_close_tag && kind == erroneous_close_tag_node_name {
|
||||
if tag_node_name_equals(&node, erroneous_close_tag_name_node_name, &tag_name) {
|
||||
if !is_after_open_tag(&node) {
|
||||
unclosed_open_tag_count -= 1;
|
||||
}
|
||||
}
|
||||
} else if kind == config.jsx_element_node_name {
|
||||
// perf: filter only open,close,element,erroneous nodes
|
||||
stack.extend(node.children(&mut cursor));
|
||||
}
|
||||
}
|
||||
|
||||
if unclosed_open_tag_count <= 0 {
|
||||
// skip if already closed
|
||||
continue;
|
||||
}
|
||||
}
|
||||
let edit_anchor = buffer.anchor_after(edited_range.end);
|
||||
let edit_range = edit_anchor..edit_anchor;
|
||||
edits.push((edit_range, format!("</{}>", tag_name)));
|
||||
}
|
||||
return Ok(edits);
|
||||
}
|
||||
|
||||
pub(crate) fn refresh_enabled_in_any_buffer(
|
||||
editor: &mut Editor,
|
||||
multi_buffer: &Entity<MultiBuffer>,
|
||||
cx: &Context<Editor>,
|
||||
) {
|
||||
editor.jsx_tag_auto_close_enabled_in_any_buffer = {
|
||||
let multi_buffer = multi_buffer.read(cx);
|
||||
let mut found_enabled = false;
|
||||
multi_buffer.for_each_buffer(|buffer| {
|
||||
let buffer = buffer.read(cx);
|
||||
let snapshot = buffer.snapshot();
|
||||
for syntax_layer in snapshot.syntax_layers() {
|
||||
let language = syntax_layer.language;
|
||||
if language.config().jsx_tag_auto_close.is_none() {
|
||||
continue;
|
||||
}
|
||||
let language_settings = language::language_settings::language_settings(
|
||||
Some(language.name()),
|
||||
snapshot.file(),
|
||||
cx,
|
||||
);
|
||||
if language_settings.jsx_tag_auto_close.enabled {
|
||||
found_enabled = true;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
found_enabled
|
||||
};
|
||||
}
|
||||
|
||||
pub(crate) type InitialBufferVersionsMap = HashMap<language::BufferId, clock::Global>;
|
||||
|
||||
pub(crate) fn construct_initial_buffer_versions_map<
|
||||
D: ToOffset + Copy,
|
||||
_S: Into<std::sync::Arc<str>>,
|
||||
>(
|
||||
editor: &Editor,
|
||||
edits: &[(Range<D>, _S)],
|
||||
cx: &Context<Editor>,
|
||||
) -> InitialBufferVersionsMap {
|
||||
let mut initial_buffer_versions = InitialBufferVersionsMap::default();
|
||||
|
||||
if !editor.jsx_tag_auto_close_enabled_in_any_buffer {
|
||||
return initial_buffer_versions;
|
||||
}
|
||||
|
||||
for (edit_range, _) in edits {
|
||||
let edit_range_buffer = editor
|
||||
.buffer()
|
||||
.read(cx)
|
||||
.excerpt_containing(edit_range.end, cx)
|
||||
.map(|e| e.1);
|
||||
if let Some(buffer) = edit_range_buffer {
|
||||
let (buffer_id, buffer_version) =
|
||||
buffer.read_with(cx, |buffer, _| (buffer.remote_id(), buffer.version.clone()));
|
||||
initial_buffer_versions.insert(buffer_id, buffer_version);
|
||||
}
|
||||
}
|
||||
return initial_buffer_versions;
|
||||
}
|
||||
|
||||
pub(crate) fn handle_from(
|
||||
editor: &Editor,
|
||||
initial_buffer_versions: InitialBufferVersionsMap,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Editor>,
|
||||
) {
|
||||
if !editor.jsx_tag_auto_close_enabled_in_any_buffer {
|
||||
return;
|
||||
}
|
||||
|
||||
struct JsxAutoCloseEditContext {
|
||||
buffer: Entity<language::Buffer>,
|
||||
config: language::JsxTagAutoCloseConfig,
|
||||
edits: Vec<Range<usize>>,
|
||||
}
|
||||
|
||||
let mut edit_contexts =
|
||||
HashMap::<(language::BufferId, language::LanguageId), JsxAutoCloseEditContext>::default();
|
||||
|
||||
for (buffer_id, buffer_version_initial) in initial_buffer_versions {
|
||||
let Some(buffer) = editor.buffer.read(cx).buffer(buffer_id) else {
|
||||
continue;
|
||||
};
|
||||
let snapshot = buffer.read(cx).snapshot();
|
||||
for edit in buffer.read(cx).edits_since(&buffer_version_initial) {
|
||||
let Some(language) = snapshot.language_at(edit.new.end) else {
|
||||
continue;
|
||||
};
|
||||
|
||||
let Some(config) = language.config().jsx_tag_auto_close.as_ref() else {
|
||||
continue;
|
||||
};
|
||||
|
||||
let language_settings = snapshot.settings_at(edit.new.end, cx);
|
||||
if !language_settings.jsx_tag_auto_close.enabled {
|
||||
continue;
|
||||
}
|
||||
|
||||
edit_contexts
|
||||
.entry((snapshot.remote_id(), language.id()))
|
||||
.or_insert_with(|| JsxAutoCloseEditContext {
|
||||
buffer: buffer.clone(),
|
||||
config: config.clone(),
|
||||
edits: vec![],
|
||||
})
|
||||
.edits
|
||||
.push(edit.new);
|
||||
}
|
||||
}
|
||||
|
||||
for ((buffer_id, _), auto_close_context) in edit_contexts {
|
||||
let JsxAutoCloseEditContext {
|
||||
buffer,
|
||||
config: jsx_tag_auto_close_config,
|
||||
edits: edited_ranges,
|
||||
} = auto_close_context;
|
||||
|
||||
let (buffer_version_initial, mut buffer_parse_status_rx) =
|
||||
buffer.read_with(cx, |buffer, _| (buffer.version(), buffer.parse_status()));
|
||||
|
||||
cx.spawn_in(window, |this, mut cx| async move {
|
||||
let Some(buffer_parse_status) = buffer_parse_status_rx.recv().await.ok() else {
|
||||
return Some(());
|
||||
};
|
||||
if buffer_parse_status == language::ParseStatus::Parsing {
|
||||
let Some(language::ParseStatus::Idle) = buffer_parse_status_rx.recv().await.ok()
|
||||
else {
|
||||
return Some(());
|
||||
};
|
||||
}
|
||||
|
||||
let buffer_snapshot = buffer.read_with(&cx, |buf, _| buf.snapshot()).ok()?;
|
||||
|
||||
let Some(edit_behavior_state) =
|
||||
should_auto_close(&buffer_snapshot, &edited_ranges, &jsx_tag_auto_close_config)
|
||||
else {
|
||||
return Some(());
|
||||
};
|
||||
|
||||
let ensure_no_edits_since_start = || -> Option<()> {
|
||||
// <div>wef,wefwef
|
||||
let has_edits_since_start = this
|
||||
.read_with(&cx, |this, cx| {
|
||||
this.buffer.read_with(cx, |buffer, cx| {
|
||||
buffer.buffer(buffer_id).map_or(true, |buffer| {
|
||||
buffer.read_with(cx, |buffer, _| {
|
||||
buffer.has_edits_since(&buffer_version_initial)
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
.ok()?;
|
||||
|
||||
if has_edits_since_start {
|
||||
Err(anyhow!(
|
||||
"Auto-close Operation Failed - Buffer has edits since start"
|
||||
))
|
||||
.log_err()?;
|
||||
}
|
||||
|
||||
Some(())
|
||||
};
|
||||
|
||||
ensure_no_edits_since_start()?;
|
||||
|
||||
let edits = cx
|
||||
.background_executor()
|
||||
.spawn({
|
||||
let buffer_snapshot = buffer_snapshot.clone();
|
||||
async move {
|
||||
generate_auto_close_edits(
|
||||
&buffer_snapshot,
|
||||
&edited_ranges,
|
||||
&jsx_tag_auto_close_config,
|
||||
edit_behavior_state,
|
||||
)
|
||||
}
|
||||
})
|
||||
.await;
|
||||
|
||||
let edits = edits
|
||||
.context("Auto-close Operation Failed - Failed to compute edits")
|
||||
.log_err()?;
|
||||
|
||||
if edits.is_empty() {
|
||||
return Some(());
|
||||
}
|
||||
|
||||
// check again after awaiting background task before applying edits
|
||||
ensure_no_edits_since_start()?;
|
||||
|
||||
let multi_buffer_snapshot = this
|
||||
.read_with(&cx, |this, cx| {
|
||||
this.buffer.read_with(cx, |buffer, cx| buffer.snapshot(cx))
|
||||
})
|
||||
.ok()?;
|
||||
|
||||
let mut base_selections = Vec::new();
|
||||
let mut buffer_selection_map = HashMap::default();
|
||||
|
||||
{
|
||||
let selections = this
|
||||
.read_with(&cx, |this, _| this.selections.disjoint_anchors().clone())
|
||||
.ok()?;
|
||||
for selection in selections.iter() {
|
||||
let Some(selection_buffer_offset_head) =
|
||||
multi_buffer_snapshot.point_to_buffer_offset(selection.head())
|
||||
else {
|
||||
base_selections.push(selection.clone());
|
||||
continue;
|
||||
};
|
||||
let Some(selection_buffer_offset_tail) =
|
||||
multi_buffer_snapshot.point_to_buffer_offset(selection.tail())
|
||||
else {
|
||||
base_selections.push(selection.clone());
|
||||
continue;
|
||||
};
|
||||
|
||||
let is_entirely_in_buffer = selection_buffer_offset_head.0.remote_id()
|
||||
== buffer_id
|
||||
&& selection_buffer_offset_tail.0.remote_id() == buffer_id;
|
||||
if !is_entirely_in_buffer {
|
||||
base_selections.push(selection.clone());
|
||||
continue;
|
||||
}
|
||||
|
||||
let selection_buffer_offset_head = selection_buffer_offset_head.1;
|
||||
let selection_buffer_offset_tail = selection_buffer_offset_tail.1;
|
||||
buffer_selection_map.insert(
|
||||
(selection_buffer_offset_head, selection_buffer_offset_tail),
|
||||
(selection.clone(), None),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
let mut any_selections_need_update = false;
|
||||
for edit in &edits {
|
||||
let edit_range_offset = edit.0.to_offset(&buffer_snapshot);
|
||||
if edit_range_offset.start != edit_range_offset.end {
|
||||
continue;
|
||||
}
|
||||
if let Some(selection) =
|
||||
buffer_selection_map.get_mut(&(edit_range_offset.start, edit_range_offset.end))
|
||||
{
|
||||
if selection.0.head().bias() != text::Bias::Right
|
||||
|| selection.0.tail().bias() != text::Bias::Right
|
||||
{
|
||||
continue;
|
||||
}
|
||||
if selection.1.is_none() {
|
||||
any_selections_need_update = true;
|
||||
selection.1 = Some(
|
||||
selection
|
||||
.0
|
||||
.clone()
|
||||
.map(|anchor| multi_buffer_snapshot.anchor_before(anchor)),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
buffer
|
||||
.update(&mut cx, |buffer, cx| {
|
||||
buffer.edit(edits, None, cx);
|
||||
})
|
||||
.ok()?;
|
||||
|
||||
if any_selections_need_update {
|
||||
let multi_buffer_snapshot = this
|
||||
.read_with(&cx, |this, cx| {
|
||||
this.buffer.read_with(cx, |buffer, cx| buffer.snapshot(cx))
|
||||
})
|
||||
.ok()?;
|
||||
|
||||
base_selections.extend(buffer_selection_map.values().map(|selection| {
|
||||
match &selection.1 {
|
||||
Some(left_biased_selection) => left_biased_selection.clone(),
|
||||
None => selection.0.clone(),
|
||||
}
|
||||
}));
|
||||
|
||||
let base_selections = base_selections
|
||||
.into_iter()
|
||||
.map(|selection| {
|
||||
selection.map(|anchor| anchor.to_offset(&multi_buffer_snapshot))
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
this.update_in(&mut cx, |this, window, cx| {
|
||||
this.change_selections_inner(None, false, window, cx, |s| {
|
||||
s.select(base_selections);
|
||||
});
|
||||
})
|
||||
.ok()?;
|
||||
}
|
||||
|
||||
Some(())
|
||||
})
|
||||
.detach();
|
||||
}
|
||||
}
|
|
@ -3080,6 +3080,25 @@ impl BufferSnapshot {
|
|||
.last()
|
||||
}
|
||||
|
||||
pub fn smallest_syntax_layer_containing<D: ToOffset>(
|
||||
&self,
|
||||
range: Range<D>,
|
||||
) -> Option<SyntaxLayer> {
|
||||
let range = range.to_offset(self);
|
||||
return self
|
||||
.syntax
|
||||
.layers_for_range(range, &self.text, false)
|
||||
.max_by(|a, b| {
|
||||
if a.depth != b.depth {
|
||||
a.depth.cmp(&b.depth)
|
||||
} else if a.offset.0 != b.offset.0 {
|
||||
a.offset.0.cmp(&b.offset.0)
|
||||
} else {
|
||||
a.node().end_byte().cmp(&b.node().end_byte()).reverse()
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/// Returns the main [`Language`].
|
||||
pub fn language(&self) -> Option<&Arc<Language>> {
|
||||
self.language.as_ref()
|
||||
|
|
|
@ -680,6 +680,9 @@ pub struct LanguageConfig {
|
|||
/// languages, but should not appear to the user as a distinct language.
|
||||
#[serde(default)]
|
||||
pub hidden: bool,
|
||||
/// If configured, this language contains JSX style tags, and should support auto-closing of those tags.
|
||||
#[serde(default)]
|
||||
pub jsx_tag_auto_close: Option<JsxTagAutoCloseConfig>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, Default, JsonSchema)]
|
||||
|
@ -697,6 +700,34 @@ pub struct LanguageMatcher {
|
|||
pub first_line_pattern: Option<Regex>,
|
||||
}
|
||||
|
||||
/// The configuration for JSX tag auto-closing.
|
||||
#[derive(Clone, Deserialize, JsonSchema)]
|
||||
pub struct JsxTagAutoCloseConfig {
|
||||
/// The name of the node for a opening tag
|
||||
pub open_tag_node_name: String,
|
||||
/// The name of the node for an closing tag
|
||||
pub close_tag_node_name: String,
|
||||
/// The name of the node for a complete element with children for open and close tags
|
||||
pub jsx_element_node_name: String,
|
||||
/// The name of the node found within both opening and closing
|
||||
/// tags that describes the tag name
|
||||
pub tag_name_node_name: String,
|
||||
/// Some grammars are smart enough to detect a closing tag
|
||||
/// that is not valid i.e. doesn't match it's corresponding
|
||||
/// opening tag or does not have a corresponding opening tag
|
||||
/// This should be set to the name of the node for invalid
|
||||
/// closing tags if the grammar contains such a node, otherwise
|
||||
/// detecting already closed tags will not work properly
|
||||
#[serde(default)]
|
||||
pub erroneous_close_tag_node_name: Option<String>,
|
||||
/// See above for erroneous_close_tag_node_name for details
|
||||
/// This should be set if the node used for the tag name
|
||||
/// within erroneous closing tags is different from the
|
||||
/// normal tag name node name
|
||||
#[serde(default)]
|
||||
pub erroneous_close_tag_name_node_name: Option<String>,
|
||||
}
|
||||
|
||||
/// Represents a language for the given range. Some languages (e.g. HTML)
|
||||
/// interleave several languages together, thus a single buffer might actually contain
|
||||
/// several nested scopes.
|
||||
|
@ -767,6 +798,7 @@ impl Default for LanguageConfig {
|
|||
soft_wrap: None,
|
||||
prettier_parser_name: None,
|
||||
hidden: false,
|
||||
jsx_tag_auto_close: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -888,7 +920,7 @@ pub struct BracketPair {
|
|||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy)]
|
||||
pub(crate) struct LanguageId(usize);
|
||||
pub struct LanguageId(usize);
|
||||
|
||||
impl LanguageId {
|
||||
pub(crate) fn new() -> Self {
|
||||
|
@ -1056,6 +1088,10 @@ impl Language {
|
|||
Self::new_with_id(LanguageId::new(), config, ts_language)
|
||||
}
|
||||
|
||||
pub fn id(&self) -> LanguageId {
|
||||
self.id
|
||||
}
|
||||
|
||||
fn new_with_id(
|
||||
id: LanguageId,
|
||||
config: LanguageConfig,
|
||||
|
|
|
@ -100,6 +100,8 @@ pub struct LanguageSettings {
|
|||
pub formatter: SelectedFormatter,
|
||||
/// Zed's Prettier integration settings.
|
||||
pub prettier: PrettierSettings,
|
||||
/// Whether to automatically close JSX tags.
|
||||
pub jsx_tag_auto_close: JsxTagAutoCloseSettings,
|
||||
/// Whether to use language servers to provide code intelligence.
|
||||
pub enable_language_server: bool,
|
||||
/// The list of language servers to use (or disable) for this language.
|
||||
|
@ -374,6 +376,9 @@ pub struct LanguageSettingsContent {
|
|||
/// Default: off
|
||||
#[serde(default)]
|
||||
pub prettier: Option<PrettierSettings>,
|
||||
/// Whether to automatically close JSX tags.
|
||||
#[serde(default)]
|
||||
pub jsx_tag_auto_close: Option<JsxTagAutoCloseSettings>,
|
||||
/// Whether to use language servers to provide code intelligence.
|
||||
///
|
||||
/// Default: true
|
||||
|
@ -1335,6 +1340,10 @@ fn merge_settings(settings: &mut LanguageSettings, src: &LanguageSettingsContent
|
|||
);
|
||||
merge(&mut settings.formatter, src.formatter.clone());
|
||||
merge(&mut settings.prettier, src.prettier.clone());
|
||||
merge(
|
||||
&mut settings.jsx_tag_auto_close,
|
||||
src.jsx_tag_auto_close.clone(),
|
||||
);
|
||||
merge(&mut settings.format_on_save, src.format_on_save.clone());
|
||||
merge(
|
||||
&mut settings.remove_trailing_whitespace_on_save,
|
||||
|
@ -1398,6 +1407,13 @@ pub struct PrettierSettings {
|
|||
pub options: HashMap<String, serde_json::Value>,
|
||||
}
|
||||
|
||||
#[derive(Default, Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
|
||||
pub struct JsxTagAutoCloseSettings {
|
||||
/// Enables or disables auto-closing of JSX tags.
|
||||
#[serde(default)]
|
||||
pub enabled: bool,
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use gpui::TestAppContext;
|
||||
|
|
|
@ -121,9 +121,9 @@ impl SyntaxLayerContent {
|
|||
pub struct SyntaxLayer<'a> {
|
||||
/// The language for this layer.
|
||||
pub language: &'a Arc<Language>,
|
||||
depth: usize,
|
||||
pub(crate) depth: usize,
|
||||
tree: &'a Tree,
|
||||
offset: (usize, tree_sitter::Point),
|
||||
pub(crate) offset: (usize, tree_sitter::Point),
|
||||
}
|
||||
|
||||
/// A layer of syntax highlighting. Like [SyntaxLayer], but holding
|
||||
|
@ -133,7 +133,7 @@ pub struct OwnedSyntaxLayer {
|
|||
/// The language for this layer.
|
||||
pub language: Arc<Language>,
|
||||
tree: tree_sitter::Tree,
|
||||
offset: (usize, tree_sitter::Point),
|
||||
pub offset: (usize, tree_sitter::Point),
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
|
|
|
@ -20,6 +20,12 @@ tab_size = 2
|
|||
scope_opt_in_language_servers = ["tailwindcss-language-server", "emmet-language-server"]
|
||||
prettier_parser_name = "babel"
|
||||
|
||||
[jsx_tag_auto_close]
|
||||
open_tag_node_name = "jsx_opening_element"
|
||||
close_tag_node_name = "jsx_closing_element"
|
||||
jsx_element_node_name = "jsx_element"
|
||||
tag_name_node_name = "identifier"
|
||||
|
||||
[overrides.element]
|
||||
line_comments = { remove = true }
|
||||
block_comment = ["{/* ", " */}"]
|
||||
|
|
|
@ -11,7 +11,7 @@ use std::{str, sync::Arc};
|
|||
use typescript::typescript_task_context;
|
||||
use util::{asset_str, ResultExt};
|
||||
|
||||
use crate::{bash::bash_task_context, go::GoContextProvider, rust::RustContextProvider};
|
||||
use crate::{bash::bash_task_context, rust::RustContextProvider};
|
||||
|
||||
mod bash;
|
||||
mod c;
|
||||
|
@ -74,177 +74,191 @@ pub fn init(languages: Arc<LanguageRegistry>, node_runtime: NodeRuntime, cx: &mu
|
|||
("gitcommit", tree_sitter_gitcommit::LANGUAGE),
|
||||
]);
|
||||
|
||||
macro_rules! language {
|
||||
($name:literal) => {
|
||||
let config = load_config($name);
|
||||
languages.register_language(
|
||||
config.name.clone(),
|
||||
config.grammar.clone(),
|
||||
config.matcher.clone(),
|
||||
config.hidden,
|
||||
Arc::new(move || {
|
||||
Ok(LoadedLanguage {
|
||||
config: config.clone(),
|
||||
queries: load_queries($name),
|
||||
context_provider: None,
|
||||
toolchain_provider: None,
|
||||
})
|
||||
}),
|
||||
);
|
||||
// Following are a series of helper macros for registering languages.
|
||||
// Macros are used instead of a function or for loop in order to avoid
|
||||
// code duplication and improve readability as the types get quite verbose
|
||||
// to type out in some cases.
|
||||
// Additionally, the `provider` fields in LoadedLanguage
|
||||
// would have be `Copy` if we were to use a function or for-loop to register the languages
|
||||
// due to the fact that we pass an `Arc<Fn>` to `languages.register_language`
|
||||
// that loads and initializes the language lazily.
|
||||
// We avoid this entirely by using a Macro
|
||||
|
||||
macro_rules! context_provider {
|
||||
($name:expr) => {
|
||||
Some(Arc::new($name) as Arc<dyn ContextProvider>)
|
||||
};
|
||||
($name:literal, $adapters:expr) => {
|
||||
let config = load_config($name);
|
||||
// typeck helper
|
||||
let adapters: Vec<Arc<dyn LspAdapter>> = $adapters;
|
||||
for adapter in adapters {
|
||||
languages.register_lsp_adapter(config.name.clone(), adapter);
|
||||
}
|
||||
languages.register_language(
|
||||
config.name.clone(),
|
||||
config.grammar.clone(),
|
||||
config.matcher.clone(),
|
||||
config.hidden,
|
||||
Arc::new(move || {
|
||||
Ok(LoadedLanguage {
|
||||
config: config.clone(),
|
||||
queries: load_queries($name),
|
||||
context_provider: None,
|
||||
toolchain_provider: None,
|
||||
})
|
||||
}),
|
||||
);
|
||||
};
|
||||
($name:literal, $adapters:expr, $context_provider:expr) => {
|
||||
let config = load_config($name);
|
||||
// typeck helper
|
||||
let adapters: Vec<Arc<dyn LspAdapter>> = $adapters;
|
||||
for adapter in adapters {
|
||||
languages.register_lsp_adapter(config.name.clone(), adapter);
|
||||
}
|
||||
languages.register_language(
|
||||
config.name.clone(),
|
||||
config.grammar.clone(),
|
||||
config.matcher.clone(),
|
||||
config.hidden,
|
||||
Arc::new(move || {
|
||||
Ok(LoadedLanguage {
|
||||
config: config.clone(),
|
||||
queries: load_queries($name),
|
||||
context_provider: Some(Arc::new($context_provider)),
|
||||
toolchain_provider: None,
|
||||
})
|
||||
}),
|
||||
);
|
||||
};
|
||||
($name:literal, $adapters:expr, $context_provider:expr, $toolchain_provider:expr) => {
|
||||
let config = load_config($name);
|
||||
// typeck helper
|
||||
let adapters: Vec<Arc<dyn LspAdapter>> = $adapters;
|
||||
for adapter in adapters {
|
||||
languages.register_lsp_adapter(config.name.clone(), adapter);
|
||||
}
|
||||
languages.register_language(
|
||||
config.name.clone(),
|
||||
config.grammar.clone(),
|
||||
config.matcher.clone(),
|
||||
config.hidden,
|
||||
Arc::new(move || {
|
||||
Ok(LoadedLanguage {
|
||||
config: config.clone(),
|
||||
queries: load_queries($name),
|
||||
context_provider: Some(Arc::new($context_provider)),
|
||||
toolchain_provider: Some($toolchain_provider),
|
||||
})
|
||||
}),
|
||||
);
|
||||
() => {
|
||||
None
|
||||
};
|
||||
}
|
||||
language!("bash", Vec::new(), bash_task_context());
|
||||
language!("c", vec![Arc::new(c::CLspAdapter) as Arc<dyn LspAdapter>]);
|
||||
language!("cpp", vec![Arc::new(c::CLspAdapter)]);
|
||||
language!(
|
||||
"css",
|
||||
vec![Arc::new(css::CssLspAdapter::new(node_runtime.clone())),]
|
||||
);
|
||||
language!("diff");
|
||||
language!("go", vec![Arc::new(go::GoLspAdapter)], GoContextProvider);
|
||||
language!("gomod", vec![Arc::new(go::GoLspAdapter)], GoContextProvider);
|
||||
language!(
|
||||
"gowork",
|
||||
vec![Arc::new(go::GoLspAdapter)],
|
||||
GoContextProvider
|
||||
|
||||
macro_rules! toolchain_provider {
|
||||
($name:expr) => {
|
||||
Some(Arc::new($name) as Arc<dyn ToolchainLister>)
|
||||
};
|
||||
() => {
|
||||
None
|
||||
};
|
||||
}
|
||||
|
||||
macro_rules! adapters {
|
||||
($($item:expr),+ $(,)?) => {
|
||||
vec![
|
||||
$(Arc::new($item) as Arc<dyn LspAdapter>,)*
|
||||
]
|
||||
};
|
||||
() => {
|
||||
vec![]
|
||||
};
|
||||
}
|
||||
|
||||
macro_rules! register_language {
|
||||
($name:expr, adapters => $adapters:expr, context => $context:expr, toolchain => $toolchain:expr) => {
|
||||
let config = load_config($name);
|
||||
for adapter in $adapters {
|
||||
languages.register_lsp_adapter(config.name.clone(), adapter);
|
||||
}
|
||||
languages.register_language(
|
||||
config.name.clone(),
|
||||
config.grammar.clone(),
|
||||
config.matcher.clone(),
|
||||
config.hidden,
|
||||
Arc::new(move || {
|
||||
Ok(LoadedLanguage {
|
||||
config: config.clone(),
|
||||
queries: load_queries($name),
|
||||
context_provider: $context,
|
||||
toolchain_provider: $toolchain,
|
||||
})
|
||||
}),
|
||||
);
|
||||
};
|
||||
($name:expr) => {
|
||||
register_language!($name, adapters => adapters![], context => context_provider!(), toolchain => toolchain_provider!())
|
||||
};
|
||||
($name:expr, adapters => $adapters:expr, context => $context:expr, toolchain => $toolchain:expr) => {
|
||||
register_language!($name, adapters => $adapters, context => $context, toolchain => $toolchain)
|
||||
};
|
||||
($name:expr, adapters => $adapters:expr, context => $context:expr) => {
|
||||
register_language!($name, adapters => $adapters, context => $context, toolchain => toolchain_provider!())
|
||||
};
|
||||
($name:expr, adapters => $adapters:expr) => {
|
||||
register_language!($name, adapters => $adapters, context => context_provider!(), toolchain => toolchain_provider!())
|
||||
};
|
||||
}
|
||||
|
||||
register_language!(
|
||||
"bash",
|
||||
adapters => adapters![],
|
||||
context => context_provider!(bash_task_context()),
|
||||
toolchain => toolchain_provider!()
|
||||
);
|
||||
|
||||
language!(
|
||||
register_language!(
|
||||
"c",
|
||||
adapters => adapters![c::CLspAdapter]
|
||||
);
|
||||
register_language!(
|
||||
"cpp",
|
||||
adapters => adapters![c::CLspAdapter]
|
||||
);
|
||||
|
||||
register_language!(
|
||||
"css",
|
||||
adapters => adapters![css::CssLspAdapter::new(node_runtime.clone())]
|
||||
);
|
||||
|
||||
register_language!("diff");
|
||||
|
||||
register_language!(
|
||||
"go",
|
||||
adapters => adapters![go::GoLspAdapter],
|
||||
context => context_provider!(go::GoContextProvider)
|
||||
);
|
||||
register_language!(
|
||||
"gomod",
|
||||
adapters => adapters![go::GoLspAdapter],
|
||||
context => context_provider!(go::GoContextProvider)
|
||||
);
|
||||
register_language!(
|
||||
"gowork",
|
||||
adapters => adapters![go::GoLspAdapter],
|
||||
context => context_provider!(go::GoContextProvider)
|
||||
);
|
||||
|
||||
register_language!(
|
||||
"json",
|
||||
vec![
|
||||
Arc::new(json::JsonLspAdapter::new(
|
||||
node_runtime.clone(),
|
||||
languages.clone(),
|
||||
)),
|
||||
Arc::new(json::NodeVersionAdapter)
|
||||
adapters => adapters![
|
||||
json::JsonLspAdapter::new(node_runtime.clone(), languages.clone(),),
|
||||
json::NodeVersionAdapter,
|
||||
],
|
||||
json_task_context()
|
||||
context => context_provider!(json_task_context())
|
||||
);
|
||||
language!(
|
||||
register_language!(
|
||||
"jsonc",
|
||||
vec![Arc::new(json::JsonLspAdapter::new(
|
||||
node_runtime.clone(),
|
||||
languages.clone(),
|
||||
))],
|
||||
json_task_context()
|
||||
adapters => adapters![
|
||||
json::JsonLspAdapter::new(node_runtime.clone(), languages.clone(),),
|
||||
],
|
||||
context => context_provider!(json_task_context())
|
||||
);
|
||||
language!("markdown");
|
||||
language!("markdown-inline");
|
||||
language!(
|
||||
|
||||
register_language!("markdown");
|
||||
register_language!("markdown-inline");
|
||||
|
||||
register_language!(
|
||||
"python",
|
||||
vec![
|
||||
Arc::new(python::PythonLspAdapter::new(node_runtime.clone(),)),
|
||||
Arc::new(python::PyLspAdapter::new())
|
||||
adapters => adapters![
|
||||
python::PythonLspAdapter::new(node_runtime.clone()),
|
||||
python::PyLspAdapter::new()
|
||||
],
|
||||
PythonContextProvider,
|
||||
Arc::new(PythonToolchainProvider::default()) as Arc<dyn ToolchainLister>
|
||||
context => context_provider!(PythonContextProvider),
|
||||
toolchain => toolchain_provider!(PythonToolchainProvider::default())
|
||||
);
|
||||
language!(
|
||||
register_language!(
|
||||
"rust",
|
||||
vec![Arc::new(rust::RustLspAdapter)],
|
||||
RustContextProvider
|
||||
adapters => adapters![rust::RustLspAdapter],
|
||||
context => context_provider!(RustContextProvider)
|
||||
);
|
||||
language!(
|
||||
register_language!(
|
||||
"tsx",
|
||||
vec![
|
||||
Arc::new(typescript::TypeScriptLspAdapter::new(node_runtime.clone())),
|
||||
Arc::new(vtsls::VtslsLspAdapter::new(node_runtime.clone()))
|
||||
adapters => adapters![
|
||||
typescript::TypeScriptLspAdapter::new(node_runtime.clone()),
|
||||
vtsls::VtslsLspAdapter::new(node_runtime.clone()),
|
||||
],
|
||||
typescript_task_context()
|
||||
context => context_provider!(typescript_task_context()),
|
||||
toolchain => toolchain_provider!()
|
||||
);
|
||||
language!(
|
||||
register_language!(
|
||||
"typescript",
|
||||
vec![
|
||||
Arc::new(typescript::TypeScriptLspAdapter::new(node_runtime.clone())),
|
||||
Arc::new(vtsls::VtslsLspAdapter::new(node_runtime.clone()))
|
||||
adapters => adapters![
|
||||
typescript::TypeScriptLspAdapter::new(node_runtime.clone()),
|
||||
vtsls::VtslsLspAdapter::new(node_runtime.clone()),
|
||||
],
|
||||
typescript_task_context()
|
||||
context => context_provider!(typescript_task_context())
|
||||
);
|
||||
language!(
|
||||
register_language!(
|
||||
"javascript",
|
||||
vec![
|
||||
Arc::new(typescript::TypeScriptLspAdapter::new(node_runtime.clone())),
|
||||
Arc::new(vtsls::VtslsLspAdapter::new(node_runtime.clone()))
|
||||
adapters => adapters![
|
||||
typescript::TypeScriptLspAdapter::new(node_runtime.clone()),
|
||||
vtsls::VtslsLspAdapter::new(node_runtime.clone()),
|
||||
],
|
||||
typescript_task_context()
|
||||
context => context_provider!(typescript_task_context())
|
||||
);
|
||||
language!(
|
||||
register_language!(
|
||||
"jsdoc",
|
||||
vec![
|
||||
Arc::new(typescript::TypeScriptLspAdapter::new(node_runtime.clone(),)),
|
||||
Arc::new(vtsls::VtslsLspAdapter::new(node_runtime.clone()))
|
||||
adapters => adapters![
|
||||
typescript::TypeScriptLspAdapter::new(node_runtime.clone()),
|
||||
vtsls::VtslsLspAdapter::new(node_runtime.clone()),
|
||||
]
|
||||
);
|
||||
language!("regex");
|
||||
language!(
|
||||
"yaml",
|
||||
vec![Arc::new(yaml::YamlLspAdapter::new(node_runtime.clone()))]
|
||||
|
||||
register_language!("regex");
|
||||
|
||||
register_language!("yaml",
|
||||
adapters => adapters![
|
||||
yaml::YamlLspAdapter::new(node_runtime.clone()),
|
||||
]
|
||||
);
|
||||
|
||||
// Register globally available language servers.
|
||||
|
@ -366,6 +380,7 @@ fn load_config(name: &str) -> LanguageConfig {
|
|||
config = LanguageConfig {
|
||||
name: config.name,
|
||||
matcher: config.matcher,
|
||||
jsx_tag_auto_close: config.jsx_tag_auto_close,
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -18,6 +18,12 @@ scope_opt_in_language_servers = ["tailwindcss-language-server", "emmet-language-
|
|||
prettier_parser_name = "typescript"
|
||||
tab_size = 2
|
||||
|
||||
[jsx_tag_auto_close]
|
||||
open_tag_node_name = "jsx_opening_element"
|
||||
close_tag_node_name = "jsx_closing_element"
|
||||
jsx_element_node_name = "jsx_element"
|
||||
tag_name_node_name = "identifier"
|
||||
|
||||
[overrides.element]
|
||||
line_comments = { remove = true }
|
||||
block_comment = ["{/* ", " */}"]
|
||||
|
|
|
@ -1071,6 +1071,11 @@ impl MultiBuffer {
|
|||
self.history.start_transaction(now)
|
||||
}
|
||||
|
||||
pub fn last_transaction_id(&self) -> Option<TransactionId> {
|
||||
let last_transaction = self.history.undo_stack.last()?;
|
||||
return Some(last_transaction.id);
|
||||
}
|
||||
|
||||
pub fn end_transaction(&mut self, cx: &mut Context<Self>) -> Option<TransactionId> {
|
||||
self.end_transaction_at(Instant::now(), cx)
|
||||
}
|
||||
|
|
|
@ -591,6 +591,7 @@ impl<'a> Cursor<'a> {
|
|||
}
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct Chunks<'a> {
|
||||
chunks: sum_tree::Cursor<'a, Chunk, usize>,
|
||||
range: Range<usize>,
|
||||
|
@ -780,6 +781,40 @@ impl<'a> Chunks<'a> {
|
|||
reversed,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn equals_str(&self, other: &str) -> bool {
|
||||
let chunk = self.clone();
|
||||
if chunk.reversed {
|
||||
let mut offset = other.len();
|
||||
for chunk in chunk {
|
||||
if other[0..offset].ends_with(chunk) {
|
||||
offset -= chunk.len();
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
if offset != 0 {
|
||||
return false;
|
||||
}
|
||||
} else {
|
||||
let mut offset = 0;
|
||||
for chunk in chunk {
|
||||
if offset >= other.len() {
|
||||
return false;
|
||||
}
|
||||
if other[offset..].starts_with(chunk) {
|
||||
offset += chunk.len();
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
if offset != other.len() {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> Iterator for Chunks<'a> {
|
||||
|
@ -1855,6 +1890,53 @@ mod tests {
|
|||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_chunks_equals_str() {
|
||||
let text = "This is a multi-chunk\n& multi-line test string!";
|
||||
let rope = Rope::from(text);
|
||||
for start in 0..text.len() {
|
||||
for end in start..text.len() {
|
||||
let range = start..end;
|
||||
let correct_substring = &text[start..end];
|
||||
|
||||
// Test that correct range returns true
|
||||
assert!(rope
|
||||
.chunks_in_range(range.clone())
|
||||
.equals_str(correct_substring));
|
||||
assert!(rope
|
||||
.reversed_chunks_in_range(range.clone())
|
||||
.equals_str(correct_substring));
|
||||
|
||||
// Test that all other ranges return false (unless they happen to match)
|
||||
for other_start in 0..text.len() {
|
||||
for other_end in other_start..text.len() {
|
||||
if other_start == start && other_end == end {
|
||||
continue;
|
||||
}
|
||||
let other_substring = &text[other_start..other_end];
|
||||
|
||||
// Only assert false if the substrings are actually different
|
||||
if other_substring == correct_substring {
|
||||
continue;
|
||||
}
|
||||
assert!(!rope
|
||||
.chunks_in_range(range.clone())
|
||||
.equals_str(other_substring));
|
||||
assert!(!rope
|
||||
.reversed_chunks_in_range(range.clone())
|
||||
.equals_str(other_substring));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let rope = Rope::from("");
|
||||
assert!(rope.chunks_in_range(0..0).equals_str(""));
|
||||
assert!(rope.reversed_chunks_in_range(0..0).equals_str(""));
|
||||
assert!(!rope.chunks_in_range(0..0).equals_str("foo"));
|
||||
assert!(!rope.reversed_chunks_in_range(0..0).equals_str("foo"));
|
||||
}
|
||||
|
||||
fn clip_offset(text: &str, mut offset: usize, bias: Bias) -> usize {
|
||||
while !text.is_char_boundary(offset) {
|
||||
match bias {
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue