fix(tui): preserve unicode fuzzy behavior
This commit is contained in:
@@ -35,6 +35,36 @@
|
||||
|
||||
- Sped up fuzzy filtering in selectors (model, settings, file/tree, hook, OAuth) by preparing the query once per filter instead of once per candidate, and memoizing the per-text search index across keystrokes. Incremental typing over a 400-item list drops ~57% (6.7ms → 2.9ms for an 8-keystroke session) with no change to match ranking and no first-keystroke regression; long texts (pasted prompts, transcripts) bypass the cache so memory stays bounded.
|
||||
|
||||
## [16.2.13] - 2026-07-01
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fixed fuzzy-search filtering for CJK and other non-ASCII queries by preserving Unicode letters and numbers during query normalization ([#4114](https://github.com/can1357/oh-my-pi/issues/4114)).
|
||||
|
||||
## [16.2.12] - 2026-07-01
|
||||
|
||||
### Fixed
|
||||
|
||||
- Optimized streaming markdown rendering to reuse already-rendered prefix lines and only render new content deltas, improving performance and reducing redraw flicker.
|
||||
|
||||
## [16.2.10] - 2026-06-30
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fixed mid-prompt `/skill:<name>` autocomplete acceptance wiping the user's draft. The autocomplete now inserts the `/skill:<name> ` token at the cursor (replacing only the partial `/sk` slash token) and preserves prose typed before and after it, so a user can compose a prompt and reach for a skill without losing their train of thought ([#3913](https://github.com/can1357/oh-my-pi/issues/3913)).
|
||||
|
||||
## [16.2.9] - 2026-06-30
|
||||
|
||||
### Added
|
||||
|
||||
- Added `Editor.submit()` to allow programmatic composer submission, enabling integration with speech input and other automated flows.
|
||||
|
||||
## [16.2.7] - 2026-06-30
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fixed an issue where a fast double-Escape keypress was swallowed and ignored, preventing double-escape gestures and subsequent Escape key handlers from firing.
|
||||
|
||||
## [16.2.3] - 2026-06-28
|
||||
|
||||
### Added
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
import { describe, expect, it } from "bun:test";
|
||||
import { fuzzyMatch, fuzzyRank, resetFuzzyIndexCache } from "@oh-my-pi/pi-tui/fuzzy";
|
||||
|
||||
describe("fuzzy index cache", () => {
|
||||
it("produces identical ordering whether the cache is cold or warm", () => {
|
||||
const items = [
|
||||
"openai/gpt-4o",
|
||||
"openai/gpt-4o-mini",
|
||||
"openai/gpt-4-turbo",
|
||||
"openai/o3",
|
||||
"anthropic/claude-3.5-sonnet",
|
||||
"anthropic/claude-4-opus",
|
||||
"google/gemini-2.5-pro",
|
||||
];
|
||||
resetFuzzyIndexCache();
|
||||
const cold = fuzzyRank(items, "gpt 4o", item => item).map(result => result.item);
|
||||
// Second pass reuses the now-cached per-text indices; the result must be byte-for-byte identical.
|
||||
const warm = fuzzyRank(items, "gpt 4o", item => item).map(result => result.item);
|
||||
expect(warm).toEqual(cold);
|
||||
});
|
||||
|
||||
it("matches long candidate texts (cache bypass) deterministically", () => {
|
||||
const longText = `openai/gpt-4o ${"x".repeat(5000)}`;
|
||||
resetFuzzyIndexCache();
|
||||
const first = fuzzyMatch("gpt4", longText);
|
||||
const second = fuzzyMatch("gpt4", longText);
|
||||
expect(first).toEqual(second);
|
||||
expect(first.matches).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe("fuzzyRank empty-normalized query", () => {
|
||||
it("still calls getText for every item when the query normalizes to empty", () => {
|
||||
const seen: string[] = [];
|
||||
const items = ["alpha", "beta", "gamma"];
|
||||
const out = fuzzyRank(items, "!!!", item => {
|
||||
seen.push(item);
|
||||
return item;
|
||||
});
|
||||
// A non-blank query that normalizes to empty matches everything with score 0,
|
||||
// and must still invoke getText per item (preserving callback side effects).
|
||||
expect(seen).toEqual(items);
|
||||
expect(out.map(result => result.item)).toEqual(items);
|
||||
expect(out.every(result => result.score === 0)).toBe(true);
|
||||
});
|
||||
});
|
||||
@@ -1,5 +1,5 @@
|
||||
import { describe, expect, it } from "bun:test";
|
||||
import { fuzzyFilter, fuzzyMatch, fuzzyRank, resetFuzzyIndexCache } from "@oh-my-pi/pi-tui/fuzzy";
|
||||
import { fuzzyFilter, fuzzyMatch } from "@oh-my-pi/pi-tui/fuzzy";
|
||||
|
||||
describe("fuzzyFilter", () => {
|
||||
it("does not satisfy long tokens by scattering letters across unrelated words", () => {
|
||||
@@ -44,46 +44,3 @@ describe("fuzzyFilter", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("fuzzy index cache", () => {
|
||||
it("produces identical ordering whether the cache is cold or warm", () => {
|
||||
const items = [
|
||||
"openai/gpt-4o",
|
||||
"openai/gpt-4o-mini",
|
||||
"openai/gpt-4-turbo",
|
||||
"openai/o3",
|
||||
"anthropic/claude-3.5-sonnet",
|
||||
"anthropic/claude-4-opus",
|
||||
"google/gemini-2.5-pro",
|
||||
];
|
||||
resetFuzzyIndexCache();
|
||||
const cold = fuzzyRank(items, "gpt 4o", item => item).map(result => result.item);
|
||||
// Second pass reuses the now-cached per-text indices; the result must be byte-for-byte identical.
|
||||
const warm = fuzzyRank(items, "gpt 4o", item => item).map(result => result.item);
|
||||
expect(warm).toEqual(cold);
|
||||
});
|
||||
|
||||
it("matches long candidate texts (cache bypass) deterministically", () => {
|
||||
const longText = `openai/gpt-4o ${"x".repeat(5000)}`;
|
||||
resetFuzzyIndexCache();
|
||||
const first = fuzzyMatch("gpt4", longText);
|
||||
const second = fuzzyMatch("gpt4", longText);
|
||||
expect(first).toEqual(second);
|
||||
expect(first.matches).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe("fuzzyRank empty-normalized query", () => {
|
||||
it("still calls getText for every item when the query normalizes to empty", () => {
|
||||
const seen: string[] = [];
|
||||
const items = ["alpha", "beta", "gamma"];
|
||||
const out = fuzzyRank(items, "!!!", item => {
|
||||
seen.push(item);
|
||||
return item;
|
||||
});
|
||||
// A non-blank query that normalizes to empty matches everything with score 0,
|
||||
// and must still invoke getText per item (preserving callback side effects).
|
||||
expect(seen).toEqual(items);
|
||||
expect(out.map(result => result.item)).toEqual(items);
|
||||
expect(out.every(result => result.score === 0)).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user