diff --git a/crates/editor/src/test/editor_lsp_test_context.rs b/crates/editor/src/test/editor_lsp_test_context.rs index b1b04f01839c21f5f6522723b8e8600f721e681a..a085221e71cc52e43fac3b652b57f2360e99fa14 100644 --- a/crates/editor/src/test/editor_lsp_test_context.rs +++ b/crates/editor/src/test/editor_lsp_test_context.rs @@ -262,6 +262,77 @@ impl EditorLspTestContext { Self::new(language, capabilities, cx).await } + pub async fn new_tsx( + capabilities: lsp::ServerCapabilities, + cx: &mut gpui::TestAppContext, + ) -> EditorLspTestContext { + let mut word_characters: HashSet = Default::default(); + word_characters.insert('$'); + word_characters.insert('#'); + let language = Language::new( + LanguageConfig { + name: "TSX".into(), + matcher: LanguageMatcher { + path_suffixes: vec!["tsx".to_string()], + ..Default::default() + }, + brackets: language::BracketPairConfig { + pairs: vec![language::BracketPair { + start: "{".to_string(), + end: "}".to_string(), + close: true, + surround: true, + newline: true, + }], + disabled_scopes_by_bracket_ix: Default::default(), + }, + word_characters, + ..Default::default() + }, + Some(tree_sitter_typescript::LANGUAGE_TSX.into()), + ) + .with_queries(LanguageQueries { + brackets: Some(Cow::from(indoc! {r#" + ("(" @open ")" @close) + ("[" @open "]" @close) + ("{" @open "}" @close) + ("<" @open ">" @close) + ("<" @open "/>" @close) + ("" @close) + ("\"" @open "\"" @close) + ("'" @open "'" @close) + ("`" @open "`" @close) + ((jsx_element (jsx_opening_element) @open (jsx_closing_element) @close) (#set! newline.only))"#})), + indents: Some(Cow::from(indoc! {r#" + [ + (call_expression) + (assignment_expression) + (member_expression) + (lexical_declaration) + (variable_declaration) + (assignment_expression) + (if_statement) + (for_statement) + ] @indent + + (_ "[" "]" @end) @indent + (_ "<" ">" @end) @indent + (_ "{" "}" @end) @indent + (_ "(" ")" @end) @indent + + (jsx_opening_element ">" @end) @indent + + (jsx_element + (jsx_opening_element) @start + (jsx_closing_element)? @end) @indent + "#})), + ..Default::default() + }) + .expect("Could not parse queries"); + + Self::new(language, capabilities, cx).await + } + pub async fn new_html(cx: &mut gpui::TestAppContext) -> Self { let language = Language::new( LanguageConfig { diff --git a/crates/vim/src/motion.rs b/crates/vim/src/motion.rs index d1d645d3854aa9e108f0233ed511b4beea841eee..666d2573a53cbf74ed1c2edee02c8561167038c3 100644 --- a/crates/vim/src/motion.rs +++ b/crates/vim/src/motion.rs @@ -2388,6 +2388,7 @@ fn matching(map: &DisplaySnapshot, display_point: DisplayPoint) -> DisplayPoint let display_point = map.clip_at_line_end(display_point); let point = display_point.to_point(map); let offset = point.to_offset(&map.buffer_snapshot()); + let snapshot = map.buffer_snapshot(); // Ensure the range is contained by the current line. let mut line_end = map.next_line_boundary(point).0; @@ -2395,10 +2396,19 @@ fn matching(map: &DisplaySnapshot, display_point: DisplayPoint) -> DisplayPoint line_end = map.max_point().to_point(map); } - if let Some((opening_range, closing_range)) = map - .buffer_snapshot() - .innermost_enclosing_bracket_ranges(offset..offset, None) - { + // Attempt to find the smallest enclosing bracket range that also contains + // the offset, which only happens if the cursor is currently in a bracket. + let range_filter = |_buffer: &language::BufferSnapshot, + opening_range: Range, + closing_range: Range| { + opening_range.contains(&offset) || closing_range.contains(&offset) + }; + + let bracket_ranges = snapshot + .innermost_enclosing_bracket_ranges(offset..offset, Some(&range_filter)) + .or_else(|| snapshot.innermost_enclosing_bracket_ranges(offset..offset, None)); + + if let Some((opening_range, closing_range)) = bracket_ranges { if opening_range.contains(&offset) { return closing_range.start.to_display_point(map); } else if closing_range.contains(&offset) { @@ -2440,7 +2450,6 @@ fn matching(map: &DisplaySnapshot, display_point: DisplayPoint) -> DisplayPoint if distance < closest_distance { closest_pair_destination = Some(close_range.start); closest_distance = distance; - continue; } } @@ -2451,7 +2460,6 @@ fn matching(map: &DisplaySnapshot, display_point: DisplayPoint) -> DisplayPoint if distance < closest_distance { closest_pair_destination = Some(open_range.start); closest_distance = distance; - continue; } } @@ -3391,6 +3399,22 @@ mod test { }"}); } + #[gpui::test] + async fn test_matching_nested_brackets(cx: &mut gpui::TestAppContext) { + let mut cx = NeovimBackedTestContext::new_tsx(cx).await; + + cx.set_shared_state(indoc! {r""}) + .await; + cx.simulate_shared_keystrokes("%").await; + cx.shared_state() + .await + .assert_eq(indoc! {r""}); + cx.simulate_shared_keystrokes("%").await; + cx.shared_state() + .await + .assert_eq(indoc! {r""}); + } + #[gpui::test] async fn test_comma_semicolon(cx: &mut gpui::TestAppContext) { let mut cx = NeovimBackedTestContext::new(cx).await; diff --git a/crates/vim/src/test/neovim_backed_test_context.rs b/crates/vim/src/test/neovim_backed_test_context.rs index bc4d47d8ea1e7c81f5bdf6164f8b51070fb9b96f..9d2452ab20a6a99138c4b0d86f597f084a0876d6 100644 --- a/crates/vim/src/test/neovim_backed_test_context.rs +++ b/crates/vim/src/test/neovim_backed_test_context.rs @@ -207,6 +207,26 @@ impl NeovimBackedTestContext { } } + pub async fn new_tsx(cx: &mut gpui::TestAppContext) -> NeovimBackedTestContext { + #[cfg(feature = "neovim")] + cx.executor().allow_parking(); + let thread = thread::current(); + let test_name = thread + .name() + .expect("thread is not named") + .split(':') + .next_back() + .unwrap() + .to_string(); + Self { + cx: VimTestContext::new_tsx(cx).await, + neovim: NeovimConnection::new(test_name).await, + + last_set_state: None, + recent_keystrokes: Default::default(), + } + } + pub async fn set_shared_state(&mut self, marked_text: &str) { let mode = if marked_text.contains('»') { Mode::Visual diff --git a/crates/vim/src/test/vim_test_context.rs b/crates/vim/src/test/vim_test_context.rs index a2db0493d99190bc7355a5af5a0687befcd02f63..8dfc0c392d98073746e894bd4569f0edbf19e469 100644 --- a/crates/vim/src/test/vim_test_context.rs +++ b/crates/vim/src/test/vim_test_context.rs @@ -66,6 +66,28 @@ impl VimTestContext { ) } + pub async fn new_tsx(cx: &mut gpui::TestAppContext) -> VimTestContext { + Self::init(cx); + Self::new_with_lsp( + EditorLspTestContext::new_tsx( + lsp::ServerCapabilities { + completion_provider: Some(lsp::CompletionOptions { + trigger_characters: Some(vec![".".to_string()]), + ..Default::default() + }), + rename_provider: Some(lsp::OneOf::Right(lsp::RenameOptions { + prepare_provider: Some(true), + work_done_progress_options: Default::default(), + })), + ..Default::default() + }, + cx, + ) + .await, + true, + ) + } + pub fn init_keybindings(enabled: bool, cx: &mut App) { SettingsStore::update_global(cx, |store, cx| { store.update_user_settings(cx, |s| s.vim_mode = Some(enabled)); diff --git a/crates/vim/test_data/test_matching_nested_brackets.json b/crates/vim/test_data/test_matching_nested_brackets.json new file mode 100644 index 0000000000000000000000000000000000000000..d90b38416e62d1824779cd7a1cb194670fdb00ab --- /dev/null +++ b/crates/vim/test_data/test_matching_nested_brackets.json @@ -0,0 +1,5 @@ +{"Put":{"state":""}} +{"Key":"%"} +{"Get":{"state":"","mode":"Normal"}} +{"Key":"%"} +{"Get":{"state":"","mode":"Normal"}}