mirror of
https://github.com/OrcaSlicer/OrcaSlicer.git
synced 2026-09-27 10:51:22 +00:00
Search improvements. Code refactoring so its easier to maintain. Experimental features for speed dial: connect/disconnect from printer.
This commit is contained in:
@@ -39,37 +39,60 @@ var qEl = null, listEl = null, favEl = null, clearEl = null, eyeEl = null, count
|
||||
|
||||
// ---- pure helpers (no DOM; unit-tested) -------------------------------------
|
||||
// Pre-normalized haystacks, cached on the action object. The fold is length-preserving (1:1 per
|
||||
// char) so the ranges FuzzyRangesNorm returns slice the ORIGINAL title/source text correctly. The
|
||||
// action objects arrive from C++ and are stable for the dialog's lifetime, so we compute these once.
|
||||
// char) so the ranges FuzzyRangesNorm returns slice the ORIGINAL title/group/source text correctly.
|
||||
// The action objects arrive from C++ and are stable for the dialog's lifetime, so we compute these once.
|
||||
function titleNorm(a) {
|
||||
if (a._tn === undefined)
|
||||
a._tn = NormText(a.title, false);
|
||||
return a._tn;
|
||||
}
|
||||
function otherNorm(a) {
|
||||
if (a._on === undefined)
|
||||
a._on = NormText((a.source || "") + " " + (a.group || ""), false);
|
||||
return a._on;
|
||||
// The eyebrow (header) line shows group when present, else source. Settings keep an empty group so
|
||||
// their source path is the eyebrow; commands / plates / recents carry a non-empty group. Splitting the
|
||||
// two lets a match range stay aligned to whichever string the eyebrow actually renders.
|
||||
function groupNorm(a) {
|
||||
if (a._gn === undefined)
|
||||
a._gn = NormText(a.group || "", false);
|
||||
return a._gn;
|
||||
}
|
||||
function sourceNorm(a) {
|
||||
if (a._sn === undefined)
|
||||
a._sn = NormText(a.source || "", false);
|
||||
return a._sn;
|
||||
}
|
||||
|
||||
// Relevance score for a single field vs the current query needle, or -1 when there's no match.
|
||||
// Higher is better: an earlier start and a more contiguous (fewer gaps) match beat a scattered late one.
|
||||
function matchScoreNorm(haystackNorm) {
|
||||
if (!searchNeedle) return -1;
|
||||
var r = FuzzyRangesNorm(haystackNorm || "", searchNeedle);
|
||||
if (!r) return -1;
|
||||
var gaps = 0;
|
||||
for (var i = 1; i < r.length; i++)
|
||||
gaps += r[i][0] - r[i - 1][1];
|
||||
return 1000 - r[0][0] * 10 - gaps * 10;
|
||||
// Match one pre-normalized field vs the current needle. Returns {score, ranges, contiguous} when the
|
||||
// needle is present, else null. wwRe is a compiled whole-word (\b-bounded) regex for the current needle.
|
||||
// A whole-word hit is preferred - it highlights the full word (e.g. "orient" in "Auto-Orient", not the
|
||||
// stray "o" of "Auto") and marks a perfect match. Otherwise FuzzyRangesNorm (which now prefers the
|
||||
// most-contiguous run) is used. score is higher for an earlier start and fewer gaps; contiguous marks
|
||||
// a perfect match - the whole needle landed as one unbroken run.
|
||||
function fieldMatchScore(norm, wwRe) {
|
||||
if (!searchNeedle) return null;
|
||||
if (wwRe) {
|
||||
var m = wwRe.exec(norm || "");
|
||||
if (m)
|
||||
return {score: 1000 - m.index * 10, ranges: [[m.index, m.index + m[0].length]], contiguous: true};
|
||||
}
|
||||
var r = FuzzyRangesNorm(norm || "", searchNeedle);
|
||||
if (!r) return null;
|
||||
var gaps = 0, len = 0;
|
||||
for (var i = 0; i < r.length; i++) {
|
||||
if (i > 0)
|
||||
gaps += r[i][0] - r[i - 1][1];
|
||||
len += r[i][1] - r[i][0];
|
||||
}
|
||||
return {score: 1000 - r[0][0] * 10 - gaps * 10, ranges: r, contiguous: r.length === 1 && len === searchNeedle.length};
|
||||
}
|
||||
|
||||
// Per-action score: title matches rank above a source/group-only match of equal quality.
|
||||
function actionSearchScore(a) {
|
||||
var title = matchScoreNorm(titleNorm(a));
|
||||
var other = matchScoreNorm(otherNorm(a));
|
||||
if (title < 0 && other < 0) return -1;
|
||||
return Math.max(title < 0 ? -1e9 : title + 10000, other < 0 ? -1e9 : other);
|
||||
// Combine the per-field match scores into one comparable value. Ranking tiers, strongest first:
|
||||
// tier (contiguous/perfect vs fuzzy) > field (title > group > source) > start/gaps.
|
||||
// The additive weights keep every contiguous match above every fuzzy one regardless of field.
|
||||
function scoreFields(t, g, s) {
|
||||
var best = -1;
|
||||
if (t) best = Math.max(best, (t.contiguous ? 100000 : 0) + 2000 + t.score);
|
||||
if (g) best = Math.max(best, (g.contiguous ? 100000 : 0) + 1000 + g.score);
|
||||
if (s) best = Math.max(best, (s.contiguous ? 100000 : 0) + s.score);
|
||||
return best;
|
||||
}
|
||||
|
||||
// The unified main-phase search: every action (command/plugin/setting) matching the query, ranked
|
||||
@@ -82,15 +105,27 @@ function searchActions(actions, query) {
|
||||
matchIndex = {};
|
||||
if (!q) { searchNeedle = ""; return list.slice(0); }
|
||||
searchNeedle = NormText(q, false);
|
||||
// Compiled once per pass, reused over every field: non-global so no exec()/lastIndex state leaks
|
||||
// between fields, and EscapeRegExp keeps regex metachars in the query literal.
|
||||
var wwRe = new RegExp("\\b" + EscapeRegExp(searchNeedle) + "\\b");
|
||||
|
||||
var scored = [];
|
||||
for (var i = 0; i < list.length; i++) {
|
||||
var a = list[i];
|
||||
var s = actionSearchScore(a);
|
||||
if (s < 0) continue;
|
||||
var titleMatch = FuzzyRangesNorm(titleNorm(a), searchNeedle);
|
||||
matchIndex[a.id] = { title: titleMatch, source: FuzzyRangesNorm(otherNorm(a), searchNeedle), useTitle: !!titleMatch };
|
||||
scored.push({ a: a, s: s });
|
||||
var t = fieldMatchScore(titleNorm(a), wwRe);
|
||||
var g = fieldMatchScore(groupNorm(a), wwRe);
|
||||
var s = fieldMatchScore(sourceNorm(a), wwRe);
|
||||
var score = scoreFields(t, g, s);
|
||||
if (score < 0) continue;
|
||||
// Ranges are per-field against the ACTUAL text drawn: title for the row-name, and group (or
|
||||
// source when group is empty) for the eyebrow - so highlight offsets stay aligned to the label.
|
||||
matchIndex[a.id] = {
|
||||
title: t ? t.ranges : null,
|
||||
group: g ? g.ranges : null,
|
||||
source: s ? s.ranges : null,
|
||||
useEyebrowGroup: !!(a.group)
|
||||
};
|
||||
scored.push({ a: a, s: score });
|
||||
}
|
||||
scored.sort(function (x, y) {
|
||||
if (x.s !== y.s) return y.s - x.s;
|
||||
@@ -112,7 +147,7 @@ function buildKey() { return phase + "|" + (query || "").trim(); }
|
||||
|
||||
function visibleFavourites(favourites, actions) {
|
||||
// why: a fav whose id has no live action (plugin unloaded/disabled) renders a dead
|
||||
// monogram tile whose click run()s to a silent no-op; drop it from the quick-bar.
|
||||
// placeholder tile whose click run()s to a silent no-op; drop it from the quick-bar.
|
||||
var seen = {};
|
||||
(actions || []).forEach(function (a) { seen[a.id] = true; });
|
||||
return (favourites || []).filter(function (id, i, arr) {
|
||||
@@ -164,21 +199,42 @@ function shouldRenderActionList(query) {
|
||||
return !!((query || "").trim());
|
||||
}
|
||||
|
||||
// Put an action's pattern pictogram into a tile (search row or favourites tile) when it has one,
|
||||
// otherwise fall back to the monogram. Toggles the has-icon class so CSS neutralises the hue.
|
||||
// Monogram code for a tile: title initial, escalated on collision by PREPENDING the source
|
||||
// initial (pi+ti, e.g. "GC"), then a 1-based ordinal - so same-titled items stay distinct.
|
||||
// why: ordinal is assigned by id, not by list order - list order is frecency-sorted and
|
||||
// reshuffles as usage changes, which would otherwise flip who's "1" and who's "2" across runs.
|
||||
function monogramFor(item, list, titleOf, sourceOf, idOf) {
|
||||
var items = list || [];
|
||||
var title = titleOf(item) || " ";
|
||||
var ti = title.charAt(0).toUpperCase();
|
||||
var sameTitle = items.filter(function (o) { return (titleOf(o) || " ").charAt(0).toUpperCase() === ti; });
|
||||
if (sameTitle.length <= 1)
|
||||
return ti;
|
||||
var source = sourceOf(item) || " ";
|
||||
var pi = source.charAt(0).toUpperCase();
|
||||
var sameSource = sameTitle.filter(function (o) { return (sourceOf(o) || " ").charAt(0).toUpperCase() === pi; });
|
||||
if (sameSource.length <= 1)
|
||||
return pi + ti;
|
||||
sameSource.sort(function (a, b) { return idOf(a) < idOf(b) ? -1 : idOf(a) > idOf(b) ? 1 : 0; });
|
||||
for (var i = 0; i < sameSource.length; i++)
|
||||
if (sameSource[i] === item || idOf(sameSource[i]) === idOf(item))
|
||||
return pi + ti + (i + 1);
|
||||
return pi + ti;
|
||||
}
|
||||
|
||||
// Action tile code - see monogramFor for the escalation ladder. Settings are actions now, so
|
||||
// they share this ladder (title initial, then source, then a stable ordinal).
|
||||
function tileCode(action, actions) {
|
||||
return monogramFor(action, actions,
|
||||
function (o) { return o.title; },
|
||||
function (o) { return o.source; },
|
||||
function (o) { return o.id; });
|
||||
}
|
||||
|
||||
// Put an action's monogram into a tile (search row or favourites tile). A null action (a tab row
|
||||
// with no backing action) renders an empty tile.
|
||||
function fillTile(tile, a) {
|
||||
tile.classList.remove("has-icon");
|
||||
if (a && a.icon) {
|
||||
tile.textContent = "";
|
||||
var img = document.createElement("img");
|
||||
img.className = "tile-icon";
|
||||
img.src = a.icon;
|
||||
img.alt = "";
|
||||
tile.appendChild(img);
|
||||
tile.classList.add("has-icon");
|
||||
} else {
|
||||
tile.textContent = a ? tileCode(a, ACTIONS) : "";
|
||||
}
|
||||
tile.textContent = a ? tileCode(a, ACTIONS) : "";
|
||||
}
|
||||
|
||||
// The active list for the main phase. A typed query ranks every action (commands/plugins/settings)
|
||||
@@ -224,38 +280,6 @@ function actionLabel(action, actions) {
|
||||
return label;
|
||||
}
|
||||
|
||||
// Monogram code for a tile: title initial, escalated on collision by PREPENDING the source
|
||||
// initial (pi+ti, e.g. "GC"), then a 1-based ordinal - so same-titled items stay distinct.
|
||||
// why: ordinal is assigned by id, not by list order - list order is frecency-sorted and
|
||||
// reshuffles as usage changes, which would otherwise flip who's "1" and who's "2" across runs.
|
||||
function monogramFor(item, list, titleOf, sourceOf, idOf) {
|
||||
var items = list || [];
|
||||
var title = titleOf(item) || " ";
|
||||
var ti = title.charAt(0).toUpperCase();
|
||||
var sameTitle = items.filter(function (o) { return (titleOf(o) || " ").charAt(0).toUpperCase() === ti; });
|
||||
if (sameTitle.length <= 1)
|
||||
return ti;
|
||||
var source = sourceOf(item) || " ";
|
||||
var pi = source.charAt(0).toUpperCase();
|
||||
var sameSource = sameTitle.filter(function (o) { return (sourceOf(o) || " ").charAt(0).toUpperCase() === pi; });
|
||||
if (sameSource.length <= 1)
|
||||
return pi + ti;
|
||||
sameSource.sort(function (a, b) { return idOf(a) < idOf(b) ? -1 : idOf(a) > idOf(b) ? 1 : 0; });
|
||||
for (var i = 0; i < sameSource.length; i++)
|
||||
if (sameSource[i] === item || idOf(sameSource[i]) === idOf(item))
|
||||
return pi + ti + (i + 1);
|
||||
return pi + ti;
|
||||
}
|
||||
|
||||
// Action tile code - see monogramFor for the escalation ladder. Settings are actions now, so
|
||||
// they share this ladder (title initial, then source, then a stable ordinal).
|
||||
function tileCode(action, actions) {
|
||||
return monogramFor(action, actions,
|
||||
function (o) { return o.title; },
|
||||
function (o) { return o.source; },
|
||||
function (o) { return o.id; });
|
||||
}
|
||||
|
||||
function syncClearButton() {
|
||||
if (clearEl)
|
||||
clearEl.hidden = !query;
|
||||
@@ -542,7 +566,12 @@ function renderActionRow(a, i) {
|
||||
var left = document.createElement("div");
|
||||
left.className = "row-left";
|
||||
var mi = matchIndex[a.id];
|
||||
var sourceEl = markedText("row-eyebrow", a.group || a.source, mi ? mi.source : null);
|
||||
// The eyebrow shows group when present, else source. Highlight with the ranges of whichever of the
|
||||
// two the eyebrow actually renders (so a "Recent Projects"/"Object" header match lights up like a
|
||||
// setting path does - the offsets are computed against the same string we are marking).
|
||||
var eyebrow = a.group || a.source;
|
||||
var eyebrowMatch = mi ? (mi.useEyebrowGroup ? mi.group : mi.source) : null;
|
||||
var sourceEl = markedText("row-eyebrow", eyebrow, eyebrowMatch);
|
||||
var line = document.createElement("div");
|
||||
line.className = "row-line";
|
||||
var name = markedText("row-name", a.title, mi ? mi.title : null);
|
||||
@@ -675,8 +704,8 @@ function renderCommandsList() {
|
||||
updateSelection();
|
||||
}
|
||||
|
||||
// A tab row: no star/unpin (tabs aren't pinnable), tile monogram from the title. Uses tabTitle so
|
||||
// pages added with an empty text (e.g. Home) still show a label and an icon letter.
|
||||
// A tab row: no star/unpin (tabs aren't pinnable), placeholder tile (tabs have no pictogram). Uses
|
||||
// tabTitle so pages added with an empty text (e.g. Home) still show a label.
|
||||
function renderTabRow(t, i) {
|
||||
var label = tabTitle(t);
|
||||
var row = document.createElement("div");
|
||||
@@ -686,7 +715,7 @@ function renderTabRow(t, i) {
|
||||
var tile = document.createElement("div");
|
||||
tile.className = "tile";
|
||||
tile.style.setProperty("--h", hue(t.id));
|
||||
tile.textContent = label.charAt(0).toUpperCase();
|
||||
fillTile(tile, null);
|
||||
|
||||
var left = document.createElement("div");
|
||||
left.className = "row-left";
|
||||
|
||||
@@ -59,7 +59,7 @@ assert.equal(ctx.tabTitle({ id: "home" }), "Home",
|
||||
assert.equal(ctx.tabTitle({ id: "prepare", title: "Prepare" }), "Prepare",
|
||||
"a populated title is kept as-is");
|
||||
assert.equal(ctx.tabTitle({ id: "prepare", title: " Prepare" }), "Prepare",
|
||||
"a leading space from the Notebook button label is trimmed so the icon letter shows");
|
||||
"a leading space from the Notebook button label is trimmed so the label shows cleanly");
|
||||
assert.equal(ctx.filterTabs([{ id: "home", title: "" }], "home").length, 1,
|
||||
"an untitled tab still matches a typed query via the id/title fallback");
|
||||
assert.equal(ctx.filterTabs([{ id: "prepare", title: " Prepare" }], "prepare").length, 1,
|
||||
@@ -82,6 +82,33 @@ assert.equal(ctx.searchActions(pool, "layer").length >= 2, true,
|
||||
assert.equal(ctx.searchActions(pool, "surface")[0].id, "c2",
|
||||
"a later-but-precise match still ranks by relevance, not by pool type");
|
||||
|
||||
// A perfect match (the needle as one contiguous run) outranks a fuzzy match of the same field - and a
|
||||
// contiguous GROUP/header hit ("Recent Projects") beats a scattered fuzzy TITLE hit ("Retraction Length"),
|
||||
// which is what the old flat title-bonus ranking got backwards.
|
||||
const perfectPool = [
|
||||
{ id: "set", title: "Retraction Length", source: "Process : Quality : Retraction", group: "", input: "" },
|
||||
{ id: "recent", title: "myproject.3mf", source: "/home/me/projects/myproject.3mf", group: "Recent Projects", input: "" }
|
||||
];
|
||||
assert.equal(ctx.searchActions(perfectPool, "recent")[0].id, "recent",
|
||||
"a contiguous header/group match ranks above a scattered fuzzy title match");
|
||||
// Within a perfect match, the row-name (title) outranks the header (group): the action whose TITLE
|
||||
// contains the needle perfectly beats the action whose GROUP does, both being contiguous matches.
|
||||
const titleFirstPool = [
|
||||
{ id: "grp", title: "Delete Selected", source: "OrcaSlicer", group: "Object", input: "" },
|
||||
{ id: "t", title: "Object Preview", source: "OrcaSlicer", group: "View", input: "" }
|
||||
];
|
||||
assert.equal(ctx.searchActions(titleFirstPool, "object")[0].id, "t",
|
||||
"a perfect title match ranks above an equally-perfect group match");
|
||||
|
||||
// Highlighting: the needle is matched as a whole word / most-contiguous run, so "orient" lights up the
|
||||
// whole word in "Auto-Orient" instead of the stray "o" of "Auto" plus "rient" (greedy-leftmost).
|
||||
const orientPool = [
|
||||
{ id: "ao", title: "Auto-Orient", source: "OrcaSlicer", group: "Object", input: "" }
|
||||
];
|
||||
ctx.searchActions(orientPool, "orient");
|
||||
assert.deepEqual(ctx.matchIndex.ao.title, [[5, 11]],
|
||||
"a whole-word match highlights the full word, not a scattered fuzzy pick");
|
||||
|
||||
// commandList (the main-phase list) delegates to the ranked search for a typed query and returns
|
||||
// the mixed recents (no discrimination) for an empty query.
|
||||
const mixed = [
|
||||
|
||||
@@ -43,8 +43,8 @@ body {
|
||||
cursor: pointer;
|
||||
color: hsl(var(--h) var(--speed-tile-text-s, 72%) var(--speed-tile-text-l, 38%));
|
||||
font-weight: 700;
|
||||
/* why: mirror .tile centering - tileCode can be 2-3 chars (e.g. "EA1") on collision, and a
|
||||
bare <button> inherits 13px + UA padding, clipping the code (e.g. "AE1"). inline-flex + 12px + pad:0 fits it. */
|
||||
/* why: mirror .tile centering - icons/placeholder are 18px glyphs, and a bare <button> inherits
|
||||
13px + UA padding, which would clip them. inline-flex + pad:0 + overflow:hidden fits them. */
|
||||
font-size: 12px;
|
||||
padding: 0;
|
||||
display: inline-flex;
|
||||
@@ -227,17 +227,6 @@ body {
|
||||
background: var(--speed-tile-bg, #f0f0f0);
|
||||
border: 1px solid var(--speed-tile-border, #d8d8d8);
|
||||
}
|
||||
/* A tile holding a pattern pictogram: drop the hue fill so the stroked SVG reads cleanly. */
|
||||
.tile.has-icon,
|
||||
.fav-tile.has-icon {
|
||||
background: none;
|
||||
border-color: transparent;
|
||||
}
|
||||
.tile-icon {
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
display: block;
|
||||
}
|
||||
.row-left { flex: 1 1 auto; min-width: 0; display: flex; flex-direction: column; }
|
||||
.row-eyebrow {
|
||||
font-size: 10px;
|
||||
|
||||
@@ -34,24 +34,44 @@ function NormText(text, caseSensitive) {
|
||||
// Match a PRE-normalized haystack against a PRE-normalized needle (both produced by NormText with the
|
||||
// same caseSensitive flag). Skipping the per-character fold makes repeated matching (per keystroke over a
|
||||
// cached pool) cheap. Returns ranges in original coordinates, or null on no match.
|
||||
// Prefers the most-contiguous (smallest-span) occurrence over greedy-leftmost: a scattered match that
|
||||
// spans a stray earlier character is worse than a tight run later, so "orient" against a normalized
|
||||
// "auto-orient" returns [[5,11]] (the word) not [[3,4],[6,11]]. A fully contiguous run is the optimum
|
||||
// and short-circuits early.
|
||||
function FuzzyRangesNorm(haystackNorm, needleNorm) {
|
||||
const t = haystackNorm || "";
|
||||
const needle = needleNorm || "";
|
||||
if (!needle)
|
||||
return null;
|
||||
const ranges = [];
|
||||
let qi = 0;
|
||||
for (let i = 0; i < t.length && qi < needle.length; i++) {
|
||||
if (t[i] === needle[qi]) {
|
||||
const last = ranges[ranges.length - 1];
|
||||
if (last && last[1] === i)
|
||||
last[1] = i + 1;
|
||||
else
|
||||
ranges.push([i, i + 1]);
|
||||
qi++;
|
||||
const n = t.length, nl = needle.length;
|
||||
let best = null; // {ranges, span, start}
|
||||
for (let start = 0; start < n; start++) {
|
||||
if (t[start] !== needle[0])
|
||||
continue;
|
||||
let qi = 0;
|
||||
const ranges = [];
|
||||
let lastEnd = start;
|
||||
for (let i = start; i < n && qi < nl; i++) {
|
||||
if (t[i] === needle[qi]) {
|
||||
const last = ranges[ranges.length - 1];
|
||||
if (last && last[1] === i)
|
||||
last[1] = i + 1;
|
||||
else
|
||||
ranges.push([i, i + 1]);
|
||||
lastEnd = i + 1;
|
||||
qi++;
|
||||
}
|
||||
}
|
||||
if (qi !== nl)
|
||||
continue;
|
||||
const span = lastEnd - start;
|
||||
if (!best || span < best.span || (span === best.span && start < best.start)) {
|
||||
best = { ranges, span, start };
|
||||
if (span === nl)
|
||||
return ranges; // can't beat a fully contiguous run
|
||||
}
|
||||
}
|
||||
return qi === needle.length ? ranges : null;
|
||||
return best ? best.ranges : null;
|
||||
}
|
||||
|
||||
function EscapeRegExp(value) {
|
||||
@@ -95,3 +115,18 @@ function WholeWordRanges(text, query, caseSensitive) {
|
||||
ranges.push([match.index, match.index + match[0].length]);
|
||||
return ranges.length > 0 ? ranges : null;
|
||||
}
|
||||
|
||||
// Same whole-word (\b-bounded) match as WholeWordRanges, but against PRE-normalized haystack/needle
|
||||
// (NormText output, so offsets stay length-aligned to the original text). Returns the first match as
|
||||
// [[i, i+len]] in original coordinates, or null. Non-global so the caller can reuse one compiled regex
|
||||
// across many fields without re-setting lastIndex. Skipping the per-char fold keeps the Speed Dial's
|
||||
// per-keystroke scan over thousands of cached settings cheap.
|
||||
function WholeWordRangesNorm(haystackNorm, needleNorm) {
|
||||
const t = haystackNorm || "";
|
||||
const needle = needleNorm || "";
|
||||
if (!needle)
|
||||
return null;
|
||||
const re = new RegExp(`\\b${EscapeRegExp(needle)}\\b`);
|
||||
const match = re.exec(t);
|
||||
return match ? [[match.index, match.index + match[0].length]] : null;
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@ const vm = require("vm"), assert = require("assert"), fs = require("fs");
|
||||
const ctx = {};
|
||||
vm.createContext(ctx);
|
||||
vm.runInContext(fs.readFileSync(__dirname + "/fuzzy-search.js", "utf8"), ctx);
|
||||
const { FoldChar, Norm, EscapeRegExp, FuzzyRanges, WholeWordRanges } = ctx;
|
||||
const { FoldChar, Norm, NormText, EscapeRegExp, FuzzyRanges, WholeWordRanges, FuzzyRangesNorm, WholeWordRangesNorm } = ctx;
|
||||
|
||||
// FoldChar / Norm: accents fold, case only folds when case-insensitive.
|
||||
assert.equal(FoldChar("é"), "e");
|
||||
@@ -34,4 +34,15 @@ assert.deepEqual(WholeWordRanges("a.b", "a", false), [[0, 1]]); // '.' is a
|
||||
assert.equal(EscapeRegExp("a.b*"), "a\\.b\\*");
|
||||
assert.deepEqual(WholeWordRanges("c++ tool", "c", false), [[0, 1]]); // '+' would be a regex error unescaped
|
||||
|
||||
// FuzzyRangesNorm: prefers the most-contiguous (smallest-span) occurrence over greedy-leftmost, so a
|
||||
// stray earlier character does not steal the highlight from a later tight word.
|
||||
assert.deepEqual(FuzzyRangesNorm(NormText("Auto-Orient", false), NormText("orient", false)), [[5, 11]]);
|
||||
// A contiguous run inside a larger word is also preferred over a scattered greedy pick.
|
||||
assert.deepEqual(FuzzyRangesNorm(NormText("AutoOriented", false), NormText("orient", false)), [[4, 10]]);
|
||||
assert.equal(FuzzyRangesNorm(NormText("Measure", false), NormText("xyz", false)), null); // no subsequence
|
||||
|
||||
// WholeWordRangesNorm: \b-bounded literal against a pre-normalized haystack, offsets in original coords.
|
||||
assert.deepEqual(WholeWordRangesNorm(NormText("Auto-Orient", false), NormText("orient", false)), [[5, 11]]);
|
||||
assert.equal(WholeWordRangesNorm(NormText("AutoOriented", false), NormText("orient", false)), null); // inside a word
|
||||
|
||||
console.log("ok");
|
||||
|
||||
Reference in New Issue
Block a user