From 56402651609746639145a2e3a5b11f53d91be02a Mon Sep 17 00:00:00 2001 From: Smit Barmase Date: Tue, 6 May 2025 14:43:28 +0530 Subject: [PATCH] language: Fix larger syntax node when cursor is at end of word or line (#29978) Closes #28699 Fixes two cases in the `editor::SelectLargerSyntaxNode` action: 1. When cursor is at the end of a word, it now selects that word first instead of selecting the whole line. 2. When cursor is at the end of a line, it now selects that line first instead of selecting the whole code block. Before and After: https://github.com/user-attachments/assets/233b891e-15f1-4f10-a51f-75693323c2bd Release Notes: - Fixed `editor::SelectLargerSyntaxNode` to properly select nodes when the cursor is positioned at the end of words or lines. --- crates/editor/src/editor_tests.rs | 62 +++++++++++++++++++++++++++++++ crates/language/src/buffer.rs | 12 +++++- 2 files changed, 72 insertions(+), 2 deletions(-) diff --git a/crates/editor/src/editor_tests.rs b/crates/editor/src/editor_tests.rs index b45675dda6..b9623d2acf 100644 --- a/crates/editor/src/editor_tests.rs +++ b/crates/editor/src/editor_tests.rs @@ -6402,6 +6402,68 @@ async fn test_select_larger_smaller_syntax_node(cx: &mut TestAppContext) { }); } +#[gpui::test] +async fn test_select_larger_syntax_node_for_cursor_at_end(cx: &mut TestAppContext) { + init_test(cx, |_| {}); + + let language = Arc::new(Language::new( + LanguageConfig::default(), + Some(tree_sitter_rust::LANGUAGE.into()), + )); + + let text = "let a = 2;"; + + let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx)); + let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx)); + let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx)); + + editor + .condition::(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx)) + .await; + + // Test case 1: Cursor at end of word + editor.update_in(cx, |editor, window, cx| { + editor.change_selections(None, window, cx, |s| { + s.select_display_ranges([ + DisplayPoint::new(DisplayRow(0), 5)..DisplayPoint::new(DisplayRow(0), 5) + ]); + }); + }); + editor.update(cx, |editor, cx| { + assert_text_with_selections(editor, "let aˇ = 2;", cx); + }); + editor.update_in(cx, |editor, window, cx| { + editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx); + }); + editor.update(cx, |editor, cx| { + assert_text_with_selections(editor, "let «ˇa» = 2;", cx); + }); + editor.update_in(cx, |editor, window, cx| { + editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx); + }); + editor.update(cx, |editor, cx| { + assert_text_with_selections(editor, "«ˇlet a = 2;»", cx); + }); + + // Test case 2: Cursor at end of statement + editor.update_in(cx, |editor, window, cx| { + editor.change_selections(None, window, cx, |s| { + s.select_display_ranges([ + DisplayPoint::new(DisplayRow(0), 11)..DisplayPoint::new(DisplayRow(0), 11) + ]); + }); + }); + editor.update(cx, |editor, cx| { + assert_text_with_selections(editor, "let a = 2;ˇ", cx); + }); + editor.update_in(cx, |editor, window, cx| { + editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx); + }); + editor.update(cx, |editor, cx| { + assert_text_with_selections(editor, "«ˇlet a = 2;»", cx); + }); +} + #[gpui::test] async fn test_select_larger_smaller_syntax_node_for_string(cx: &mut TestAppContext) { init_test(cx, |_| {}); diff --git a/crates/language/src/buffer.rs b/crates/language/src/buffer.rs index 98b577ea39..b40d889d17 100644 --- a/crates/language/src/buffer.rs +++ b/crates/language/src/buffer.rs @@ -3307,12 +3307,20 @@ impl BufferSnapshot { { let mut cursor = layer.node().walk(); - // Descend to the first leaf that touches the start of the range, - // and if the range is non-empty, extends beyond the start. + // Descend to the first leaf that touches the start of the range. + // + // If the range is non-empty and the current node ends exactly at the start, + // move to the next sibling to find a node that extends beyond the start. + // + // If the range is empty and the current node starts after the range position, + // move to the previous sibling to find the node that contains the position. while cursor.goto_first_child_for_byte(range.start).is_some() { if !range.is_empty() && cursor.node().end_byte() == range.start { cursor.goto_next_sibling(); } + if range.is_empty() && cursor.node().start_byte() > range.start { + cursor.goto_previous_sibling(); + } } // Ascend to the smallest ancestor that strictly contains the range.