fuzzy-match + recency-rank the command palette

Completes the command-palette acceptance: the filter is now a
subsequence fuzzy match (was substring), and recently-invoked commands
float to the top and break score ties. The subsequence matcher is
extracted to a shared fuzzy helper so the palette and quick-open file
finder use one implementation instead of a private copy each.

Pinned commands and cross-session recency persistence are left as a
follow-up (they need a pin affordance + settings storage).

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
2026-06-06 09:40:56 +02:00
co-authored by Claude
parent 968c3dbfa8
commit 0e85f7f5e7
8 changed files with 225 additions and 29 deletions
+2 -23
View File
@@ -5,6 +5,7 @@
/// the palette's interaction model.
library;
import 'package:clide/kernel/src/fuzzy.dart';
import 'package:flutter/foundation.dart';
class QuickOpenController extends ChangeNotifier {
@@ -111,7 +112,7 @@ class QuickOpenController extends ChangeNotifier {
final q = _filter.toLowerCase().trim();
final scored = <_Scored>[];
for (final p in _files) {
final s = _fuzzyScore(p.toLowerCase(), q);
final s = fuzzyScore(p.toLowerCase(), q);
if (s != null) scored.add(_Scored(p, s));
}
scored.sort((a, b) {
@@ -128,25 +129,3 @@ class _Scored {
final String path;
final int score;
}
/// Subsequence fuzzy match. Returns null when [query]'s characters
/// don't appear in order within [text]; otherwise a score where lower
/// is better — contiguous, early matches score best (gaps and a late
/// start add penalty).
int? _fuzzyScore(String text, String query) {
if (query.isEmpty) return 0;
var ti = 0;
var qi = 0;
var score = 0;
int? last;
while (ti < text.length && qi < query.length) {
if (text.codeUnitAt(ti) == query.codeUnitAt(qi)) {
score += last == null ? ti : (ti - last - 1);
last = ti;
qi++;
}
ti++;
}
if (qi != query.length) return null;
return score;
}