Cargo.lock 🔗
@@ -4154,6 +4154,7 @@ dependencies = [
"inline_completion",
"itertools 0.14.0",
"language",
+ "languages",
"linkify",
"log",
"lsp",
Ben Kunkle , Cole Miller , Max Brunsfeld , Marshall Bowers , Mikayla , and Peter Tripp created
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>
Cargo.lock | 1
assets/settings/default.json | 5
crates/editor/Cargo.toml | 1
crates/editor/src/editor.rs | 11
crates/editor/src/editor_tests.rs | 239 ++++++++
crates/editor/src/jsx_tag_auto_close.rs | 616 +++++++++++++++++++++++
crates/language/src/buffer.rs | 19
crates/language/src/language.rs | 38 +
crates/language/src/language_settings.rs | 16
crates/language/src/syntax_map.rs | 6
crates/languages/src/javascript/config.toml | 6
crates/languages/src/lib.rs | 265 +++++----
crates/languages/src/tsx/config.toml | 6
crates/multi_buffer/src/multi_buffer.rs | 5
crates/rope/src/rope.rs | 82 +++
15 files changed, 1,187 insertions(+), 129 deletions(-)
@@ -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
@@ -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,73 +74,49 @@ 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,
- })
- }),
- );
+ () => {
+ 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,
- })
- }),
- );
+ }
+
+ macro_rules! toolchain_provider {
+ ($name:expr) => {
+ Some(Arc::new($name) as Arc<dyn ToolchainLister>)
};
- ($name:literal, $adapters:expr, $context_provider:expr, $toolchain_provider:expr) => {
+ () => {
+ 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);
- // typeck helper
- let adapters: Vec<Arc<dyn LspAdapter>> = $adapters;
- for adapter in adapters {
+ for adapter in $adapters {
languages.register_lsp_adapter(config.name.clone(), adapter);
}
languages.register_language(
@@ -152,99 +128,137 @@ pub fn init(languages: Arc<LanguageRegistry>, node_runtime: NodeRuntime, cx: &mu
Ok(LoadedLanguage {
config: config.clone(),
queries: load_queries($name),
- context_provider: Some(Arc::new($context_provider)),
- toolchain_provider: Some($toolchain_provider),
+ 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!())
+ };
}
- 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!(
+
+ register_language!(
+ "bash",
+ adapters => adapters![],
+ context => context_provider!(bash_task_context()),
+ toolchain => toolchain_provider!()
+ );
+
+ register_language!(
+ "c",
+ adapters => adapters![c::CLspAdapter]
+ );
+ register_language!(
+ "cpp",
+ adapters => adapters![c::CLspAdapter]
+ );
+
+ register_language!(
"css",
- vec![Arc::new(css::CssLspAdapter::new(node_runtime.clone())),]
+ 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)
);
- language!("diff");
- language!("go", vec![Arc::new(go::GoLspAdapter)], GoContextProvider);
- language!("gomod", vec![Arc::new(go::GoLspAdapter)], GoContextProvider);
- language!(
+ register_language!(
"gowork",
- vec![Arc::new(go::GoLspAdapter)],
- GoContextProvider
+ adapters => adapters![go::GoLspAdapter],
+ context => context_provider!(go::GoContextProvider)
);
- language!(
+ 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 {