C14.13: a search hit puts the caret on the match, not the line start

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
ZacharyZhang-NYandClaude Opus 5.5 committed 2026-10-12 09:33:02 -04:00
1 parent 584e08d8ad
commit 10b9e7b694
8 files changed
+121 -24

No files matched your search

+13 -8
View File
@@ -36,8 +36,8 @@ pub struct Desk {
/// Files and folders opened lately, newest first.
pub recent: Vec<PathBuf>,
pub recent_file: PathBuf,
/// A file asked to open at a line.
pub going: Option<(PathBuf, usize)>,
/// A file asked to open at a line and column.
pub going: Option<(PathBuf, (usize, usize))>,
/// The path each tab last put in the recent list.
pub remembered: HashMap<u64, PathBuf>,
pub(crate) side: Entity<crate::side::Side>,
@@ -140,9 +140,9 @@ impl Host for Desk {
.going
.as_ref()
.is_some_and(|(file, _)| Some(file) == real.as_ref())
&& let Some((_, line)) = self.going.take()
&& let Some((_, at)) = self.going.take()
{
self.reveal_later(view.clone(), line, cx);
self.reveal_later(view.clone(), at, cx);
}
}
cx.notify();
@@ -337,14 +337,19 @@ impl Desk {
}
}
/// Brings `line` into view from outside a window update.
pub fn reveal_later(&mut self, view: Entity<MarkView>, line: usize, cx: &mut Context<Self>) {
/// Shows line and column `at` from outside an update.
pub fn reveal_later(
&mut self,
view: Entity<MarkView>,
at: (usize, usize),
cx: &mut Context<Self>,
) {
cx.spawn(async move |desk, cx| {
let shown = ely_office::in_window(&desk, cx, |_, window, cx| {
view.update(cx, |view, cx| view.reveal(line, window, cx))
view.update(cx, |view, cx| view.reveal(at, window, cx))
});
if let Err(error) = shown {
log::warn!("mark: line {line} was not shown: {error:#}");
log::warn!("mark: line {} was not shown: {error:#}", at.0);
}
})
.detach();
+1 -1
View File
@@ -68,7 +68,7 @@ fn a_file_opens_with_its_outline_and_joins_the_recent_list(cx: &mut TestAppConte
[(1, "One".into(), 0), (2, "Two".into(), 4)]
);
let view = desk.read_with(cx, |desk, _| desk.active_view().expect("a tab"));
view.update_in(cx, |view, window, cx| view.reveal(4, window, cx));
view.update_in(cx, |view, window, cx| view.reveal((4, 0), window, cx));
cx.run_until_parked();
assert_eq!(
view.read_with(cx, |view, cx| view.place(cx).line),
+27 -3
View File
@@ -49,9 +49,24 @@ pub struct Hit {
pub file: PathBuf,
/// Counted from one.
pub line: usize,
/// The first match, in characters from zero.
pub column: usize,
pub text: String,
}
/// Where lowercase `query` starts in `line`, in characters.
fn column_of(line: &str, query: &str) -> Option<usize> {
// Each lowered byte keeps its character's place.
let (mut lowered, mut places) = (String::new(), Vec::new());
for (place, ch) in line.chars().enumerate() {
for low in ch.to_lowercase() {
lowered.push(low);
places.extend(std::iter::repeat_n(place, low.len_utf8()));
}
}
lowered.find(query).map(|at| places[at])
}
/// Lines of `text` holding `query`, any case.
pub fn lines_with(file: &Path, text: &str, query: &str, hits: &mut Vec<Hit>) {
let query = query.to_lowercase();
@@ -59,11 +74,12 @@ pub fn lines_with(file: &Path, text: &str, query: &str, hits: &mut Vec<Hit>) {
if hits.len() == MOST_HITS {
return;
}
if line.to_lowercase().contains(&query) {
if let Some(column) = column_of(line, &query) {
let shown: String = line.trim().chars().take(160).collect();
hits.push(Hit {
file: file.to_path_buf(),
line: at + 1,
column,
text: shown,
});
}
@@ -108,8 +124,16 @@ mod tests {
);
let lines: Vec<_> = hits
.iter()
.map(|hit| (hit.line, hit.text.as_str()))
.map(|hit| (hit.line, hit.column, hit.text.as_str()))
.collect();
assert_eq!(lines, [(2, "some Words here"), (4, "WORDS")]);
assert_eq!(lines, [(2, 5, "some Words here"), (4, 0, "WORDS")]);
}
#[test]
fn the_column_counts_characters_through_case_changes() {
// İ lowers to two characters.
assert_eq!(super::column_of("İİ née Word", "word"), Some(7));
assert_eq!(super::column_of("中文 搜索", "搜索"), Some(3));
assert_eq!(super::column_of("none", "word"), None);
}
}
+4 -4
View File
@@ -108,12 +108,12 @@ impl Desk {
}
}
/// Line `line` in `tab`, or in `file` once open.
/// Line and column `at` in a tab, or a file once open.
pub fn open_at(
&mut self,
tab: Option<u64>,
file: PathBuf,
line: usize,
at: (usize, usize),
window: &mut Window,
cx: &mut Context<Self>,
) {
@@ -123,14 +123,14 @@ impl Desk {
if let Some(view) = self.tab(id).map(|open| open.view.clone()) {
// After the list's own key handling, which keeps focus.
window.defer(cx, move |window, cx| {
view.update(cx, |view, cx| view.reveal(line, window, cx))
view.update(cx, |view, cx| view.reveal(at, window, cx))
});
}
return cx.notify();
}
// A file gone is reported by the open itself.
if let Ok(real) = std::fs::canonicalize(&file) {
self.going = Some((real, line));
self.going = Some((real, at));
}
self.open_file(file, cx);
}
+3 -3
View File
@@ -256,7 +256,7 @@ impl Side {
};
// The heading in view, the caret on it.
let line = heading.line;
view.update(cx, |view, cx| view.reveal(line, window, cx));
view.update(cx, |view, cx| view.reveal((line, 0), window, cx));
});
ScrollArea::new("mark-outline-scroll")
.vertical()
@@ -317,9 +317,9 @@ impl Side {
return;
};
// Hits count lines from one.
let (tab, file, line) = (*tab, hit.file.clone(), hit.line - 1);
let (tab, file, at) = (*tab, hit.file.clone(), (hit.line - 1, hit.column));
opener
.update(cx, |desk, cx| desk.open_at(tab, file, line, window, cx))
.update(cx, |desk, cx| desk.open_at(tab, file, at, window, cx))
.ok();
});
let more = (self.hits.len() == MOST_HITS)
+66
View File
@@ -45,3 +45,69 @@ fn a_long_outline_scrolls_to_its_last_heading(cx: &mut TestAppContext) {
"the last heading is in view: {after:?}"
);
}
/// Searches the folder for `words`; the hits as found.
fn search(
side: &gpui::Entity<super::Side>,
words: &str,
cx: &mut gpui::VisualTestContext,
) -> Vec<(Option<u64>, crate::folder::Hit)> {
side.update(cx, |side, cx| {
side.query.update(cx, |query, cx| query.set_text(words, cx));
side.wide = true;
side.search(cx);
});
cx.run_until_parked();
side.read_with(cx, |side, _| side.hits.clone())
}
#[gpui::test]
fn a_hit_opens_its_file_with_the_caret_on_the_match(cx: &mut TestAppContext) {
let (data, files) = (
tempfile::tempdir().expect("data"),
tempfile::tempdir().expect("files"),
);
std::fs::write(files.path().join("a.md"), "# A\n\nNo match.\n").expect("a");
std::fs::write(files.path().join("b.md"), "# B\n\nThe Needle here.\n").expect("b");
let (desk, cx) = start(cx, data.path());
open(&desk, files.path(), cx);
let side = desk.read_with(cx, |desk, _| desk.side.clone());
let hits = search(&side, "needle", cx);
let found: Vec<_> = hits.iter().map(|(_, hit)| (hit.line, hit.column)).collect();
assert_eq!(found, [(3, 4)]);
let (tab, hit) = hits[0].clone();
assert_eq!(tab, None, "b.md is not open");
desk.update_in(cx, |desk, window, cx| {
desk.open_at(tab, hit.file, (hit.line - 1, hit.column), window, cx)
});
cx.run_until_parked();
let place = desk.read_with(cx, |desk, cx| {
desk.active_view().expect("b.md opened").read(cx).place(cx)
});
assert_eq!((place.line, place.column), (2, 4));
}
#[gpui::test]
fn a_hit_in_an_open_file_moves_its_caret(cx: &mut TestAppContext) {
let (data, files) = (
tempfile::tempdir().expect("data"),
tempfile::tempdir().expect("files"),
);
let path = files.path().join("a.md");
std::fs::write(&path, "# A\n\nfirst\n\nsecond Word\n").expect("a");
let (desk, cx) = start(cx, data.path());
open(&desk, files.path(), cx);
open(&desk, &path, cx);
let side = desk.read_with(cx, |desk, _| desk.side.clone());
let hits = search(&side, "word", cx);
let (tab, hit) = hits[0].clone();
assert!(tab.is_some(), "the open tab searches");
desk.update_in(cx, |desk, window, cx| {
desk.open_at(tab, hit.file, (hit.line - 1, hit.column), window, cx)
});
cx.run_until_parked();
let place = desk.read_with(cx, |desk, cx| {
desk.active_view().expect("a").read(cx).place(cx)
});
assert_eq!((place.line, place.column), (4, 7));
}
+6 -4
View File
@@ -186,10 +186,12 @@ impl MarkView {
.collect()
}
/// Line `line` in view and focus.
pub fn reveal(&mut self, line: usize, window: &mut Window, cx: &mut Context<Self>) {
self.document
.update(cx, |document, cx| document.reveal(line, window, cx));
/// Line and column `at` in view and focus, caret there.
pub fn reveal(&mut self, at: (usize, usize), window: &mut Window, cx: &mut Context<Self>) {
self.document.update(cx, |document, cx| {
document.reveal(at.0, window, cx);
document.set_caret(at.0, at.1, cx);
});
}
/// The caret and the view, to keep across launches.