mirror of
https://github.com/l0ng-ai/tty7.git
synced 2026-11-06 08:15:35 +00:00
fix(search): a command named in the query leads over a session that says it
'worktree' then Return resumed a past agent session instead of opening
New Worktree Tab. Two causes:
- The fuzzy scorer matched greedily from the left, so the query's first
letter was spent on any earlier occurrence ('New') and the rest
scattered: a title containing the whole word scored as a poor match.
It now tries every position the first letter occurs and keeps the
best; the leftmost alignment is one of those, so no score drops.
- Session titles are whatever was first typed to an agent, and those
often open with a command's name, taking the prefix bonus. On the All
tab the Sessions section now stands back by a little more than that
bonus when sections are ordered; a session still leads when it is
plainly the better answer, and the Sessions tab ranks untouched.
This commit is contained in:
+34
-12
@@ -17,11 +17,34 @@ pub fn fuzzy_score(query: &str, text: &str) -> Option<i32> {
|
||||
return None;
|
||||
}
|
||||
|
||||
// Every place the first letter occurs is a place the match could begin,
|
||||
// and the best of them is the score. Matching greedily from the left
|
||||
// alone let an early stray letter decide the alignment: `worktree`
|
||||
// against `New Worktree Tab…` spent its `w` on "New" and scattered the
|
||||
// rest, scoring the title as a poor match of the very word it contains.
|
||||
let best = (0..hay.len())
|
||||
.filter(|&i| hay[i] == needle[0])
|
||||
.filter_map(|start| align(&needle, &hay, start))
|
||||
.max()?;
|
||||
|
||||
let mut score = best;
|
||||
if hay == needle {
|
||||
score += 120;
|
||||
} else if hay.starts_with(&needle) {
|
||||
score += 50;
|
||||
}
|
||||
score -= (hay.len() as i32) / 6;
|
||||
Some(score)
|
||||
}
|
||||
|
||||
/// `needle` as a subsequence of `hay` beginning at `start`, taken greedily
|
||||
/// from there; `None` when the rest of it does not fit.
|
||||
fn align(needle: &[char], hay: &[char], start: usize) -> Option<i32> {
|
||||
let mut qi = 0usize;
|
||||
let mut score = 0i32;
|
||||
let mut run = 0i32;
|
||||
let mut prev_hit = false;
|
||||
for (i, ch) in hay.iter().enumerate() {
|
||||
for (i, ch) in hay.iter().enumerate().skip(start) {
|
||||
if qi >= needle.len() {
|
||||
break;
|
||||
}
|
||||
@@ -47,17 +70,7 @@ pub fn fuzzy_score(query: &str, text: &str) -> Option<i32> {
|
||||
prev_hit = true;
|
||||
qi += 1;
|
||||
}
|
||||
if qi < needle.len() {
|
||||
return None;
|
||||
}
|
||||
|
||||
if hay == needle {
|
||||
score += 120;
|
||||
} else if hay.starts_with(&needle) {
|
||||
score += 50;
|
||||
}
|
||||
score -= (hay.len() as i32) / 6;
|
||||
Some(score)
|
||||
(qi == needle.len()).then_some(score)
|
||||
}
|
||||
|
||||
/// How far behind the visible label an alias hit lands. An alias *is* the
|
||||
@@ -117,6 +130,15 @@ mod tests {
|
||||
assert_eq!(frecency_bonus(1_000.0), frecency_bonus(10_000.0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_whole_word_is_found_past_an_earlier_stray_letter() {
|
||||
// The `w` of "New" used to claim the match and scatter the rest.
|
||||
let whole = fuzzy_score("worktree", "New Worktree Tab…").expect("matches");
|
||||
let scattered = fuzzy_score("worktree", "New work on tree").expect("matches");
|
||||
assert!(whole > scattered, "{whole} should beat {scattered}");
|
||||
assert_eq!(whole, 88);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn empty_query_matches_everything() {
|
||||
assert_eq!(fuzzy_score("", "anything"), Some(0));
|
||||
|
||||
@@ -154,7 +154,7 @@ impl Catalog {
|
||||
let mut found: Vec<(i32, Section)> = tabs
|
||||
.filter_map(|s| {
|
||||
let hits = s.search(query, cx);
|
||||
let best = hits.first()?.0;
|
||||
let best = hits.first()?.0 - section_bias(s.tab());
|
||||
let hidden = hits.len().saturating_sub(ALL_TAB_ROWS);
|
||||
let mut rows: Vec<Row> = hits
|
||||
.into_iter()
|
||||
@@ -181,6 +181,25 @@ impl Catalog {
|
||||
}
|
||||
}
|
||||
|
||||
/// How far a past session's best row stands back when the All tab orders its
|
||||
/// sections — a little more than a prefix match is worth.
|
||||
///
|
||||
/// A session is titled with the first thing someone typed to an agent, and
|
||||
/// people start those with the words the commands are named after:
|
||||
/// `Worktree cleanup` took the prefix bonus from `New Worktree Tab…`, so
|
||||
/// `worktree` then Return resumed an agent instead of opening the dialog —
|
||||
/// the heaviest thing on the page picked by a two-word guess. A session
|
||||
/// still leads when it is plainly the better answer; the Sessions tab itself
|
||||
/// ranks untouched.
|
||||
const SESSION_SECTION_BIAS: i32 = 64;
|
||||
|
||||
fn section_bias(tab: SearchTab) -> i32 {
|
||||
match tab {
|
||||
SearchTab::Sessions => SESSION_SECTION_BIAS,
|
||||
_ => 0,
|
||||
}
|
||||
}
|
||||
|
||||
/// A tab's rows on the All tab before anything is typed: what it has been
|
||||
/// used for lately first, topped up from the rest of the tab to
|
||||
/// [`ALL_TAB_ROWS`], then a row into the tab for whatever did not fit. An
|
||||
@@ -693,6 +712,26 @@ mod tests {
|
||||
.in_section(section.to_string())
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
fn a_session_named_like_a_command_does_not_take_return_from_it(cx: &mut TestAppContext) {
|
||||
with_config(cx);
|
||||
let mut catalog = Catalog::new(
|
||||
vec![Item::new("New Worktree Tab…", CommandKind::NewWorktreeTab)],
|
||||
Vec::new(),
|
||||
Vec::new(),
|
||||
);
|
||||
catalog.sessions = vec![session("Worktree cleanup", "Recent")];
|
||||
cx.update(|cx| {
|
||||
let sections = catalog.sections(SearchTab::All, "worktree", cx);
|
||||
let headers: Vec<_> = sections.iter().filter_map(|s| s.title.clone()).collect();
|
||||
assert_eq!(headers, vec!["Actions", "Sessions"]);
|
||||
|
||||
// Typed out, the session is the answer, and it leads.
|
||||
let sections = catalog.sections(SearchTab::All, "worktree cleanup", cx);
|
||||
assert_eq!(sections[0].title.as_deref(), Some("Sessions"));
|
||||
});
|
||||
}
|
||||
|
||||
/// Before anything is typed, the All tab offers to pick up where you left
|
||||
/// off here first; past its rows, the rest of the tab is one row away.
|
||||
#[gpui::test]
|
||||
|
||||
Reference in New Issue
Block a user