diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index ed3fb5ccf8fe915718c012d208deb8fb32a4615e..6ac1e351886ff3558a5f132ec37790f21b2dea3b 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -18219,6 +18219,20 @@ impl Editor { for ranges in locations.values_mut() { ranges.sort_by_key(|range| (range.start, Reverse(range.end))); ranges.dedup(); + // Merge overlapping or contained ranges. After sorting by + // (start, Reverse(end)), we can merge in a single pass: + // if the next range starts before the current one ends, + // extend the current range's end if needed. + let mut i = 0; + while i + 1 < ranges.len() { + if ranges[i + 1].start <= ranges[i].end { + let merged_end = ranges[i].end.max(ranges[i + 1].end); + ranges[i].end = merged_end; + ranges.remove(i + 1); + } else { + i += 1; + } + } let fits_in_one_excerpt = ranges .iter() .tuple_windows() diff --git a/crates/editor/src/editor_tests.rs b/crates/editor/src/editor_tests.rs index 2ba12a2ff18a4d6a01d52f7c2a61c682942f9a4d..eb8b59a511ee580e0bc6f2ba0c7ce9a5ebdccb48 100644 --- a/crates/editor/src/editor_tests.rs +++ b/crates/editor/src/editor_tests.rs @@ -25403,6 +25403,55 @@ async fn test_goto_definition_far_ranges_open_multibuffer(cx: &mut TestAppContex }); } +#[gpui::test] +async fn test_goto_definition_contained_ranges(cx: &mut TestAppContext) { + init_test(cx, |_| {}); + let mut cx = EditorLspTestContext::new_rust( + lsp::ServerCapabilities { + definition_provider: Some(lsp::OneOf::Left(true)), + ..lsp::ServerCapabilities::default() + }, + cx, + ) + .await; + + // The LSP returns two single-line definitions on the same row where one + // range contains the other. Both are on the same line so the + // `fits_in_one_excerpt` check won't underflow, and the code reaches + // `change_selections`. + cx.set_state( + &r#"fn caller() { + let _ = ˇtarget(); + } + fn target_outer() { fn target_inner() {} } + "# + .unindent(), + ); + + // Return two definitions on the same line: an outer range covering the + // whole line and an inner range for just the inner function name. + cx.set_request_handler::(move |url, _, _| async move { + Ok(Some(lsp::GotoDefinitionResponse::Array(vec![ + // Inner range: just "target_inner" (cols 23..35) + lsp::Location { + uri: url.clone(), + range: lsp::Range::new(lsp::Position::new(3, 23), lsp::Position::new(3, 35)), + }, + // Outer range: the whole line (cols 0..48) + lsp::Location { + uri: url, + range: lsp::Range::new(lsp::Position::new(3, 0), lsp::Position::new(3, 48)), + }, + ]))) + }); + + let navigated = cx + .update_editor(|editor, window, cx| editor.go_to_definition(&GoToDefinition, window, cx)) + .await + .expect("Failed to navigate to definitions"); + assert_eq!(navigated, Navigated::Yes); +} + #[gpui::test] async fn test_find_all_references_editor_reuse(cx: &mut TestAppContext) { init_test(cx, |_| {});