vim: Make vaf include const for arrow functions in JS/TS/TSX (#45327)

Closes #24264

Release Notes:

- N/A *or* Added/Fixed/Improved ...
This commit is contained in:
Ben Kunkle
2025-12-18 20:56:47 -06:00
committed by GitHub
parent e0ff995e2d
commit 435d4c5f24
8 changed files with 742 additions and 20 deletions

View File

@@ -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");

View File

@@ -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]

View File

@@ -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>>,

View File

@@ -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: (_

View File

@@ -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

View File

@@ -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

View File

@@ -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,
);
}
}

View File

@@ -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
}
}
}
}