Files
codex/prs/bolinfest/study/PR-1830-study.md
2025-09-02 15:17:45 -07:00

6.5 KiB
Raw Blame History

DOs

  • Explain Scoring Constant: Add a clear comment for the prefix bonus and how the score is computed.
// Score is "extra span" outside the needle between first/last hits.
// Lower is better. Strongly reward prefix matches to surface commands quickly.
let mut score = window.max(0);
/*
Prefix bonus rationale:
-100 is large enough to dominate small window differences, so a prefix match
beats a slightly tighter non-prefix match but not a wildly better one.
*/
if first_lower_pos == 0 {
    score -= 100; // prefix bonus
}
  • Assert Scores In Tests: Treat tests as executable documentation by checking both indices and scores.
#[test]
fn prefer_contiguous_prefix() {
    let (idx, score) = fuzzy_match("abc", "abc").expect("match");
    assert_eq!(idx, vec![0, 1, 2]);
    assert_eq!(score, -100); // contiguous prefix → window 0 + prefix bonus
}

#[test]
fn spread_match_is_worse() {
    let (_idx, score) = fuzzy_match("a-b-c", "abc").expect("match");
    assert_eq!(score, -98); // window 2 - 100 bonus
    // Prefix contiguous (-100) still beats spread (-98).
}
  • Centralize Popup Limits: Keep popup row limits in one place and use them consistently.
// popup_consts.rs
pub(crate) const MAX_POPUP_ROWS: usize = 8;

// usage
let vis = MAX_POPUP_ROWS.min(matches_len);
state.ensure_visible(matches_len, vis);
  • Cache Filtered Results: Recompute only when the filter or source data changes; reuse on up/down.
pub(crate) struct CommandPopup {
    command_filter: String,
    all_commands: Vec<(&'static str, SlashCommand)>,
    cached: Vec<(&'static SlashCommand, Option<Vec<usize>>, i32)>,
    state: ScrollState,
}

impl CommandPopup {
    fn rebuild_cache(&mut self) {
        self.cached = self.compute_filtered(); // called on filter change only
        let len = self.cached.len();
        self.state.clamp_selection(len);
        self.state.ensure_visible(len, len.min(MAX_POPUP_ROWS));
    }

    pub(crate) fn move_down(&mut self) {
        let len = self.cached.len();
        self.state.move_down_wrap(len);
        self.state.ensure_visible(len, len.min(MAX_POPUP_ROWS));
    }
}
  • Clamp And Keep Selection Visible: Always clamp selection to bounds and update scroll.
let len = rows.len();
state.clamp_selection(len);
state.ensure_visible(len, len.min(MAX_POPUP_ROWS));
  • Adjust Highlight Indices When Prefixing: If you add a "/" or other prefix to the displayed name, shift indices.
// internal indices are for "help"
let indices = fuzzy_indices("help", "hp").unwrap();
// UI shows "/help" → shift by 1 so bold aligns with UI text
let shifted: Vec<usize> = indices.into_iter().map(|i| i + 1).collect();
  • Use ratatui Stylize Helpers: Prefer concise styling over manual Style building.
use ratatui::style::Stylize;

let spans = vec![
    "/".into(),
    "help".bold(),       // matched chars can be bolded one-by-one
    "  ".into(),
    "Show help".dim(),   // description is dim gray
];

// Selected row styling
let cell = Cell::from(Line::from(spans)).style("".yellow().bold().style());
  • Deterministic Sorting: Sort by score, then by command for stability.
out.sort_by(|a, b| a.2.cmp(&b.2).then_with(|| a.0.command().cmp(b.0.command())));
  • Preserve “Searching…” State: Distinguish waiting from no-results to avoid confusing users.
if waiting && rows_all.is_empty() {
    // show an explicit searching state
    let row = Row::new(vec![Cell::from(Line::from("(searching …)".dim()))]);
    Table::new(vec![row], [Constraint::Percentage(100)]).render(area, buf);
} else {
    render_rows(area, buf, &rows_all, &state, MAX_POPUP_ROWS);
}
  • Keep Comments Accurate: If a helper is used at runtime, dont label it “tests only”.
/// Returns filtered commands for both UI rendering and tests.
fn filtered_commands(&self) -> Vec<&SlashCommand> { /* ... */ }
  • Make Wrap-Around Explicit: Gate wrap-around behavior for clarity and easier UX tweaks.
pub(crate) struct ScrollState {
    pub selected_idx: Option<usize>,
    pub scroll_top: usize,
    pub wrap: bool,
}

pub fn move_down(&mut self, len: usize) {
    if len == 0 { self.selected_idx = None; return; }
    self.selected_idx = Some(match (self.selected_idx, self.wrap) {
        (Some(i), _) if i + 1 < len => i + 1,
        (_, true) => 0,
        (Some(i), false) => i,
        (None, _) => 0,
    });
}
  • Be Explicit About Unicode Behavior: Dedup indices after lowercase expansion (e.g., İ → i̇).
result_orig_indices.sort_unstable();
result_orig_indices.dedup(); // safe for multi-char case mappings

DONTs

  • Dont Leave Magic Numbers Unexplained: Avoid bare “-100” without comment or rationale.
// BAD
if first_lower_pos == 0 { score -= 100; }
  • Dont Skip Score Assertions: Verifying only indices omits critical ranking behavior.
// BAD: indices only
let (idx, _score) = fuzzy_match("hello", "hl").unwrap();
assert_eq!(idx, vec![0, 2]);
  • Dont Recompute On Every Keypress: Avoid re-filtering on Up/Down when input hasnt changed.
// BAD
pub fn move_up(&mut self) {
    let matches = self.compute_filtered(); // unnecessary work per keypress
}
  • Dont Let Selection Drift Out Of Range: Always clamp after data changes; otherwise panics or stale UI can occur.
// BAD: no clamp, potential OOB
self.state.selected_idx = Some(self.state.selected_idx.unwrap() + 1);
  • Dont Show “no matches” While Searching: Users read it as terminal, not in-progress.
// BAD
if matches.is_empty() { show("no matches"); }
  • Dont Keep Unused Fields: Remove or wire is_current; dead fields confuse future maintainers.
// BAD: never set, always false
pub is_current: bool,
  • Dont Bypass Stylize: Avoid verbose Style plumbing when helpers are available.
// BAD
Span::styled("text", Style::default().fg(Color::Yellow).add_modifier(Modifier::BOLD));
  • Dont Use Unstable Ordering: Ties without deterministic tiebreakers jitter the UI.
// BAD: score ties produce arbitrary order
out.sort_by(|a, b| a.2.cmp(&b.2));
  • Dont Forget Prefix Offset For Highlights: Your bolding will be misaligned.
// BAD: indices for "help" applied to "/help" without +1 shift
  • Dont Assume Wrap-Around Is Always Desired: Either make it configurable or document it explicitly.
// BAD: hardcoded wrap-around with no way to disable
self.state.move_down_wrap(len);