mirror of
https://github.com/block/buzz.git
synced 2026-08-18 06:50:31 +02:00
detectPrefixQuery required the trigger char to sit at start-of-string or
after whitespace, so typing `(#channel` (or `[@name`, `{#chan`) glued to
an opening bracket never opened the autocomplete — the user had to add a
space after the bracket. Allow opening brackets ([{ as a word boundary in
both the fast-path regex and the multi-word backward scan, kept in sync via
a single isBoundaryChar definition. Fixes 7796a4f4.
Adds detectPrefixQuery.test.mjs (first test on this helper): bracket
boundaries for # and @, single- and multi-word paths, and negatives
(prefix glued to a word char still does not trigger).
Co-authored-by: npub17jjz49l9jjmhhk7cac63j8yt9z555n9cw8vk7v5jz4vzw4ppld5qgj57cc <f4a42a97e594b77bdbd8ee35191c8b28a94a4cb871d96f32921558275421fb68@sprout-oss.stage.blox.sqprod.co>
Signed-off-by: npub17jjz49l9jjmhhk7cac63j8yt9z555n9cw8vk7v5jz4vzw4ppld5qgj57cc <f4a42a97e594b77bdbd8ee35191c8b28a94a4cb871d96f32921558275421fb68@sprout-oss.stage.blox.sqprod.co>
73 lines
2.7 KiB
TypeScript
73 lines
2.7 KiB
TypeScript
/**
|
|
* Shared helper for detecting prefix-based queries (e.g. @mention, #channel)
|
|
* within a text input, used by both useMentions and useChannelLinks hooks.
|
|
*/
|
|
|
|
/**
|
|
* Detect whether the user is typing a prefix-based query (e.g. @name or #channel)
|
|
* at the current cursor position.
|
|
*
|
|
* @param prefix - The trigger character (e.g. "@" or "#")
|
|
* @param value - The full text content
|
|
* @param cursorPosition - The current cursor position in the text
|
|
* @param knownNamesLower - Lower-cased known names for multi-word prefix matching
|
|
* @returns The query string and start index, or null if no query is detected
|
|
*/
|
|
export function detectPrefixQuery(
|
|
prefix: string,
|
|
value: string,
|
|
cursorPosition: number,
|
|
knownNamesLower: string[],
|
|
): { query: string; startIndex: number } | null {
|
|
const beforeCursor = value.slice(0, cursorPosition);
|
|
|
|
// A prefix only triggers a query when it opens a "word" — i.e. it sits at the
|
|
// start of the text or right after whitespace or an opening bracket. Opening
|
|
// brackets are included so `(#channel`, `[@name`, `{#chan` all autocomplete
|
|
// the same as ` #channel` (previously only whitespace/start counted, so a
|
|
// prefix glued to a `(` never fired). Keep the two detection paths below in
|
|
// sync with this single definition of "boundary before the prefix".
|
|
const isBoundaryChar = (ch: string) => /[\s([{]/.test(ch);
|
|
|
|
// Fast path: single-word query (no spaces after the prefix)
|
|
const escapedPrefix = prefix.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
const simplePattern = new RegExp(`(?:^|[\\s([{])${escapedPrefix}([^\\s]*)$`);
|
|
const simpleMatch = beforeCursor.match(simplePattern);
|
|
if (simpleMatch) {
|
|
const query = simpleMatch[1];
|
|
const startIndex = beforeCursor.length - query.length - 1; // -1 for prefix
|
|
return { query, startIndex };
|
|
}
|
|
|
|
// Multi-word path: scan backwards for the prefix and check if the text between
|
|
// the prefix and the cursor is a prefix of any known multi-word name.
|
|
const scanStart = Math.max(0, beforeCursor.length - 80);
|
|
for (let i = beforeCursor.length - 1; i >= scanStart; i--) {
|
|
const ch = beforeCursor[i];
|
|
if (ch === prefix) {
|
|
// Ensure prefix is at start or preceded by whitespace/opening bracket
|
|
if (i > 0 && !isBoundaryChar(beforeCursor[i - 1])) {
|
|
continue;
|
|
}
|
|
const candidate = beforeCursor.slice(i + 1);
|
|
if (candidate.length === 0) {
|
|
break;
|
|
}
|
|
const lowerCandidate = candidate.toLowerCase();
|
|
const isPrefix = knownNamesLower.some((name) =>
|
|
name.startsWith(lowerCandidate),
|
|
);
|
|
if (isPrefix) {
|
|
return { query: candidate, startIndex: i };
|
|
}
|
|
break;
|
|
}
|
|
// Stop scanning if we hit a newline
|
|
if (ch === "\n") {
|
|
break;
|
|
}
|
|
}
|
|
|
|
return null;
|
|
}
|