Show English context for Speed Dial search matches

Display English action names and breadcrumbs for English-only matches
Show English tab titles when tabs match only by their English names
This commit is contained in:
Lam Wei Lun
2026-10-11 23:44:21 +08:00
parent cab7a7d75f
commit 2ccd73759f
3 changed files with 147 additions and 9 deletions
+71 -9
View File
@@ -112,7 +112,7 @@ var activeCompletion = null;
// - *_en: English counterparts of the translated fields, sent by C++ only when they differ from the
// displayed text (a non-English UI), so the page can still be searched in English. Each carries the
// weight of the localized field it mirrors, so a match ranks the same in either language. Search-only,
// except title_en, whose ranges drive the row's English hint.
// except their ranges, which drive the row's English hint (see ENGLISH_HINT_FIELDS).
var SEARCH_FIELDS = [
{ text: function (a) { return a.title; }, weight: SCORE_TITLE, range: "title" },
{ text: function (a) { return a.group; }, weight: SCORE_GROUP, range: "group" },
@@ -121,12 +121,48 @@ var SEARCH_FIELDS = [
{ text: function (a) { return a.kind === "plugin" ? T("sd_plugins", "Plugins") : ""; }, weight: SCORE_GROUP, range: "plugin" },
{ text: function (a) { return a.kind === "plugin" ? "plugin plugins" : ""; }, weight: SCORE_GROUP, range: null },
{ text: function (a) { return a.title_en; }, weight: SCORE_TITLE, range: "title_en" },
{ text: function (a) { return a.group_en; }, weight: SCORE_GROUP, range: null },
{ text: function (a) { return a.source_en; }, weight: 0, range: null },
{ text: function (a) { return a.full_label_en; }, weight: 0, range: null }
{ text: function (a) { return a.group_en; }, weight: SCORE_GROUP, range: "group_en" },
{ text: function (a) { return a.source_en; }, weight: 0, range: "source_en" },
{ text: function (a) { return a.full_label_en; }, weight: 0, range: "full_label_en" }
];
var MATCH_RANGE_KEYS = SEARCH_FIELDS.map(function (f) { return f.range; }).filter(Boolean);
// English aliases a row can match through. `range` names the matchIndex key matchFields fills for that
// field; the hint shows the action's English name (title_en), falling back to whichever alias matched.
var ENGLISH_HINT_FIELDS = [
{ range: "title_en", text: function (a) { return a.title_en; } },
{ range: "group_en", text: function (a) { return a.group_en; } },
{ range: "source_en", text: function (a) { return a.source_en; } },
{ range: "full_label_en", text: function (a) { return a.full_label_en; } }
];
// The English alias to show as a muted hint on an action row, or null. Only a row whose match came
// through an English alias gets one (a visible translated highlight, or a local-only alias, needs
// none). The hint is the action's English NAME - highlighted when the name itself matched, otherwise
// shown plain - so the row identifies the action instead of repeating its translated eyebrow: a
// "Process : Speed" breadcrumb match still names the setting ("Inner wall"), not the breadcrumb again.
function englishHint(a, mi, visibleMatch) {
if (!mi || visibleMatch) return null;
var matched = null;
for (var i = 0; i < ENGLISH_HINT_FIELDS.length; i++) {
if (mi[ENGLISH_HINT_FIELDS[i].range]) { matched = ENGLISH_HINT_FIELDS[i]; break; }
}
if (!matched) return null;
if (a.title_en) return { text: a.title_en, ranges: mi.title_en || null };
var text = matched.text(a);
return text ? { text: text, ranges: mi[matched.range] } : null;
}
// The English breadcrumb a row matched through (a command's group, a setting's source path), with its
// highlight ranges, or null when neither matched. Shown beside the translated eyebrow so an English
// hit is visible where it landed, alongside the English name hint on the row-name line.
function englishBreadcrumb(a, mi) {
if (!mi) return null;
if (mi.group_en && a.group_en) return { text: a.group_en, ranges: mi.group_en };
if (mi.source_en && a.source_en) return { text: a.source_en, ranges: mi.source_en };
return null;
}
// The action's non-empty SEARCH_FIELDS as pre-normalized haystacks ({norm, weight, range}), cached on
// the action object. The action objects arrive from C++ and are stable for the dialog's lifetime, so
// this runs once per action; skipping empty fields keeps the per-keystroke scan to the text an action
@@ -417,6 +453,18 @@ function filterTabs(tabs, query) {
});
}
// The English title of a tab matched only through it, with its highlight ranges, or null when the tab
// matched its translated title or id (so it already shows why) or has no English title.
function tabAliasHint(t, query) {
var q = (query || "").trim();
if (!q || !t || !t.title_en)
return null;
if (FuzzyRanges(tabTitle(t), q, false) || FuzzyRanges(t.id, q, false))
return null;
var ranges = FuzzyRanges(t.title_en, q, false);
return ranges ? { text: t.title_en, ranges: ranges } : null;
}
function shouldRenderActionList(query) {
return !!((query || "").trim());
}
@@ -951,12 +999,21 @@ function renderActionRow(a, i) {
var showPluginCategory = a.kind === "plugin" && (typedQuery || isRecent);
var eyebrow = actionEyebrow(a, query, isRecent);
var eyebrowMatch = mi ? (showPluginCategory ? mi.plugin : (mi.useEyebrowGroup ? mi.group : mi.source)) : null;
shell.left.insertBefore(markedText("row-eyebrow", eyebrow, eyebrowMatch), shell.line);
// The eyebrow line: the translated breadcrumb, and beside it the English breadcrumb when the query
// matched that (non-English UI), so an English hit on a breadcrumb is visible where it landed.
var eyebrowLine = document.createElement("div");
eyebrowLine.className = "row-eyebrow-line";
eyebrowLine.appendChild(markedText("row-eyebrow", eyebrow, eyebrowMatch));
var enBreadcrumb = englishBreadcrumb(a, mi);
if (enBreadcrumb)
eyebrowLine.appendChild(markedText("row-eyebrow-en", enBreadcrumb.text, enBreadcrumb.ranges));
shell.left.insertBefore(eyebrowLine, shell.line);
shell.line.appendChild(markedText("row-name", a.title, mi ? mi.title : null));
// A row found only through its English title shows that title, highlighted, so the match is
// explained even when nothing in the translated name resembles the query.
if (mi && !mi.title && mi.title_en)
shell.line.appendChild(markedText("row-alias", a.title_en, mi.title_en));
// A row found only through an English alias shows the action's English name, highlighted when the
// name itself matched, so the match makes sense even when the translated row resembles nothing.
var hint = englishHint(a, mi, mi ? (mi.title || eyebrowMatch) : null);
if (hint)
shell.line.appendChild(markedText("row-alias", hint.text, hint.ranges));
var badge = modeBadge(a, USER_MODE);
if (badge) {
var tag = document.createElement("span");
@@ -1134,6 +1191,11 @@ function renderTabRow(t, i) {
name.className = "row-name";
name.textContent = label;
shell.line.appendChild(name);
// A tab found only through its English title shows that title, highlighted, so the match is
// explained even when the translated label looks nothing like the query.
var hint = tabAliasHint(t, query);
if (hint)
shell.line.appendChild(markedText("row-alias", hint.text, hint.ranges));
shell.row.onclick = function () { sel = { zone: "list", i: i }; render({ resize: true }); };
shell.row.ondblclick = function () { sel = { zone: "list", i: i }; jumpToTab(t); };
return shell.row;
@@ -59,6 +59,16 @@ assert.deepEqual(ctx.filterTabs(localizedTabs, "device").map(function (t) { retu
"a localized tab matches its English title");
assert.deepEqual(ctx.filterTabs(localizedTabs, "设备").map(function (t) { return t.id; }), ["monitor"],
"a localized tab still matches its translated title");
// A tab found only through its English title shows it as a hint; one that matched its translated
// title or id already shows why, and one without an English title has nothing to show.
assert.deepEqual(ctx.tabAliasHint(localizedTabs[0], "device"), { text: "Device", ranges: [[0, 6]] },
"an English-only tab match yields the English title and its highlight ranges");
assert.equal(ctx.tabAliasHint(localizedTabs[0], "设备"), null,
"a translated-title tab match draws no English hint");
assert.equal(ctx.tabAliasHint(localizedTabs[0], "monitor"), null,
"a tab matched by its id draws no English hint");
assert.equal(ctx.tabAliasHint(localizedTabs[1], "Device"), null,
"a tab with no English title has no hint");
// tabTitle: pages added with an empty title (e.g. MainFrame's Home tab) fall back to the id.
assert.equal(ctx.tabTitle({ id: "home", title: "" }), "Home",
@@ -242,6 +252,47 @@ assert.equal(ctx.completionFor("reverse o", ctx.searchActions(zhPool, "reverse o
"completion extends an English word from title_en");
assert.equal(ctx.completionFor("quality overh", ctx.searchActions(zhPool, "quality overh")).suffix, "angs",
"completion also reaches the English breadcrumb");
// A row matched only through an English group/source/full_label alias still names the action in
// English: the hint is title_en (the English name), not the alias that matched, so a breadcrumb hit
// does not just repeat the translated eyebrow.
ctx.searchActions(zhPool, "export");
assert.deepEqual(ctx.matchIndex.slice.group_en, [[8, 14]], "the English group keeps its ranges");
assert.deepEqual(ctx.englishHint(zhPool[1], ctx.matchIndex.slice, null), { text: "Slice and Preview", ranges: null },
"an English group match names the action, unhighlighted");
assert.deepEqual(ctx.englishBreadcrumb(zhPool[1], ctx.matchIndex.slice), { text: "Slice & Export", ranges: [[8, 14]] },
"an English group match shows the English breadcrumb beside the eyebrow");
ctx.searchActions(zhPool, "overhang reversal");
assert.deepEqual(ctx.matchIndex.rev.full_label_en, [[0, 17]], "the English full_label keeps its ranges");
assert.deepEqual(ctx.englishHint(zhPool[0], ctx.matchIndex.rev, null), { text: "Reverse on even", ranges: null },
"an English full_label match names the action, unhighlighted");
ctx.searchActions(zhPool, "quality");
assert.deepEqual(ctx.matchIndex.rev.source_en, [[10, 17]], "the English breadcrumb keeps its ranges");
assert.deepEqual(ctx.englishHint(zhPool[0], ctx.matchIndex.rev, null), { text: "Reverse on even", ranges: null },
"an English breadcrumb match names the action, not the breadcrumb");
assert.deepEqual(ctx.englishBreadcrumb(zhPool[0], ctx.matchIndex.rev), { text: "Process : Quality : Overhangs", ranges: [[10, 17]] },
"an English breadcrumb match shows the English breadcrumb beside the eyebrow");
// The name itself matching is the one case that highlights the hint.
ctx.searchActions(zhPool, "reverse on even");
assert.deepEqual(ctx.englishHint(zhPool[0], ctx.matchIndex.rev, null), { text: "Reverse on even", ranges: [[0, 15]] },
"an English title match names and highlights the action");
assert.equal(ctx.englishBreadcrumb(zhPool[0], ctx.matchIndex.rev), null,
"an English title match shows no breadcrumb hint");
// A breadcrumb match with no English name falls back to the matched alias.
assert.deepEqual(ctx.englishHint({ title_en: "", source_en: "Process : Speed" }, { source_en: [[10, 15]] }, null),
{ text: "Process : Speed", ranges: [[10, 15]] }, "without an English name the matched alias is shown");
// A visible translated highlight suppresses the hint even when an English alias also matched.
ctx.searchActions(zhPool, "brim");
assert.equal(ctx.englishHint(zhPool[2], ctx.matchIndex.brim, ctx.matchIndex.brim.title), null,
"a row that highlights its translated title draws no English hint");
// The reported case: "process speed" matches only the English breadcrumb of "Inner wall", so the hint
// names the setting rather than echoing the breadcrumb.
const zhInner = [{ id: "inner", title: "内壁", title_en: "Inner wall", source: "工艺 : 速度",
source_en: "Process : Speed", group: "", input: "" }];
ctx.searchActions(zhInner, "process speed");
assert.deepEqual(ctx.englishHint(zhInner[0], ctx.matchIndex.inner, null), { text: "Inner wall", ranges: null },
"a breadcrumb-only match names the setting in English");
assert.deepEqual(ctx.englishBreadcrumb(zhInner[0], ctx.matchIndex.inner), { text: "Process : Speed", ranges: [[0, 8], [10, 15]] },
"a breadcrumb-only match shows the English breadcrumb beside the eyebrow");
// actionCategory: a command/dynamic action's group is its category; a setting uses the top-level
// source segment; every plugin shares one header; a category-less action falls back to "Other".
+25
View File
@@ -390,7 +390,31 @@ body {
flex-direction: column;
}
/* Eyebrow line: the translated breadcrumb, plus the English breadcrumb beside it when it matched. */
.row-eyebrow-line {
display: flex;
align-items: baseline;
gap: 6px;
min-width: 0;
overflow: hidden;
}
.row-eyebrow {
font-size: 10px;
line-height: 1.3;
color: var(--muted, var(--orca-muted, #6b7280));
opacity: .85;
flex: 0 1 auto;
min-width: 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
/* English breadcrumb of a row found only through it (non-English UI); gives way to the translation. */
.row-eyebrow-en {
flex: 0 100 auto;
min-width: 0;
font-size: 10px;
line-height: 1.3;
color: var(--muted, var(--orca-muted, #6b7280));
@@ -427,6 +451,7 @@ body {
.row-name mark,
.row-eyebrow mark,
.row-eyebrow-en mark,
.row-alias mark {
background: var(--plugin-status-warn-bg);
color: var(--plugin-status-warn);