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",
|
"inline_completion",
|
||||||
"itertools 0.14.0",
|
"itertools 0.14.0",
|
||||||
"language",
|
"language",
|
||||||
|
"languages",
|
||||||
"linkify",
|
"linkify",
|
||||||
"log",
|
"log",
|
||||||
"lsp",
|
"lsp",
|
||||||
|
|
|
@ -1298,6 +1298,11 @@
|
||||||
// "semi": false,
|
// "semi": false,
|
||||||
// "singleQuote": true
|
// "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 Specific settings.
|
||||||
"lsp": {
|
"lsp": {
|
||||||
// Specify the LSP name as a key here.
|
// Specify the LSP name as a key here.
|
||||||
|
|
|
@ -94,6 +94,7 @@ ctor.workspace = true
|
||||||
env_logger.workspace = true
|
env_logger.workspace = true
|
||||||
gpui = { workspace = true, features = ["test-support"] }
|
gpui = { workspace = true, features = ["test-support"] }
|
||||||
language = { workspace = true, features = ["test-support"] }
|
language = { workspace = true, features = ["test-support"] }
|
||||||
|
languages = {workspace = true, features = ["test-support"] }
|
||||||
lsp = { workspace = true, features = ["test-support"] }
|
lsp = { workspace = true, features = ["test-support"] }
|
||||||
multi_buffer = { workspace = true, features = ["test-support"] }
|
multi_buffer = { workspace = true, features = ["test-support"] }
|
||||||
project = { workspace = true, features = ["test-support"] }
|
project = { workspace = true, features = ["test-support"] }
|
||||||
|
|
|
@ -28,6 +28,7 @@ mod hover_popover;
|
||||||
mod indent_guides;
|
mod indent_guides;
|
||||||
mod inlay_hint_cache;
|
mod inlay_hint_cache;
|
||||||
pub mod items;
|
pub mod items;
|
||||||
|
mod jsx_tag_auto_close;
|
||||||
mod linked_editing_ranges;
|
mod linked_editing_ranges;
|
||||||
mod lsp_ext;
|
mod lsp_ext;
|
||||||
mod mouse_context_menu;
|
mod mouse_context_menu;
|
||||||
|
@ -724,6 +725,7 @@ pub struct Editor {
|
||||||
use_autoclose: bool,
|
use_autoclose: bool,
|
||||||
use_auto_surround: bool,
|
use_auto_surround: bool,
|
||||||
auto_replace_emoji_shortcode: bool,
|
auto_replace_emoji_shortcode: bool,
|
||||||
|
jsx_tag_auto_close_enabled_in_any_buffer: bool,
|
||||||
show_git_blame_gutter: bool,
|
show_git_blame_gutter: bool,
|
||||||
show_git_blame_inline: bool,
|
show_git_blame_inline: bool,
|
||||||
show_git_blame_inline_delay_task: Option<Task<()>>,
|
show_git_blame_inline_delay_task: Option<Task<()>>,
|
||||||
|
@ -1410,6 +1412,7 @@ impl Editor {
|
||||||
use_autoclose: true,
|
use_autoclose: true,
|
||||||
use_auto_surround: true,
|
use_auto_surround: true,
|
||||||
auto_replace_emoji_shortcode: false,
|
auto_replace_emoji_shortcode: false,
|
||||||
|
jsx_tag_auto_close_enabled_in_any_buffer: false,
|
||||||
leader_peer_id: None,
|
leader_peer_id: None,
|
||||||
remote_id: None,
|
remote_id: None,
|
||||||
hover_state: Default::default(),
|
hover_state: Default::default(),
|
||||||
|
@ -1493,6 +1496,7 @@ impl Editor {
|
||||||
|
|
||||||
this.end_selection(window, cx);
|
this.end_selection(window, cx);
|
||||||
this.scroll_manager.show_scrollbar(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 {
|
if mode == EditorMode::Full {
|
||||||
let should_auto_hide_scrollbars = cx.should_auto_hide_scrollbars();
|
let should_auto_hide_scrollbars = cx.should_auto_hide_scrollbars();
|
||||||
|
@ -3101,6 +3105,9 @@ impl Editor {
|
||||||
drop(snapshot);
|
drop(snapshot);
|
||||||
|
|
||||||
self.transact(window, cx, |this, window, cx| {
|
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| {
|
this.buffer.update(cx, |buffer, cx| {
|
||||||
buffer.edit(edits, this.autoindent_mode.clone(), 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);
|
this.trigger_completion_on_input(&text, trigger_in_words, window, cx);
|
||||||
linked_editing_ranges::refresh_linked_ranges(this, window, cx);
|
linked_editing_ranges::refresh_linked_ranges(this, window, cx);
|
||||||
this.refresh_inline_completion(true, false, 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);
|
let buffer = self.buffer.read(cx);
|
||||||
self.registered_buffers
|
self.registered_buffers
|
||||||
.retain(|buffer_id, _| buffer.buffer(*buffer_id).is_some());
|
.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() })
|
cx.emit(EditorEvent::ExcerptsRemoved { ids: ids.clone() })
|
||||||
}
|
}
|
||||||
multi_buffer::Event::ExcerptsEdited {
|
multi_buffer::Event::ExcerptsEdited {
|
||||||
|
@ -15412,6 +15421,7 @@ impl Editor {
|
||||||
}
|
}
|
||||||
multi_buffer::Event::Reparsed(buffer_id) => {
|
multi_buffer::Event::Reparsed(buffer_id) => {
|
||||||
self.tasks_update_task = Some(self.refresh_runnables(window, cx));
|
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));
|
cx.emit(EditorEvent::Reparsed(*buffer_id));
|
||||||
}
|
}
|
||||||
|
@ -15420,6 +15430,7 @@ impl Editor {
|
||||||
}
|
}
|
||||||
multi_buffer::Event::LanguageChanged(buffer_id) => {
|
multi_buffer::Event::LanguageChanged(buffer_id) => {
|
||||||
linked_editing_ranges::refresh_linked_ranges(self, window, cx);
|
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.emit(EditorEvent::Reparsed(*buffer_id));
|
||||||
cx.notify();
|
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> {
|
fn empty_range(row: usize, column: usize) -> Range<DisplayPoint> {
|
||||||
let point = DisplayPoint::new(DisplayRow(row as u32), column as u32);
|
let point = DisplayPoint::new(DisplayRow(row as u32), column as u32);
|
||||||
point..point
|
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()
|
.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`].
|
/// Returns the main [`Language`].
|
||||||
pub fn language(&self) -> Option<&Arc<Language>> {
|
pub fn language(&self) -> Option<&Arc<Language>> {
|
||||||
self.language.as_ref()
|
self.language.as_ref()
|
||||||
|
|
|
@ -680,6 +680,9 @@ pub struct LanguageConfig {
|
||||||
/// languages, but should not appear to the user as a distinct language.
|
/// languages, but should not appear to the user as a distinct language.
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub hidden: bool,
|
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)]
|
#[derive(Clone, Debug, Serialize, Deserialize, Default, JsonSchema)]
|
||||||
|
@ -697,6 +700,34 @@ pub struct LanguageMatcher {
|
||||||
pub first_line_pattern: Option<Regex>,
|
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)
|
/// Represents a language for the given range. Some languages (e.g. HTML)
|
||||||
/// interleave several languages together, thus a single buffer might actually contain
|
/// interleave several languages together, thus a single buffer might actually contain
|
||||||
/// several nested scopes.
|
/// several nested scopes.
|
||||||
|
@ -767,6 +798,7 @@ impl Default for LanguageConfig {
|
||||||
soft_wrap: None,
|
soft_wrap: None,
|
||||||
prettier_parser_name: None,
|
prettier_parser_name: None,
|
||||||
hidden: false,
|
hidden: false,
|
||||||
|
jsx_tag_auto_close: None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -888,7 +920,7 @@ pub struct BracketPair {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy)]
|
#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy)]
|
||||||
pub(crate) struct LanguageId(usize);
|
pub struct LanguageId(usize);
|
||||||
|
|
||||||
impl LanguageId {
|
impl LanguageId {
|
||||||
pub(crate) fn new() -> Self {
|
pub(crate) fn new() -> Self {
|
||||||
|
@ -1056,6 +1088,10 @@ impl Language {
|
||||||
Self::new_with_id(LanguageId::new(), config, ts_language)
|
Self::new_with_id(LanguageId::new(), config, ts_language)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn id(&self) -> LanguageId {
|
||||||
|
self.id
|
||||||
|
}
|
||||||
|
|
||||||
fn new_with_id(
|
fn new_with_id(
|
||||||
id: LanguageId,
|
id: LanguageId,
|
||||||
config: LanguageConfig,
|
config: LanguageConfig,
|
||||||
|
|
|
@ -100,6 +100,8 @@ pub struct LanguageSettings {
|
||||||
pub formatter: SelectedFormatter,
|
pub formatter: SelectedFormatter,
|
||||||
/// Zed's Prettier integration settings.
|
/// Zed's Prettier integration settings.
|
||||||
pub prettier: PrettierSettings,
|
pub prettier: PrettierSettings,
|
||||||
|
/// Whether to automatically close JSX tags.
|
||||||
|
pub jsx_tag_auto_close: JsxTagAutoCloseSettings,
|
||||||
/// Whether to use language servers to provide code intelligence.
|
/// Whether to use language servers to provide code intelligence.
|
||||||
pub enable_language_server: bool,
|
pub enable_language_server: bool,
|
||||||
/// The list of language servers to use (or disable) for this language.
|
/// The list of language servers to use (or disable) for this language.
|
||||||
|
@ -374,6 +376,9 @@ pub struct LanguageSettingsContent {
|
||||||
/// Default: off
|
/// Default: off
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub prettier: Option<PrettierSettings>,
|
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.
|
/// Whether to use language servers to provide code intelligence.
|
||||||
///
|
///
|
||||||
/// Default: true
|
/// Default: true
|
||||||
|
@ -1335,6 +1340,10 @@ fn merge_settings(settings: &mut LanguageSettings, src: &LanguageSettingsContent
|
||||||
);
|
);
|
||||||
merge(&mut settings.formatter, src.formatter.clone());
|
merge(&mut settings.formatter, src.formatter.clone());
|
||||||
merge(&mut settings.prettier, src.prettier.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.format_on_save, src.format_on_save.clone());
|
||||||
merge(
|
merge(
|
||||||
&mut settings.remove_trailing_whitespace_on_save,
|
&mut settings.remove_trailing_whitespace_on_save,
|
||||||
|
@ -1398,6 +1407,13 @@ pub struct PrettierSettings {
|
||||||
pub options: HashMap<String, serde_json::Value>,
|
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)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use gpui::TestAppContext;
|
use gpui::TestAppContext;
|
||||||
|
|
|
@ -121,9 +121,9 @@ impl SyntaxLayerContent {
|
||||||
pub struct SyntaxLayer<'a> {
|
pub struct SyntaxLayer<'a> {
|
||||||
/// The language for this layer.
|
/// The language for this layer.
|
||||||
pub language: &'a Arc<Language>,
|
pub language: &'a Arc<Language>,
|
||||||
depth: usize,
|
pub(crate) depth: usize,
|
||||||
tree: &'a Tree,
|
tree: &'a Tree,
|
||||||
offset: (usize, tree_sitter::Point),
|
pub(crate) offset: (usize, tree_sitter::Point),
|
||||||
}
|
}
|
||||||
|
|
||||||
/// A layer of syntax highlighting. Like [SyntaxLayer], but holding
|
/// A layer of syntax highlighting. Like [SyntaxLayer], but holding
|
||||||
|
@ -133,7 +133,7 @@ pub struct OwnedSyntaxLayer {
|
||||||
/// The language for this layer.
|
/// The language for this layer.
|
||||||
pub language: Arc<Language>,
|
pub language: Arc<Language>,
|
||||||
tree: tree_sitter::Tree,
|
tree: tree_sitter::Tree,
|
||||||
offset: (usize, tree_sitter::Point),
|
pub offset: (usize, tree_sitter::Point),
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
|
|
|
@ -20,6 +20,12 @@ tab_size = 2
|
||||||
scope_opt_in_language_servers = ["tailwindcss-language-server", "emmet-language-server"]
|
scope_opt_in_language_servers = ["tailwindcss-language-server", "emmet-language-server"]
|
||||||
prettier_parser_name = "babel"
|
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]
|
[overrides.element]
|
||||||
line_comments = { remove = true }
|
line_comments = { remove = true }
|
||||||
block_comment = ["{/* ", " */}"]
|
block_comment = ["{/* ", " */}"]
|
||||||
|
|
|
@ -11,7 +11,7 @@ use std::{str, sync::Arc};
|
||||||
use typescript::typescript_task_context;
|
use typescript::typescript_task_context;
|
||||||
use util::{asset_str, ResultExt};
|
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 bash;
|
||||||
mod c;
|
mod c;
|
||||||
|
@ -74,177 +74,191 @@ pub fn init(languages: Arc<LanguageRegistry>, node_runtime: NodeRuntime, cx: &mu
|
||||||
("gitcommit", tree_sitter_gitcommit::LANGUAGE),
|
("gitcommit", tree_sitter_gitcommit::LANGUAGE),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
macro_rules! language {
|
// Following are a series of helper macros for registering languages.
|
||||||
($name:literal) => {
|
// Macros are used instead of a function or for loop in order to avoid
|
||||||
let config = load_config($name);
|
// code duplication and improve readability as the types get quite verbose
|
||||||
languages.register_language(
|
// to type out in some cases.
|
||||||
config.name.clone(),
|
// Additionally, the `provider` fields in LoadedLanguage
|
||||||
config.grammar.clone(),
|
// would have be `Copy` if we were to use a function or for-loop to register the languages
|
||||||
config.matcher.clone(),
|
// due to the fact that we pass an `Arc<Fn>` to `languages.register_language`
|
||||||
config.hidden,
|
// that loads and initializes the language lazily.
|
||||||
Arc::new(move || {
|
// We avoid this entirely by using a Macro
|
||||||
Ok(LoadedLanguage {
|
|
||||||
config: config.clone(),
|
macro_rules! context_provider {
|
||||||
queries: load_queries($name),
|
($name:expr) => {
|
||||||
context_provider: None,
|
Some(Arc::new($name) as Arc<dyn ContextProvider>)
|
||||||
toolchain_provider: None,
|
|
||||||
})
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
};
|
};
|
||||||
($name:literal, $adapters:expr) => {
|
() => {
|
||||||
let config = load_config($name);
|
None
|
||||||
// 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),
|
|
||||||
})
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
language!("bash", Vec::new(), bash_task_context());
|
|
||||||
language!("c", vec![Arc::new(c::CLspAdapter) as Arc<dyn LspAdapter>]);
|
macro_rules! toolchain_provider {
|
||||||
language!("cpp", vec![Arc::new(c::CLspAdapter)]);
|
($name:expr) => {
|
||||||
language!(
|
Some(Arc::new($name) as Arc<dyn ToolchainLister>)
|
||||||
"css",
|
};
|
||||||
vec![Arc::new(css::CssLspAdapter::new(node_runtime.clone())),]
|
() => {
|
||||||
);
|
None
|
||||||
language!("diff");
|
};
|
||||||
language!("go", vec![Arc::new(go::GoLspAdapter)], GoContextProvider);
|
}
|
||||||
language!("gomod", vec![Arc::new(go::GoLspAdapter)], GoContextProvider);
|
|
||||||
language!(
|
macro_rules! adapters {
|
||||||
"gowork",
|
($($item:expr),+ $(,)?) => {
|
||||||
vec![Arc::new(go::GoLspAdapter)],
|
vec![
|
||||||
GoContextProvider
|
$(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",
|
"json",
|
||||||
vec![
|
adapters => adapters![
|
||||||
Arc::new(json::JsonLspAdapter::new(
|
json::JsonLspAdapter::new(node_runtime.clone(), languages.clone(),),
|
||||||
node_runtime.clone(),
|
json::NodeVersionAdapter,
|
||||||
languages.clone(),
|
|
||||||
)),
|
|
||||||
Arc::new(json::NodeVersionAdapter)
|
|
||||||
],
|
],
|
||||||
json_task_context()
|
context => context_provider!(json_task_context())
|
||||||
);
|
);
|
||||||
language!(
|
register_language!(
|
||||||
"jsonc",
|
"jsonc",
|
||||||
vec![Arc::new(json::JsonLspAdapter::new(
|
adapters => adapters![
|
||||||
node_runtime.clone(),
|
json::JsonLspAdapter::new(node_runtime.clone(), languages.clone(),),
|
||||||
languages.clone(),
|
],
|
||||||
))],
|
context => context_provider!(json_task_context())
|
||||||
json_task_context()
|
|
||||||
);
|
);
|
||||||
language!("markdown");
|
|
||||||
language!("markdown-inline");
|
register_language!("markdown");
|
||||||
language!(
|
register_language!("markdown-inline");
|
||||||
|
|
||||||
|
register_language!(
|
||||||
"python",
|
"python",
|
||||||
vec![
|
adapters => adapters![
|
||||||
Arc::new(python::PythonLspAdapter::new(node_runtime.clone(),)),
|
python::PythonLspAdapter::new(node_runtime.clone()),
|
||||||
Arc::new(python::PyLspAdapter::new())
|
python::PyLspAdapter::new()
|
||||||
],
|
],
|
||||||
PythonContextProvider,
|
context => context_provider!(PythonContextProvider),
|
||||||
Arc::new(PythonToolchainProvider::default()) as Arc<dyn ToolchainLister>
|
toolchain => toolchain_provider!(PythonToolchainProvider::default())
|
||||||
);
|
);
|
||||||
language!(
|
register_language!(
|
||||||
"rust",
|
"rust",
|
||||||
vec![Arc::new(rust::RustLspAdapter)],
|
adapters => adapters![rust::RustLspAdapter],
|
||||||
RustContextProvider
|
context => context_provider!(RustContextProvider)
|
||||||
);
|
);
|
||||||
language!(
|
register_language!(
|
||||||
"tsx",
|
"tsx",
|
||||||
vec![
|
adapters => adapters![
|
||||||
Arc::new(typescript::TypeScriptLspAdapter::new(node_runtime.clone())),
|
typescript::TypeScriptLspAdapter::new(node_runtime.clone()),
|
||||||
Arc::new(vtsls::VtslsLspAdapter::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",
|
"typescript",
|
||||||
vec![
|
adapters => adapters![
|
||||||
Arc::new(typescript::TypeScriptLspAdapter::new(node_runtime.clone())),
|
typescript::TypeScriptLspAdapter::new(node_runtime.clone()),
|
||||||
Arc::new(vtsls::VtslsLspAdapter::new(node_runtime.clone()))
|
vtsls::VtslsLspAdapter::new(node_runtime.clone()),
|
||||||
],
|
],
|
||||||
typescript_task_context()
|
context => context_provider!(typescript_task_context())
|
||||||
);
|
);
|
||||||
language!(
|
register_language!(
|
||||||
"javascript",
|
"javascript",
|
||||||
vec![
|
adapters => adapters![
|
||||||
Arc::new(typescript::TypeScriptLspAdapter::new(node_runtime.clone())),
|
typescript::TypeScriptLspAdapter::new(node_runtime.clone()),
|
||||||
Arc::new(vtsls::VtslsLspAdapter::new(node_runtime.clone()))
|
vtsls::VtslsLspAdapter::new(node_runtime.clone()),
|
||||||
],
|
],
|
||||||
typescript_task_context()
|
context => context_provider!(typescript_task_context())
|
||||||
);
|
);
|
||||||
language!(
|
register_language!(
|
||||||
"jsdoc",
|
"jsdoc",
|
||||||
vec![
|
adapters => adapters![
|
||||||
Arc::new(typescript::TypeScriptLspAdapter::new(node_runtime.clone(),)),
|
typescript::TypeScriptLspAdapter::new(node_runtime.clone()),
|
||||||
Arc::new(vtsls::VtslsLspAdapter::new(node_runtime.clone()))
|
vtsls::VtslsLspAdapter::new(node_runtime.clone()),
|
||||||
]
|
]
|
||||||
);
|
);
|
||||||
language!("regex");
|
|
||||||
language!(
|
register_language!("regex");
|
||||||
"yaml",
|
|
||||||
vec![Arc::new(yaml::YamlLspAdapter::new(node_runtime.clone()))]
|
register_language!("yaml",
|
||||||
|
adapters => adapters![
|
||||||
|
yaml::YamlLspAdapter::new(node_runtime.clone()),
|
||||||
|
]
|
||||||
);
|
);
|
||||||
|
|
||||||
// Register globally available language servers.
|
// Register globally available language servers.
|
||||||
|
@ -366,6 +380,7 @@ fn load_config(name: &str) -> LanguageConfig {
|
||||||
config = LanguageConfig {
|
config = LanguageConfig {
|
||||||
name: config.name,
|
name: config.name,
|
||||||
matcher: config.matcher,
|
matcher: config.matcher,
|
||||||
|
jsx_tag_auto_close: config.jsx_tag_auto_close,
|
||||||
..Default::default()
|
..Default::default()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -18,6 +18,12 @@ scope_opt_in_language_servers = ["tailwindcss-language-server", "emmet-language-
|
||||||
prettier_parser_name = "typescript"
|
prettier_parser_name = "typescript"
|
||||||
tab_size = 2
|
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]
|
[overrides.element]
|
||||||
line_comments = { remove = true }
|
line_comments = { remove = true }
|
||||||
block_comment = ["{/* ", " */}"]
|
block_comment = ["{/* ", " */}"]
|
||||||
|
|
|
@ -1071,6 +1071,11 @@ impl MultiBuffer {
|
||||||
self.history.start_transaction(now)
|
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> {
|
pub fn end_transaction(&mut self, cx: &mut Context<Self>) -> Option<TransactionId> {
|
||||||
self.end_transaction_at(Instant::now(), cx)
|
self.end_transaction_at(Instant::now(), cx)
|
||||||
}
|
}
|
||||||
|
|
|
@ -591,6 +591,7 @@ impl<'a> Cursor<'a> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
pub struct Chunks<'a> {
|
pub struct Chunks<'a> {
|
||||||
chunks: sum_tree::Cursor<'a, Chunk, usize>,
|
chunks: sum_tree::Cursor<'a, Chunk, usize>,
|
||||||
range: Range<usize>,
|
range: Range<usize>,
|
||||||
|
@ -780,6 +781,40 @@ impl<'a> Chunks<'a> {
|
||||||
reversed,
|
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> {
|
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 {
|
fn clip_offset(text: &str, mut offset: usize, bias: Bias) -> usize {
|
||||||
while !text.is_char_boundary(offset) {
|
while !text.is_char_boundary(offset) {
|
||||||
match bias {
|
match bias {
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue