opencut_ui: fix shortcut autocomplete never suggesting in prose prompts
The query was the whole token since the last , ; \n delimiter, and a trigger only
matched if that entire token was a substring of it. Typing a normal multi-word
prompt grew the token past any short trigger, so suggestions never appeared.
Now match TRAILING WORD-RUNS of the token, longest first ("golden ho" before
"ho"), keeping substring matching so "@golden" still matches "golden". The longest
run that hits any trigger wins; its span is what acceptance replaces, so completing
a multi-word trigger can't duplicate words. Verified the matching across start-of-
field, mid-prose partials, post-comma, and no-match prose cases.
Also lifted .ac-menu z-index (9000 -> 99999) so it can never render behind the
Composer floating window when completing in the Compose tab.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
247ab84e34
commit
7ed0717f73
@@ -24,7 +24,9 @@
|
||||
return out;
|
||||
}
|
||||
|
||||
function matches(query) {
|
||||
// Triggers whose text contains `query` (substring, so "@golden" matches "golden"),
|
||||
// prefix matches ranked first, identical triggers de-duped.
|
||||
function matchTriggers(query) {
|
||||
const q = query.toLowerCase();
|
||||
const scored = [];
|
||||
for (const e of triggerIndex()) {
|
||||
@@ -34,7 +36,6 @@
|
||||
scored.push({ ...e, rank: at === 0 ? 0 : 1, pos: at });
|
||||
}
|
||||
scored.sort((a, b) => a.rank - b.rank || a.pos - b.pos || a.trigger.length - b.trigger.length);
|
||||
// De-dupe identical triggers (same phrase shared by multiple shortcuts).
|
||||
const seen = new Set();
|
||||
const uniq = [];
|
||||
for (const e of scored) {
|
||||
@@ -46,17 +47,32 @@
|
||||
return uniq;
|
||||
}
|
||||
|
||||
// The token currently under the caret: text since the last delimiter, left-trimmed.
|
||||
function currentQuery(ta) {
|
||||
// What to suggest under the caret. The query is a TRAILING WORD-RUN of the current
|
||||
// token (text since the last delimiter): we try the longest run first ("golden ho"),
|
||||
// then shorter tails ("ho"), and use the longest run that matches any trigger. This
|
||||
// means suggestions surface mid-prose (you don't have to retype from a comma), while
|
||||
// the replaced span stays a clean unit so accepting can't duplicate words.
|
||||
function suggestionsFor(ta) {
|
||||
const caret = ta.selectionStart;
|
||||
if (caret == null || caret !== ta.selectionEnd) return null;
|
||||
const text = ta.value.slice(0, caret);
|
||||
let start = caret;
|
||||
while (start > 0 && !DELIMS.test(text[start - 1])) start--;
|
||||
while (start < caret && /\s/.test(text[start])) start++; // skip leading whitespace
|
||||
const query = text.slice(start, caret);
|
||||
if (query.trim().length < MIN_QUERY) return null;
|
||||
return { start, end: caret, query };
|
||||
let dStart = caret;
|
||||
while (dStart > 0 && !DELIMS.test(text[dStart - 1])) dStart--;
|
||||
// Word-run start offsets within the token, in order (longest run → shortest).
|
||||
const starts = [];
|
||||
let prevWs = true;
|
||||
for (let i = dStart; i < caret; i++) {
|
||||
const ws = /\s/.test(text[i]);
|
||||
if (!ws && prevWs) starts.push(i);
|
||||
prevWs = ws;
|
||||
}
|
||||
for (const start of starts) {
|
||||
const run = text.slice(start, caret);
|
||||
if (run.trim().length < MIN_QUERY) continue;
|
||||
const found = matchTriggers(run);
|
||||
if (found.length) return { span: { start, end: caret }, items: found };
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function attach(ta) {
|
||||
@@ -122,11 +138,9 @@
|
||||
|
||||
function refresh() {
|
||||
if (!enabled()) { close(); return; }
|
||||
const sp = currentQuery(ta);
|
||||
if (!sp) { close(); return; }
|
||||
const found = matches(sp.query);
|
||||
if (!found.length) { close(); return; }
|
||||
open(found, sp);
|
||||
const res = suggestionsFor(ta);
|
||||
if (!res) { close(); return; }
|
||||
open(res.items, res.span);
|
||||
}
|
||||
|
||||
ta.addEventListener("input", refresh);
|
||||
|
||||
@@ -1502,7 +1502,7 @@ body.tour-mode { overflow: hidden; }
|
||||
.compose-head { display: flex; align-items: center; justify-content: space-between; gap: 8px; margin-bottom: 6px; }
|
||||
|
||||
/* Shortcut autocomplete dropdown (anchored under the prompt field) */
|
||||
.ac-menu { position: fixed; z-index: 9000; max-height: 240px; overflow-y: auto;
|
||||
.ac-menu { position: fixed; z-index: 99999; max-height: 240px; overflow-y: auto;
|
||||
background: #16140f; border: 1px solid var(--line); border-radius: 8px; box-shadow: var(--shadow); padding: 4px; }
|
||||
.ac-item { display: flex; align-items: baseline; gap: 8px; padding: 6px 9px; border-radius: 5px; cursor: pointer; }
|
||||
.ac-item.active { background: var(--amber-soft); }
|
||||
|
||||
Reference in New Issue
Block a user