Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 20 additions & 9 deletions crates/editor/src/editor.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8659,19 +8659,30 @@ impl Editor {
cx: &mut Context<Self>,
) {
let selection = self.selections.newest::<Point>(&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 {
Expand Down
192 changes: 192 additions & 0 deletions crates/editor/src/editor_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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, |_| {});
Expand Down
Loading