diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index a8ec9667ca8402..55969bd78b162b 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -8659,19 +8659,30 @@ impl Editor { cx: &mut Context, ) { let selection = self.selections.newest::(&self.display_snapshot(cx)); + let multi_buffer_snapshot = self.buffer.read(cx).snapshot(cx); + + if let Some(file_location) = maybe!({ + // A selection that spans multiple buffers has no single location, + // so fall back to the buffer the cursor is in. + let (buffer, range) = multi_buffer_snapshot + .range_to_buffer_range(selection.range()) + .or_else(|| { + let (buffer, point) = + multi_buffer_snapshot.point_to_buffer_point(selection.head())?; + Some((buffer, point..point)) + })?; - let start_line = selection.start.row + 1; - let end_line = selection.end.row + 1; + let start_line = range.start.row + 1; + let end_line = range.end.row + 1; - let end_line = if selection.end.column == 0 && end_line > start_line { - end_line - 1 - } else { - end_line - }; + let end_line = if range.end.column == 0 && end_line > start_line { + end_line - 1 + } else { + end_line + }; - if let Some(file_location) = self.active_buffer(cx).and_then(|buffer| { let project = self.project()?.read(cx); - let file = buffer.read(cx).file()?; + let file = buffer.file()?; let path = file.path().display(project.path_style(cx)); let location = if start_line == end_line { diff --git a/crates/editor/src/editor_tests.rs b/crates/editor/src/editor_tests.rs index 2a4ac54cf59fc4..50eb4aedce5918 100644 --- a/crates/editor/src/editor_tests.rs +++ b/crates/editor/src/editor_tests.rs @@ -9853,6 +9853,198 @@ async fn test_clipboard_line_numbers_from_multibuffer(cx: &mut TestAppContext) { ); } +#[gpui::test] +async fn test_copy_file_location_from_multibuffer(cx: &mut TestAppContext) { + init_test(cx, |_| {}); + + let fs = FakeFs::new(cx.executor()); + fs.insert_tree( + path!("/root"), + json!({ + "file.txt": "first line\nsecond line\nthird line\nfourth line\nfifth line\n", + }), + ) + .await; + + let project = Project::test(fs, [path!("/root").as_ref()], cx).await; + + let buffer = project + .update(cx, |project, cx| { + project.open_local_buffer(path!("/root/file.txt"), cx) + }) + .await + .unwrap(); + + let multibuffer = cx.new(|cx| { + let mut multibuffer = MultiBuffer::new(ReadWrite); + multibuffer.set_excerpts_for_path( + PathKey::sorted(0), + buffer.clone(), + [Point::new(2, 0)..Point::new(5, 0)], + 0, + cx, + ); + multibuffer + }); + + let (editor, cx) = cx.add_window_view(|window, cx| { + build_editor_with_project(project.clone(), multibuffer, window, cx) + }); + + editor.update_in(cx, |editor, window, cx| { + editor.change_selections(Default::default(), window, cx, |selections| { + selections.select_ranges([Point::new(0, 0)..Point::new(0, 0)]); + }); + editor.copy_file_location(&CopyFileLocation, window, cx); + }); + assert_eq!( + cx.read_from_clipboard().and_then(|item| item.text()), + Some("file.txt:3".to_string()), + "cursor on the first excerpt row should report its line in the original file" + ); + + editor.update_in(cx, |editor, window, cx| { + editor.change_selections(Default::default(), window, cx, |selections| { + selections.select_ranges([Point::new(1, 0)..Point::new(2, 0)]); + }); + editor.copy_file_location(&CopyFileLocation, window, cx); + }); + assert_eq!( + cx.read_from_clipboard().and_then(|item| item.text()), + Some("file.txt:4".to_string()), + "a selection ending at the start of a row should not include that row" + ); + + editor.update_in(cx, |editor, window, cx| { + editor.change_selections(Default::default(), window, cx, |selections| { + selections.select_ranges([Point::new(0, 0)..Point::new(2, 3)]); + }); + editor.copy_file_location(&CopyFileLocation, window, cx); + }); + assert_eq!( + cx.read_from_clipboard().and_then(|item| item.text()), + Some("file.txt:3-5".to_string()), + "a multi-row selection should report the original file's line range" + ); +} + +#[gpui::test] +async fn test_copy_file_location_across_buffers(cx: &mut TestAppContext) { + init_test(cx, |_| {}); + + let fs = FakeFs::new(cx.executor()); + fs.insert_tree( + path!("/root"), + json!({ + "one.txt": "one\ntwo\nthree\n", + "two.txt": "four\nfive\nsix\n", + }), + ) + .await; + + let project = Project::test(fs, [path!("/root").as_ref()], cx).await; + + let buffer_1 = project + .update(cx, |project, cx| { + project.open_local_buffer(path!("/root/one.txt"), cx) + }) + .await + .unwrap(); + let buffer_2 = project + .update(cx, |project, cx| { + project.open_local_buffer(path!("/root/two.txt"), cx) + }) + .await + .unwrap(); + + let multibuffer = cx.new(|cx| { + let mut multibuffer = MultiBuffer::new(ReadWrite); + multibuffer.set_excerpts_for_path( + PathKey::sorted(0), + buffer_1, + [Point::new(1, 0)..Point::new(3, 0)], + 0, + cx, + ); + multibuffer.set_excerpts_for_path( + PathKey::sorted(1), + buffer_2, + [Point::new(1, 0)..Point::new(3, 0)], + 0, + cx, + ); + multibuffer + }); + + let (editor, cx) = cx.add_window_view(|window, cx| { + build_editor_with_project(project.clone(), multibuffer, window, cx) + }); + + // The multi-buffer reads "two\nthree\n\nfive\nsix\n", so row 3 is the first + // row of the second buffer's excerpt. Select from the first excerpt into it. + editor.update_in(cx, |editor, window, cx| { + editor.change_selections(Default::default(), window, cx, |selections| { + selections.select_ranges([Point::new(0, 0)..Point::new(3, 2)]); + }); + editor.copy_file_location(&CopyFileLocation, window, cx); + }); + assert_eq!( + cx.read_from_clipboard().and_then(|item| item.text()), + Some("two.txt:2".to_string()), + "a selection spanning buffers should report the location of the cursor" + ); +} + +#[gpui::test] +async fn test_copy_file_location_in_singleton_buffer(cx: &mut TestAppContext) { + init_test(cx, |_| {}); + + let fs = FakeFs::new(cx.executor()); + fs.insert_tree( + path!("/root"), + json!({ + "file.txt": "first line\nsecond line\nthird line\n", + }), + ) + .await; + + let project = Project::test(fs, [path!("/root").as_ref()], cx).await; + + let buffer = project + .update(cx, |project, cx| { + project.open_local_buffer(path!("/root/file.txt"), cx) + }) + .await + .unwrap(); + let multibuffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx)); + + let (editor, cx) = cx.add_window_view(|window, cx| { + build_editor_with_project(project.clone(), multibuffer, window, cx) + }); + + editor.update_in(cx, |editor, window, cx| { + editor.change_selections(Default::default(), window, cx, |selections| { + selections.select_ranges([Point::new(1, 0)..Point::new(1, 0)]); + }); + editor.copy_file_location(&CopyFileLocation, window, cx); + }); + assert_eq!( + cx.read_from_clipboard().and_then(|item| item.text()), + Some("file.txt:2".to_string()) + ); + + editor.update_in(cx, |editor, window, cx| { + editor.change_selections(Default::default(), window, cx, |selections| { + selections.select_ranges([Point::new(0, 0)..Point::new(2, 4)]); + }); + editor.copy_file_location(&CopyFileLocation, window, cx); + }); + assert_eq!( + cx.read_from_clipboard().and_then(|item| item.text()), + Some("file.txt:1-3".to_string()) + ); +} + #[gpui::test] async fn test_paste_multiline(cx: &mut TestAppContext) { init_test(cx, |_| {});