Detailed changes
@@ -205,6 +205,49 @@ impl EditorLspTestContext {
(_ "{" "}" @end) @indent
(_ "(" ")" @end) @indent
"#})),
+ text_objects: Some(Cow::from(indoc! {r#"
+ (function_declaration
+ body: (_
+ "{"
+ (_)* @function.inside
+ "}")) @function.around
+
+ (method_definition
+ body: (_
+ "{"
+ (_)* @function.inside
+ "}")) @function.around
+
+ ; Arrow function in variable declaration - capture the full declaration
+ ([
+ (lexical_declaration
+ (variable_declarator
+ value: (arrow_function
+ body: (statement_block
+ "{"
+ (_)* @function.inside
+ "}"))))
+ (variable_declaration
+ (variable_declarator
+ value: (arrow_function
+ body: (statement_block
+ "{"
+ (_)* @function.inside
+ "}"))))
+ ]) @function.around
+
+ ([
+ (lexical_declaration
+ (variable_declarator
+ value: (arrow_function)))
+ (variable_declaration
+ (variable_declarator
+ value: (arrow_function)))
+ ]) @function.around
+
+ ; Catch-all for arrow functions in other contexts (callbacks, etc.)
+ ((arrow_function) @function.around (#not-has-parent? @function.around variable_declarator))
+ "#})),
..Default::default()
})
.expect("Could not parse queries");
@@ -276,6 +319,49 @@ impl EditorLspTestContext {
(jsx_opening_element) @start
(jsx_closing_element)? @end) @indent
"#})),
+ text_objects: Some(Cow::from(indoc! {r#"
+ (function_declaration
+ body: (_
+ "{"
+ (_)* @function.inside
+ "}")) @function.around
+
+ (method_definition
+ body: (_
+ "{"
+ (_)* @function.inside
+ "}")) @function.around
+
+ ; Arrow function in variable declaration - capture the full declaration
+ ([
+ (lexical_declaration
+ (variable_declarator
+ value: (arrow_function
+ body: (statement_block
+ "{"
+ (_)* @function.inside
+ "}"))))
+ (variable_declaration
+ (variable_declarator
+ value: (arrow_function
+ body: (statement_block
+ "{"
+ (_)* @function.inside
+ "}"))))
+ ]) @function.around
+
+ ([
+ (lexical_declaration
+ (variable_declarator
+ value: (arrow_function)))
+ (variable_declaration
+ (variable_declarator
+ value: (arrow_function)))
+ ]) @function.around
+
+ ; Catch-all for arrow functions in other contexts (callbacks, etc.)
+ ((arrow_function) @function.around (#not-has-parent? @function.around variable_declarator))
+ "#})),
..Default::default()
})
.expect("Could not parse queries");
@@ -1141,6 +1141,104 @@ fn test_text_objects(cx: &mut App) {
)
}
+#[gpui::test]
+fn test_text_objects_with_has_parent_predicate(cx: &mut App) {
+ use std::borrow::Cow;
+
+ // Create a language with a custom text_objects query that uses #has-parent?
+ // This query only matches closure_expression when it's inside a call_expression
+ let language = Language::new(
+ LanguageConfig {
+ name: "Rust".into(),
+ matcher: LanguageMatcher {
+ path_suffixes: vec!["rs".to_string()],
+ ..Default::default()
+ },
+ ..Default::default()
+ },
+ Some(tree_sitter_rust::LANGUAGE.into()),
+ )
+ .with_queries(LanguageQueries {
+ text_objects: Some(Cow::from(indoc! {r#"
+ ; Only match closures that are arguments to function calls
+ (closure_expression) @function.around
+ (#has-parent? @function.around arguments)
+ "#})),
+ ..Default::default()
+ })
+ .expect("Could not parse queries");
+
+ let (text, ranges) = marked_text_ranges(
+ indoc! {r#"
+ fn main() {
+ let standalone = |x| x + 1;
+ let result = foo(|y| y * ˇ2);
+ }"#
+ },
+ false,
+ );
+
+ let buffer = cx.new(|cx| Buffer::local(text.clone(), cx).with_language(Arc::new(language), cx));
+ let snapshot = buffer.update(cx, |buffer, _| buffer.snapshot());
+
+ let matches = snapshot
+ .text_object_ranges(ranges[0].clone(), TreeSitterOptions::default())
+ .map(|(range, text_object)| (&text[range], text_object))
+ .collect::<Vec<_>>();
+
+ // Should only match the closure inside foo(), not the standalone closure
+ assert_eq!(matches, &[("|y| y * 2", TextObject::AroundFunction),]);
+}
+
+#[gpui::test]
+fn test_text_objects_with_not_has_parent_predicate(cx: &mut App) {
+ use std::borrow::Cow;
+
+ // Create a language with a custom text_objects query that uses #not-has-parent?
+ // This query only matches closure_expression when it's NOT inside a call_expression
+ let language = Language::new(
+ LanguageConfig {
+ name: "Rust".into(),
+ matcher: LanguageMatcher {
+ path_suffixes: vec!["rs".to_string()],
+ ..Default::default()
+ },
+ ..Default::default()
+ },
+ Some(tree_sitter_rust::LANGUAGE.into()),
+ )
+ .with_queries(LanguageQueries {
+ text_objects: Some(Cow::from(indoc! {r#"
+ ; Only match closures that are NOT arguments to function calls
+ (closure_expression) @function.around
+ (#not-has-parent? @function.around arguments)
+ "#})),
+ ..Default::default()
+ })
+ .expect("Could not parse queries");
+
+ let (text, ranges) = marked_text_ranges(
+ indoc! {r#"
+ fn main() {
+ let standalone = |x| x +ˇ 1;
+ let result = foo(|y| y * 2);
+ }"#
+ },
+ false,
+ );
+
+ let buffer = cx.new(|cx| Buffer::local(text.clone(), cx).with_language(Arc::new(language), cx));
+ let snapshot = buffer.update(cx, |buffer, _| buffer.snapshot());
+
+ let matches = snapshot
+ .text_object_ranges(ranges[0].clone(), TreeSitterOptions::default())
+ .map(|(range, text_object)| (&text[range], text_object))
+ .collect::<Vec<_>>();
+
+ // Should only match the standalone closure, not the one inside foo()
+ assert_eq!(matches, &[("|x| x + 1", TextObject::AroundFunction),]);
+}
+
#[gpui::test]
fn test_enclosing_bracket_ranges(cx: &mut App) {
#[track_caller]
@@ -19,7 +19,10 @@ use std::{
use streaming_iterator::StreamingIterator;
use sum_tree::{Bias, Dimensions, SeekTarget, SumTree};
use text::{Anchor, BufferSnapshot, OffsetRangeExt, Point, Rope, ToOffset, ToPoint};
-use tree_sitter::{Node, Query, QueryCapture, QueryCaptures, QueryCursor, QueryMatches, Tree};
+use tree_sitter::{
+ Node, Query, QueryCapture, QueryCaptures, QueryCursor, QueryMatch, QueryMatches,
+ QueryPredicateArg, Tree,
+};
pub const MAX_BYTES_TO_QUERY: usize = 16 * 1024;
@@ -82,6 +85,7 @@ struct SyntaxMapMatchesLayer<'a> {
next_captures: Vec<QueryCapture<'a>>,
has_next: bool,
matches: QueryMatches<'a, 'a, TextProvider<'a>, &'a [u8]>,
+ query: &'a Query,
grammar_index: usize,
_query_cursor: QueryCursorHandle,
}
@@ -1163,6 +1167,7 @@ impl<'a> SyntaxMapMatches<'a> {
depth: layer.depth,
grammar_index,
matches,
+ query,
next_pattern_index: 0,
next_captures: Vec::new(),
has_next: false,
@@ -1260,13 +1265,20 @@ impl SyntaxMapCapturesLayer<'_> {
impl SyntaxMapMatchesLayer<'_> {
fn advance(&mut self) {
- if let Some(mat) = self.matches.next() {
- self.next_captures.clear();
- self.next_captures.extend_from_slice(mat.captures);
- self.next_pattern_index = mat.pattern_index;
- self.has_next = true;
- } else {
- self.has_next = false;
+ loop {
+ if let Some(mat) = self.matches.next() {
+ if !satisfies_custom_predicates(self.query, mat) {
+ continue;
+ }
+ self.next_captures.clear();
+ self.next_captures.extend_from_slice(mat.captures);
+ self.next_pattern_index = mat.pattern_index;
+ self.has_next = true;
+ return;
+ } else {
+ self.has_next = false;
+ return;
+ }
}
}
@@ -1295,6 +1307,39 @@ impl<'a> Iterator for SyntaxMapCaptures<'a> {
}
}
+fn satisfies_custom_predicates(query: &Query, mat: &QueryMatch) -> bool {
+ for predicate in query.general_predicates(mat.pattern_index) {
+ let satisfied = match predicate.operator.as_ref() {
+ "has-parent?" => has_parent(&predicate.args, mat),
+ "not-has-parent?" => !has_parent(&predicate.args, mat),
+ _ => true,
+ };
+ if !satisfied {
+ return false;
+ }
+ }
+ true
+}
+
+fn has_parent(args: &[QueryPredicateArg], mat: &QueryMatch) -> bool {
+ let (
+ Some(QueryPredicateArg::Capture(capture_ix)),
+ Some(QueryPredicateArg::String(parent_kind)),
+ ) = (args.first(), args.get(1))
+ else {
+ return false;
+ };
+
+ let Some(capture) = mat.captures.iter().find(|c| c.index == *capture_ix) else {
+ return false;
+ };
+
+ capture
+ .node
+ .parent()
+ .is_some_and(|p| p.kind() == parent_kind.as_ref())
+}
+
fn join_ranges(
a: impl Iterator<Item = Range<usize>>,
b: impl Iterator<Item = Range<usize>>,
@@ -18,13 +18,47 @@
(_)* @function.inside
"}")) @function.around
-(arrow_function
+((arrow_function
body: (statement_block
"{"
(_)* @function.inside
"}")) @function.around
+ (#not-has-parent? @function.around variable_declarator))
-(arrow_function) @function.around
+; Arrow function in variable declaration - capture the full declaration
+([
+ (lexical_declaration
+ (variable_declarator
+ value: (arrow_function
+ body: (statement_block
+ "{"
+ (_)* @function.inside
+ "}"))))
+ (variable_declaration
+ (variable_declarator
+ value: (arrow_function
+ body: (statement_block
+ "{"
+ (_)* @function.inside
+ "}"))))
+]) @function.around
+
+; Arrow function in variable declaration (captures body for expression-bodied arrows)
+([
+ (lexical_declaration
+ (variable_declarator
+ value: (arrow_function
+ body: (_) @function.inside)))
+ (variable_declaration
+ (variable_declarator
+ value: (arrow_function
+ body: (_) @function.inside)))
+]) @function.around
+
+; Catch-all for arrow functions in other contexts (callbacks, etc.)
+((arrow_function
+ body: (_) @function.inside) @function.around
+ (#not-has-parent? @function.around variable_declarator))
(generator_function
body: (_
@@ -18,13 +18,47 @@
(_)* @function.inside
"}")) @function.around
-(arrow_function
+((arrow_function
body: (statement_block
"{"
(_)* @function.inside
"}")) @function.around
+ (#not-has-parent? @function.around variable_declarator))
-(arrow_function) @function.around
+; Arrow function in variable declaration - capture the full declaration
+([
+ (lexical_declaration
+ (variable_declarator
+ value: (arrow_function
+ body: (statement_block
+ "{"
+ (_)* @function.inside
+ "}"))))
+ (variable_declaration
+ (variable_declarator
+ value: (arrow_function
+ body: (statement_block
+ "{"
+ (_)* @function.inside
+ "}"))))
+]) @function.around
+
+; Arrow function in variable declaration (expression body fallback)
+([
+ (lexical_declaration
+ (variable_declarator
+ value: (arrow_function
+ body: (_) @function.inside)))
+ (variable_declaration
+ (variable_declarator
+ value: (arrow_function
+ body: (_) @function.inside)))
+]) @function.around
+
+; Catch-all for arrow functions in other contexts (callbacks, etc.)
+((arrow_function
+ body: (_) @function.inside) @function.around
+ (#not-has-parent? @function.around variable_declarator))
(function_signature) @function.around
(generator_function
@@ -18,13 +18,48 @@
(_)* @function.inside
"}")) @function.around
-(arrow_function
+((arrow_function
body: (statement_block
"{"
(_)* @function.inside
"}")) @function.around
+ (#not-has-parent? @function.around variable_declarator))
-(arrow_function) @function.around
+; Arrow function in variable declaration - capture the full declaration
+([
+ (lexical_declaration
+ (variable_declarator
+ value: (arrow_function
+ body: (statement_block
+ "{"
+ (_)* @function.inside
+ "}"))))
+ (variable_declaration
+ (variable_declarator
+ value: (arrow_function
+ body: (statement_block
+ "{"
+ (_)* @function.inside
+ "}"))))
+]) @function.around
+
+; Arrow function in variable declaration - capture body as @function.inside
+; (for statement blocks, the more specific pattern above captures just the contents)
+([
+ (lexical_declaration
+ (variable_declarator
+ value: (arrow_function
+ body: (_) @function.inside)))
+ (variable_declaration
+ (variable_declarator
+ value: (arrow_function
+ body: (_) @function.inside)))
+]) @function.around
+
+; Catch-all for arrow functions in other contexts (callbacks, etc.)
+((arrow_function
+ body: (_) @function.inside) @function.around
+ (#not-has-parent? @function.around variable_declarator))
(function_signature) @function.around
(generator_function
@@ -3407,4 +3407,390 @@ mod test {
.assert_eq(" ˇf = (x: unknown) => {");
cx.shared_clipboard().await.assert_eq("const ");
}
+
+ #[gpui::test]
+ async fn test_arrow_function_text_object(cx: &mut gpui::TestAppContext) {
+ let mut cx = VimTestContext::new_typescript(cx).await;
+
+ cx.set_state(
+ indoc! {"
+ const foo = () => {
+ return ˇ1;
+ };
+ "},
+ Mode::Normal,
+ );
+ cx.simulate_keystrokes("v a f");
+ cx.assert_state(
+ indoc! {"
+ «const foo = () => {
+ return 1;
+ };ˇ»
+ "},
+ Mode::VisualLine,
+ );
+
+ cx.set_state(
+ indoc! {"
+ arr.map(() => {
+ return ˇ1;
+ });
+ "},
+ Mode::Normal,
+ );
+ cx.simulate_keystrokes("v a f");
+ cx.assert_state(
+ indoc! {"
+ arr.map(«() => {
+ return 1;
+ }ˇ»);
+ "},
+ Mode::VisualLine,
+ );
+
+ cx.set_state(
+ indoc! {"
+ const foo = () => {
+ return ˇ1;
+ };
+ "},
+ Mode::Normal,
+ );
+ cx.simulate_keystrokes("v i f");
+ cx.assert_state(
+ indoc! {"
+ const foo = () => {
+ «return 1;ˇ»
+ };
+ "},
+ Mode::Visual,
+ );
+
+ cx.set_state(
+ indoc! {"
+ (() => {
+ console.log(ˇ1);
+ })();
+ "},
+ Mode::Normal,
+ );
+ cx.simulate_keystrokes("v a f");
+ cx.assert_state(
+ indoc! {"
+ («() => {
+ console.log(1);
+ }ˇ»)();
+ "},
+ Mode::VisualLine,
+ );
+
+ cx.set_state(
+ indoc! {"
+ const foo = () => {
+ return ˇ1;
+ };
+ export { foo };
+ "},
+ Mode::Normal,
+ );
+ cx.simulate_keystrokes("v a f");
+ cx.assert_state(
+ indoc! {"
+ «const foo = () => {
+ return 1;
+ };ˇ»
+ export { foo };
+ "},
+ Mode::VisualLine,
+ );
+
+ cx.set_state(
+ indoc! {"
+ let bar = () => {
+ return ˇ2;
+ };
+ "},
+ Mode::Normal,
+ );
+ cx.simulate_keystrokes("v a f");
+ cx.assert_state(
+ indoc! {"
+ «let bar = () => {
+ return 2;
+ };ˇ»
+ "},
+ Mode::VisualLine,
+ );
+
+ cx.set_state(
+ indoc! {"
+ var baz = () => {
+ return ˇ3;
+ };
+ "},
+ Mode::Normal,
+ );
+ cx.simulate_keystrokes("v a f");
+ cx.assert_state(
+ indoc! {"
+ «var baz = () => {
+ return 3;
+ };ˇ»
+ "},
+ Mode::VisualLine,
+ );
+
+ cx.set_state(
+ indoc! {"
+ const add = (a, b) => a + ˇb;
+ "},
+ Mode::Normal,
+ );
+ cx.simulate_keystrokes("v a f");
+ cx.assert_state(
+ indoc! {"
+ «const add = (a, b) => a + b;ˇ»
+ "},
+ Mode::VisualLine,
+ );
+
+ cx.set_state(
+ indoc! {"
+ const add = ˇ(a, b) => a + b;
+ "},
+ Mode::Normal,
+ );
+ cx.simulate_keystrokes("v a f");
+ cx.assert_state(
+ indoc! {"
+ «const add = (a, b) => a + b;ˇ»
+ "},
+ Mode::VisualLine,
+ );
+
+ cx.set_state(
+ indoc! {"
+ const add = (a, b) => a + bˇ;
+ "},
+ Mode::Normal,
+ );
+ cx.simulate_keystrokes("v a f");
+ cx.assert_state(
+ indoc! {"
+ «const add = (a, b) => a + b;ˇ»
+ "},
+ Mode::VisualLine,
+ );
+
+ cx.set_state(
+ indoc! {"
+ const add = (a, b) =ˇ> a + b;
+ "},
+ Mode::Normal,
+ );
+ cx.simulate_keystrokes("v a f");
+ cx.assert_state(
+ indoc! {"
+ «const add = (a, b) => a + b;ˇ»
+ "},
+ Mode::VisualLine,
+ );
+ }
+
+ #[gpui::test]
+ async fn test_arrow_function_in_jsx(cx: &mut gpui::TestAppContext) {
+ let mut cx = VimTestContext::new_tsx(cx).await;
+
+ cx.set_state(
+ indoc! {r#"
+ export const MyComponent = () => {
+ return (
+ <div>
+ <div onClick={() => {
+ alert("Hello world!");
+ console.log(ˇ"clicked");
+ }}>Hello world!</div>
+ </div>
+ );
+ };
+ "#},
+ Mode::Normal,
+ );
+ cx.simulate_keystrokes("v a f");
+ cx.assert_state(
+ indoc! {r#"
+ export const MyComponent = () => {
+ return (
+ <div>
+ <div onClick={«() => {
+ alert("Hello world!");
+ console.log("clicked");
+ }ˇ»}>Hello world!</div>
+ </div>
+ );
+ };
+ "#},
+ Mode::VisualLine,
+ );
+
+ cx.set_state(
+ indoc! {r#"
+ export const MyComponent = () => {
+ return (
+ <div>
+ <div onClick={() => console.log("clickˇed")}>Hello world!</div>
+ </div>
+ );
+ };
+ "#},
+ Mode::Normal,
+ );
+ cx.simulate_keystrokes("v a f");
+ cx.assert_state(
+ indoc! {r#"
+ export const MyComponent = () => {
+ return (
+ <div>
+ <div onClick={«() => console.log("clicked")ˇ»}>Hello world!</div>
+ </div>
+ );
+ };
+ "#},
+ Mode::VisualLine,
+ );
+
+ cx.set_state(
+ indoc! {r#"
+ export const MyComponent = () => {
+ return (
+ <div>
+ <div onClick={ˇ() => console.log("clicked")}>Hello world!</div>
+ </div>
+ );
+ };
+ "#},
+ Mode::Normal,
+ );
+ cx.simulate_keystrokes("v a f");
+ cx.assert_state(
+ indoc! {r#"
+ export const MyComponent = () => {
+ return (
+ <div>
+ <div onClick={«() => console.log("clicked")ˇ»}>Hello world!</div>
+ </div>
+ );
+ };
+ "#},
+ Mode::VisualLine,
+ );
+
+ cx.set_state(
+ indoc! {r#"
+ export const MyComponent = () => {
+ return (
+ <div>
+ <div onClick={() => console.log("clicked"ˇ)}>Hello world!</div>
+ </div>
+ );
+ };
+ "#},
+ Mode::Normal,
+ );
+ cx.simulate_keystrokes("v a f");
+ cx.assert_state(
+ indoc! {r#"
+ export const MyComponent = () => {
+ return (
+ <div>
+ <div onClick={«() => console.log("clicked")ˇ»}>Hello world!</div>
+ </div>
+ );
+ };
+ "#},
+ Mode::VisualLine,
+ );
+
+ cx.set_state(
+ indoc! {r#"
+ export const MyComponent = () => {
+ return (
+ <div>
+ <div onClick={() =ˇ> console.log("clicked")}>Hello world!</div>
+ </div>
+ );
+ };
+ "#},
+ Mode::Normal,
+ );
+ cx.simulate_keystrokes("v a f");
+ cx.assert_state(
+ indoc! {r#"
+ export const MyComponent = () => {
+ return (
+ <div>
+ <div onClick={«() => console.log("clicked")ˇ»}>Hello world!</div>
+ </div>
+ );
+ };
+ "#},
+ Mode::VisualLine,
+ );
+
+ cx.set_state(
+ indoc! {r#"
+ export const MyComponent = () => {
+ return (
+ <div>
+ <div onClick={() => {
+ console.log("cliˇcked");
+ }}>Hello world!</div>
+ </div>
+ );
+ };
+ "#},
+ Mode::Normal,
+ );
+ cx.simulate_keystrokes("v a f");
+ cx.assert_state(
+ indoc! {r#"
+ export const MyComponent = () => {
+ return (
+ <div>
+ <div onClick={«() => {
+ console.log("clicked");
+ }ˇ»}>Hello world!</div>
+ </div>
+ );
+ };
+ "#},
+ Mode::VisualLine,
+ );
+
+ cx.set_state(
+ indoc! {r#"
+ export const MyComponent = () => {
+ return (
+ <div>
+ <div onClick={() => fˇoo()}>Hello world!</div>
+ </div>
+ );
+ };
+ "#},
+ Mode::Normal,
+ );
+ cx.simulate_keystrokes("v a f");
+ cx.assert_state(
+ indoc! {r#"
+ export const MyComponent = () => {
+ return (
+ <div>
+ <div onClick={«() => foo()ˇ»}>Hello world!</div>
+ </div>
+ );
+ };
+ "#},
+ Mode::VisualLine,
+ );
+ }
}
@@ -522,12 +522,16 @@ impl Vim {
selection.start = original_point.to_display_point(map)
}
} else {
- selection.end = movement::saturating_right(
- map,
- original_point.to_display_point(map),
- );
- if original_point.column > 0 {
- selection.reversed = true
+ let original_display_point =
+ original_point.to_display_point(map);
+ if selection.end <= original_display_point {
+ selection.end = movement::saturating_right(
+ map,
+ original_display_point,
+ );
+ if original_point.column > 0 {
+ selection.reversed = true
+ }
}
}
}