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 <conrad.irwin@gmail.com>
This commit is contained in:
Hans 2024-02-27 13:48:19 +08:00 committed by GitHub
parent a42b987929
commit f3fa3b910a
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 123 additions and 6 deletions

View File

@ -383,6 +383,7 @@
"ignorePunctuation": true "ignorePunctuation": true
} }
], ],
"t": "vim::Tag",
"s": "vim::Sentence", "s": "vim::Sentence",
"'": "vim::Quotes", "'": "vim::Quotes",
"`": "vim::BackQuotes", "`": "vim::BackQuotes",

View File

@ -21,6 +21,7 @@ test-support = [
"workspace/test-support", "workspace/test-support",
"tree-sitter-rust", "tree-sitter-rust",
"tree-sitter-typescript", "tree-sitter-typescript",
"tree-sitter-html"
] ]
[dependencies] [dependencies]

View File

@ -214,6 +214,22 @@ impl EditorLspTestContext {
Self::new(language, capabilities, cx).await 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(), " -->".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 // Constructs lsp range using a marked string with '[', ']' range delimiters
pub fn lsp_range(&mut self, marked_text: &str) -> lsp::Range { pub fn lsp_range(&mut self, marked_text: &str) -> lsp::Range {
let ranges = self.ranges(marked_text); let ranges = self.ranges(marked_text);

View File

@ -1,5 +1,9 @@
use std::ops::Range; use std::ops::Range;
use crate::{
motion::right, normal::normal_object, state::Mode, utils::coerce_punctuation,
visual::visual_object, Vim,
};
use editor::{ use editor::{
display_map::{DisplaySnapshot, ToDisplayPoint}, display_map::{DisplaySnapshot, ToDisplayPoint},
movement::{self, FindRange}, movement::{self, FindRange},
@ -10,11 +14,6 @@ use language::{char_kind, BufferSnapshot, CharKind, Selection};
use serde::Deserialize; use serde::Deserialize;
use workspace::Workspace; use workspace::Workspace;
use crate::{
motion::right, normal::normal_object, state::Mode, utils::coerce_punctuation,
visual::visual_object, Vim,
};
#[derive(Copy, Clone, Debug, PartialEq)] #[derive(Copy, Clone, Debug, PartialEq)]
pub enum Object { pub enum Object {
Word { ignore_punctuation: bool }, Word { ignore_punctuation: bool },
@ -28,6 +27,7 @@ pub enum Object {
CurlyBrackets, CurlyBrackets,
AngleBrackets, AngleBrackets,
Argument, Argument,
Tag,
} }
#[derive(Clone, Deserialize, PartialEq)] #[derive(Clone, Deserialize, PartialEq)]
@ -51,7 +51,8 @@ actions!(
SquareBrackets, SquareBrackets,
CurlyBrackets, CurlyBrackets,
AngleBrackets, AngleBrackets,
Argument Argument,
Tag
] ]
); );
@ -61,6 +62,7 @@ pub fn register(workspace: &mut Workspace, _: &mut ViewContext<Workspace>) {
object(Object::Word { ignore_punctuation }, cx) object(Object::Word { ignore_punctuation }, cx)
}, },
); );
workspace.register_action(|_: &mut Workspace, _: &Tag, cx: _| object(Object::Tag, cx));
workspace workspace
.register_action(|_: &mut Workspace, _: &Sentence, cx: _| object(Object::Sentence, cx)); .register_action(|_: &mut Workspace, _: &Sentence, cx: _| object(Object::Sentence, cx));
workspace.register_action(|_: &mut Workspace, _: &Quotes, cx: _| object(Object::Quotes, cx)); workspace.register_action(|_: &mut Workspace, _: &Quotes, cx: _| object(Object::Quotes, cx));
@ -108,6 +110,7 @@ impl Object {
| Object::DoubleQuotes => false, | Object::DoubleQuotes => false,
Object::Sentence Object::Sentence
| Object::Parentheses | Object::Parentheses
| Object::Tag
| Object::AngleBrackets | Object::AngleBrackets
| Object::CurlyBrackets | Object::CurlyBrackets
| Object::SquareBrackets | Object::SquareBrackets
@ -124,6 +127,7 @@ impl Object {
| Object::VerticalBars | Object::VerticalBars
| Object::Parentheses | Object::Parentheses
| Object::SquareBrackets | Object::SquareBrackets
| Object::Tag
| Object::CurlyBrackets | Object::CurlyBrackets
| Object::AngleBrackets => true, | Object::AngleBrackets => true,
} }
@ -147,6 +151,7 @@ impl Object {
| Object::CurlyBrackets | Object::CurlyBrackets
| Object::AngleBrackets | Object::AngleBrackets
| Object::VerticalBars | Object::VerticalBars
| Object::Tag
| Object::Argument => Mode::Visual, | Object::Argument => Mode::Visual,
} }
} }
@ -181,6 +186,7 @@ impl Object {
Object::Parentheses => { Object::Parentheses => {
surrounding_markers(map, relative_to, around, self.is_multiline(), '(', ')') surrounding_markers(map, relative_to, around, self.is_multiline(), '(', ')')
} }
Object::Tag => surrounding_html_tag(map, relative_to, around),
Object::SquareBrackets => { Object::SquareBrackets => {
surrounding_markers(map, relative_to, around, self.is_multiline(), '[', ']') surrounding_markers(map, relative_to, around, self.is_multiline(), '[', ']')
} }
@ -241,6 +247,72 @@ fn in_word(
Some(start..end) Some(start..end)
} }
fn surrounding_html_tag(
map: &DisplaySnapshot,
relative_to: DisplayPoint,
surround: bool,
) -> Option<Range<DisplayPoint>> {
fn read_tag(chars: impl Iterator<Item = char>) -> String {
chars
.take_while(|c| c.is_alphanumeric() || *c == ':' || *c == '-' || *c == '_' || *c == '.')
.collect()
}
fn open_tag(mut chars: impl Iterator<Item = char>) -> Option<String> {
if Some('<') != chars.next() {
return None;
}
Some(read_tag(chars))
}
fn close_tag(mut chars: impl Iterator<Item = char>) -> Option<String> {
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 /// Returns a range that surrounds the word and following whitespace
/// relative_to is in. /// relative_to is in.
/// ///
@ -1246,4 +1318,26 @@ mod test {
.await; .await;
} }
} }
#[gpui::test]
async fn test_tags(cx: &mut gpui::TestAppContext) {
let mut cx = VimTestContext::new_html(cx).await;
cx.set_state("<html><head></head><body><b>hˇi!</b></body>", Mode::Normal);
cx.simulate_keystrokes(["v", "i", "t"]);
cx.assert_state(
"<html><head></head><body><b>«hi!ˇ»</b></body>",
Mode::Visual,
);
cx.simulate_keystrokes(["a", "t"]);
cx.assert_state(
"<html><head></head><body>«<b>hi!</b>ˇ»</body>",
Mode::Visual,
);
cx.simulate_keystrokes(["a", "t"]);
cx.assert_state(
"<html><head></head>«<body><b>hi!</b></body>ˇ»",
Mode::Visual,
);
}
} }

View File

@ -31,6 +31,11 @@ impl VimTestContext {
Self::new_with_lsp(lsp, enabled) 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 { pub async fn new_typescript(cx: &mut gpui::TestAppContext) -> VimTestContext {
Self::init(cx); Self::init(cx);
Self::new_with_lsp( Self::new_with_lsp(