From f3fa3b910a119610f1eed83cd6291fddd560422f Mon Sep 17 00:00:00 2001 From: Hans Date: Tue, 27 Feb 2024 13:48:19 +0800 Subject: [PATCH] vim: Add HTML tag support for #4503 (#8175) a simple code for html tag support, I've only done the basics, and if it's okay, I'll optimize and organize the code, and adapt other parts like `is_multiline`, `always_expands_both_ways`, `target_visual_mode`, etc --------- Co-authored-by: Conrad Irwin --- assets/keymaps/vim.json | 1 + crates/editor/Cargo.toml | 1 + .../src/test/editor_lsp_test_context.rs | 16 +++ crates/vim/src/object.rs | 106 +++++++++++++++++- crates/vim/src/test/vim_test_context.rs | 5 + 5 files changed, 123 insertions(+), 6 deletions(-) diff --git a/assets/keymaps/vim.json b/assets/keymaps/vim.json index 7f855b9c4d490a790519140902bbaa315130606a..355c8700532b1ae4fa0acd8f5cac46f93641f6be 100644 --- a/assets/keymaps/vim.json +++ b/assets/keymaps/vim.json @@ -383,6 +383,7 @@ "ignorePunctuation": true } ], + "t": "vim::Tag", "s": "vim::Sentence", "'": "vim::Quotes", "`": "vim::BackQuotes", diff --git a/crates/editor/Cargo.toml b/crates/editor/Cargo.toml index 221d6a52462eb4a085234ab1ce1b9d83c4d684d9..8266e12ba7b5361778b5342c811f382fb507b561 100644 --- a/crates/editor/Cargo.toml +++ b/crates/editor/Cargo.toml @@ -21,6 +21,7 @@ test-support = [ "workspace/test-support", "tree-sitter-rust", "tree-sitter-typescript", + "tree-sitter-html" ] [dependencies] diff --git a/crates/editor/src/test/editor_lsp_test_context.rs b/crates/editor/src/test/editor_lsp_test_context.rs index b083e63890da8ab3902dc8d1f0f21934b26460ab..fe9a80b01b9805e0342bcacb3117e500702382b5 100644 --- a/crates/editor/src/test/editor_lsp_test_context.rs +++ b/crates/editor/src/test/editor_lsp_test_context.rs @@ -214,6 +214,22 @@ impl EditorLspTestContext { Self::new(language, capabilities, cx).await } + pub async fn new_html(cx: &mut gpui::TestAppContext) -> Self { + let language = Language::new( + LanguageConfig { + name: "HTML".into(), + matcher: LanguageMatcher { + path_suffixes: vec!["html".into()], + ..Default::default() + }, + block_comment: Some(("".into())), + ..Default::default() + }, + Some(tree_sitter_html::language()), + ); + Self::new(language, Default::default(), cx).await + } + // Constructs lsp range using a marked string with '[', ']' range delimiters pub fn lsp_range(&mut self, marked_text: &str) -> lsp::Range { let ranges = self.ranges(marked_text); diff --git a/crates/vim/src/object.rs b/crates/vim/src/object.rs index 72aa189d9f9ca21d51a801ced2be27ad89f828a6..d53be263440377d40a96ea51252393fc1659eb51 100644 --- a/crates/vim/src/object.rs +++ b/crates/vim/src/object.rs @@ -1,5 +1,9 @@ use std::ops::Range; +use crate::{ + motion::right, normal::normal_object, state::Mode, utils::coerce_punctuation, + visual::visual_object, Vim, +}; use editor::{ display_map::{DisplaySnapshot, ToDisplayPoint}, movement::{self, FindRange}, @@ -10,11 +14,6 @@ use language::{char_kind, BufferSnapshot, CharKind, Selection}; use serde::Deserialize; use workspace::Workspace; -use crate::{ - motion::right, normal::normal_object, state::Mode, utils::coerce_punctuation, - visual::visual_object, Vim, -}; - #[derive(Copy, Clone, Debug, PartialEq)] pub enum Object { Word { ignore_punctuation: bool }, @@ -28,6 +27,7 @@ pub enum Object { CurlyBrackets, AngleBrackets, Argument, + Tag, } #[derive(Clone, Deserialize, PartialEq)] @@ -51,7 +51,8 @@ actions!( SquareBrackets, CurlyBrackets, AngleBrackets, - Argument + Argument, + Tag ] ); @@ -61,6 +62,7 @@ pub fn register(workspace: &mut Workspace, _: &mut ViewContext) { object(Object::Word { ignore_punctuation }, cx) }, ); + workspace.register_action(|_: &mut Workspace, _: &Tag, cx: _| object(Object::Tag, cx)); workspace .register_action(|_: &mut Workspace, _: &Sentence, cx: _| object(Object::Sentence, cx)); workspace.register_action(|_: &mut Workspace, _: &Quotes, cx: _| object(Object::Quotes, cx)); @@ -108,6 +110,7 @@ impl Object { | Object::DoubleQuotes => false, Object::Sentence | Object::Parentheses + | Object::Tag | Object::AngleBrackets | Object::CurlyBrackets | Object::SquareBrackets @@ -124,6 +127,7 @@ impl Object { | Object::VerticalBars | Object::Parentheses | Object::SquareBrackets + | Object::Tag | Object::CurlyBrackets | Object::AngleBrackets => true, } @@ -147,6 +151,7 @@ impl Object { | Object::CurlyBrackets | Object::AngleBrackets | Object::VerticalBars + | Object::Tag | Object::Argument => Mode::Visual, } } @@ -181,6 +186,7 @@ impl Object { Object::Parentheses => { surrounding_markers(map, relative_to, around, self.is_multiline(), '(', ')') } + Object::Tag => surrounding_html_tag(map, relative_to, around), Object::SquareBrackets => { surrounding_markers(map, relative_to, around, self.is_multiline(), '[', ']') } @@ -241,6 +247,72 @@ fn in_word( Some(start..end) } +fn surrounding_html_tag( + map: &DisplaySnapshot, + relative_to: DisplayPoint, + surround: bool, +) -> Option> { + fn read_tag(chars: impl Iterator) -> String { + chars + .take_while(|c| c.is_alphanumeric() || *c == ':' || *c == '-' || *c == '_' || *c == '.') + .collect() + } + fn open_tag(mut chars: impl Iterator) -> Option { + if Some('<') != chars.next() { + return None; + } + Some(read_tag(chars)) + } + fn close_tag(mut chars: impl Iterator) -> Option { + if (Some('<'), Some('/')) != (chars.next(), chars.next()) { + return None; + } + Some(read_tag(chars)) + } + + let snapshot = &map.buffer_snapshot; + let offset = relative_to.to_offset(map, Bias::Left); + let excerpt = snapshot.excerpt_containing(offset..offset)?; + let buffer = excerpt.buffer(); + let offset = excerpt.map_offset_to_buffer(offset); + + // Find the most closest to current offset + let mut cursor = buffer.syntax_layer_at(offset)?.node().walk(); + let mut last_child_node = cursor.node(); + while cursor.goto_first_child_for_byte(offset).is_some() { + last_child_node = cursor.node(); + } + + let mut last_child_node = Some(last_child_node); + while let Some(cur_node) = last_child_node { + if cur_node.child_count() >= 2 { + let first_child = cur_node.child(0); + let last_child = cur_node.child(cur_node.child_count() - 1); + if let (Some(first_child), Some(last_child)) = (first_child, last_child) { + let open_tag = open_tag(buffer.chars_for_range(first_child.byte_range())); + let close_tag = close_tag(buffer.chars_for_range(last_child.byte_range())); + if open_tag.is_some() + && open_tag == close_tag + && (first_child.end_byte() + 1..last_child.start_byte()).contains(&offset) + { + let range = if surround { + first_child.byte_range().start..last_child.byte_range().end + } else { + first_child.byte_range().end..last_child.byte_range().start + }; + if excerpt.contains_buffer_range(range.clone()) { + let result = excerpt.map_range_from_buffer(range); + return Some( + result.start.to_display_point(map)..result.end.to_display_point(map), + ); + } + } + } + } + last_child_node = cur_node.parent(); + } + None +} /// Returns a range that surrounds the word and following whitespace /// relative_to is in. /// @@ -1246,4 +1318,26 @@ mod test { .await; } } + + #[gpui::test] + async fn test_tags(cx: &mut gpui::TestAppContext) { + let mut cx = VimTestContext::new_html(cx).await; + + cx.set_state("hˇi!", Mode::Normal); + cx.simulate_keystrokes(["v", "i", "t"]); + cx.assert_state( + "«hi!ˇ»", + Mode::Visual, + ); + cx.simulate_keystrokes(["a", "t"]); + cx.assert_state( + "«hi!ˇ»", + Mode::Visual, + ); + cx.simulate_keystrokes(["a", "t"]); + cx.assert_state( + "«hi!ˇ»", + Mode::Visual, + ); + } } diff --git a/crates/vim/src/test/vim_test_context.rs b/crates/vim/src/test/vim_test_context.rs index 872531bf6854e9bd5f2d578e0f25640b3c8a51f7..35fd74b70d8efbf8009c67268e53cc55a1cbfa49 100644 --- a/crates/vim/src/test/vim_test_context.rs +++ b/crates/vim/src/test/vim_test_context.rs @@ -31,6 +31,11 @@ impl VimTestContext { Self::new_with_lsp(lsp, enabled) } + pub async fn new_html(cx: &mut gpui::TestAppContext) -> VimTestContext { + Self::init(cx); + Self::new_with_lsp(EditorLspTestContext::new_html(cx).await, true) + } + pub async fn new_typescript(cx: &mut gpui::TestAppContext) -> VimTestContext { Self::init(cx); Self::new_with_lsp(