feat(core): Use completionStart/End for slash command auto-completion (#5374)
This commit is contained in:
parent
24c5a15d7a
commit
c795168e9c
|
@ -136,13 +136,14 @@ export function useCompletion(
|
||||||
|
|
||||||
// Check if cursor is after @ or / without unescaped spaces
|
// Check if cursor is after @ or / without unescaped spaces
|
||||||
const commandIndex = useMemo(() => {
|
const commandIndex = useMemo(() => {
|
||||||
if (isSlashCommand(buffer.text.trim())) {
|
const currentLine = buffer.lines[cursorRow] || '';
|
||||||
return 0;
|
if (cursorRow === 0 && isSlashCommand(currentLine.trim())) {
|
||||||
|
return currentLine.indexOf('/');
|
||||||
}
|
}
|
||||||
|
|
||||||
// For other completions like '@', we search backwards from the cursor.
|
// For other completions like '@', we search backwards from the cursor.
|
||||||
|
|
||||||
const codePoints = toCodePoints(buffer.lines[cursorRow] || '');
|
const codePoints = toCodePoints(currentLine);
|
||||||
for (let i = cursorCol - 1; i >= 0; i--) {
|
for (let i = cursorCol - 1; i >= 0; i--) {
|
||||||
const char = codePoints[i];
|
const char = codePoints[i];
|
||||||
|
|
||||||
|
@ -162,7 +163,7 @@ export function useCompletion(
|
||||||
}
|
}
|
||||||
|
|
||||||
return -1;
|
return -1;
|
||||||
}, [buffer.text, cursorRow, cursorCol, buffer.lines]);
|
}, [cursorRow, cursorCol, buffer.lines]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (commandIndex === -1) {
|
if (commandIndex === -1) {
|
||||||
|
@ -170,14 +171,15 @@ export function useCompletion(
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const trimmedQuery = buffer.text.trimStart();
|
const currentLine = buffer.lines[cursorRow] || '';
|
||||||
|
const codePoints = toCodePoints(currentLine);
|
||||||
|
|
||||||
if (trimmedQuery.startsWith('/')) {
|
if (codePoints[commandIndex] === '/') {
|
||||||
// Always reset perfect match at the beginning of processing.
|
// Always reset perfect match at the beginning of processing.
|
||||||
setIsPerfectMatch(false);
|
setIsPerfectMatch(false);
|
||||||
|
|
||||||
const fullPath = trimmedQuery.substring(1);
|
const fullPath = currentLine.substring(commandIndex + 1);
|
||||||
const hasTrailingSpace = trimmedQuery.endsWith(' ');
|
const hasTrailingSpace = currentLine.endsWith(' ');
|
||||||
|
|
||||||
// Get all non-empty parts of the command.
|
// Get all non-empty parts of the command.
|
||||||
const rawParts = fullPath.split(/\s+/).filter((p) => p);
|
const rawParts = fullPath.split(/\s+/).filter((p) => p);
|
||||||
|
@ -217,9 +219,10 @@ export function useCompletion(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let exactMatchAsParent: SlashCommand | undefined;
|
||||||
// Handle the Ambiguous Case
|
// Handle the Ambiguous Case
|
||||||
if (!hasTrailingSpace && currentLevel) {
|
if (!hasTrailingSpace && currentLevel) {
|
||||||
const exactMatchAsParent = currentLevel.find(
|
exactMatchAsParent = currentLevel.find(
|
||||||
(cmd) =>
|
(cmd) =>
|
||||||
(cmd.name === partial || cmd.altNames?.includes(partial)) &&
|
(cmd.name === partial || cmd.altNames?.includes(partial)) &&
|
||||||
cmd.subCommands,
|
cmd.subCommands,
|
||||||
|
@ -253,15 +256,33 @@ export function useCompletion(
|
||||||
}
|
}
|
||||||
|
|
||||||
const depth = commandPathParts.length;
|
const depth = commandPathParts.length;
|
||||||
|
const isArgumentCompletion =
|
||||||
// Provide Suggestions based on the now-corrected context
|
|
||||||
|
|
||||||
// Argument Completion
|
|
||||||
if (
|
|
||||||
leafCommand?.completion &&
|
leafCommand?.completion &&
|
||||||
(hasTrailingSpace ||
|
(hasTrailingSpace ||
|
||||||
(rawParts.length > depth && depth > 0 && partial !== ''))
|
(rawParts.length > depth && depth > 0 && partial !== ''));
|
||||||
) {
|
|
||||||
|
// Set completion range
|
||||||
|
if (hasTrailingSpace || exactMatchAsParent) {
|
||||||
|
completionStart.current = currentLine.length;
|
||||||
|
completionEnd.current = currentLine.length;
|
||||||
|
} else if (partial) {
|
||||||
|
if (isArgumentCompletion) {
|
||||||
|
const commandSoFar = `/${commandPathParts.join(' ')}`;
|
||||||
|
const argStartIndex =
|
||||||
|
commandSoFar.length + (commandPathParts.length > 0 ? 1 : 0);
|
||||||
|
completionStart.current = argStartIndex;
|
||||||
|
} else {
|
||||||
|
completionStart.current = currentLine.length - partial.length;
|
||||||
|
}
|
||||||
|
completionEnd.current = currentLine.length;
|
||||||
|
} else {
|
||||||
|
// e.g. /
|
||||||
|
completionStart.current = commandIndex + 1;
|
||||||
|
completionEnd.current = currentLine.length;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Provide Suggestions based on the now-corrected context
|
||||||
|
if (isArgumentCompletion) {
|
||||||
const fetchAndSetSuggestions = async () => {
|
const fetchAndSetSuggestions = async () => {
|
||||||
setIsLoadingSuggestions(true);
|
setIsLoadingSuggestions(true);
|
||||||
const argString = rawParts.slice(depth).join(' ');
|
const argString = rawParts.slice(depth).join(' ');
|
||||||
|
@ -317,9 +338,6 @@ export function useCompletion(
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handle At Command Completion
|
// Handle At Command Completion
|
||||||
const currentLine = buffer.lines[cursorRow] || '';
|
|
||||||
const codePoints = toCodePoints(currentLine);
|
|
||||||
|
|
||||||
completionEnd.current = codePoints.length;
|
completionEnd.current = codePoints.length;
|
||||||
for (let i = cursorCol; i < codePoints.length; i++) {
|
for (let i = cursorCol; i < codePoints.length; i++) {
|
||||||
if (codePoints[i] === ' ') {
|
if (codePoints[i] === ' ') {
|
||||||
|
@ -639,73 +657,34 @@ export function useCompletion(
|
||||||
if (indexToUse < 0 || indexToUse >= suggestions.length) {
|
if (indexToUse < 0 || indexToUse >= suggestions.length) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const query = buffer.text;
|
|
||||||
const suggestion = suggestions[indexToUse].value;
|
const suggestion = suggestions[indexToUse].value;
|
||||||
|
|
||||||
if (query.trimStart().startsWith('/')) {
|
|
||||||
const hasTrailingSpace = query.endsWith(' ');
|
|
||||||
const parts = query
|
|
||||||
.trimStart()
|
|
||||||
.substring(1)
|
|
||||||
.split(/\s+/)
|
|
||||||
.filter(Boolean);
|
|
||||||
|
|
||||||
let isParentPath = false;
|
|
||||||
// If there's no trailing space, we need to check if the current query
|
|
||||||
// is already a complete path to a parent command.
|
|
||||||
if (!hasTrailingSpace) {
|
|
||||||
let currentLevel: readonly SlashCommand[] | undefined = slashCommands;
|
|
||||||
for (let i = 0; i < parts.length; i++) {
|
|
||||||
const part = parts[i];
|
|
||||||
const found: SlashCommand | undefined = currentLevel?.find(
|
|
||||||
(cmd) => cmd.name === part || cmd.altNames?.includes(part),
|
|
||||||
);
|
|
||||||
|
|
||||||
if (found) {
|
|
||||||
if (i === parts.length - 1 && found.subCommands) {
|
|
||||||
isParentPath = true;
|
|
||||||
}
|
|
||||||
currentLevel = found.subCommands as
|
|
||||||
| readonly SlashCommand[]
|
|
||||||
| undefined;
|
|
||||||
} else {
|
|
||||||
// Path is invalid, so it can't be a parent path.
|
|
||||||
currentLevel = undefined;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Determine the base path of the command.
|
|
||||||
// - If there's a trailing space, the whole command is the base.
|
|
||||||
// - If it's a known parent path, the whole command is the base.
|
|
||||||
// - If the last part is a complete argument, the whole command is the base.
|
|
||||||
// - Otherwise, the base is everything EXCEPT the last partial part.
|
|
||||||
const lastPart = parts.length > 0 ? parts[parts.length - 1] : '';
|
|
||||||
const isLastPartACompleteArg =
|
|
||||||
lastPart.startsWith('--') && lastPart.includes('=');
|
|
||||||
|
|
||||||
const basePath =
|
|
||||||
hasTrailingSpace || isParentPath || isLastPartACompleteArg
|
|
||||||
? parts
|
|
||||||
: parts.slice(0, -1);
|
|
||||||
const newValue = `/${[...basePath, suggestion].join(' ')} `;
|
|
||||||
|
|
||||||
buffer.setText(newValue);
|
|
||||||
} else {
|
|
||||||
if (completionStart.current === -1 || completionEnd.current === -1) {
|
if (completionStart.current === -1 || completionEnd.current === -1) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const isSlash = (buffer.lines[cursorRow] || '')[commandIndex] === '/';
|
||||||
|
let suggestionText = suggestion;
|
||||||
|
if (isSlash) {
|
||||||
|
// If we are inserting (not replacing), and the preceding character is not a space, add one.
|
||||||
|
if (
|
||||||
|
completionStart.current === completionEnd.current &&
|
||||||
|
completionStart.current > commandIndex + 1 &&
|
||||||
|
(buffer.lines[cursorRow] || '')[completionStart.current - 1] !== ' '
|
||||||
|
) {
|
||||||
|
suggestionText = ' ' + suggestionText;
|
||||||
|
}
|
||||||
|
suggestionText += ' ';
|
||||||
|
}
|
||||||
|
|
||||||
buffer.replaceRangeByOffset(
|
buffer.replaceRangeByOffset(
|
||||||
logicalPosToOffset(buffer.lines, cursorRow, completionStart.current),
|
logicalPosToOffset(buffer.lines, cursorRow, completionStart.current),
|
||||||
logicalPosToOffset(buffer.lines, cursorRow, completionEnd.current),
|
logicalPosToOffset(buffer.lines, cursorRow, completionEnd.current),
|
||||||
suggestion,
|
suggestionText,
|
||||||
);
|
);
|
||||||
}
|
|
||||||
resetCompletionState();
|
resetCompletionState();
|
||||||
},
|
},
|
||||||
[cursorRow, resetCompletionState, buffer, suggestions, slashCommands],
|
[cursorRow, resetCompletionState, buffer, suggestions, commandIndex],
|
||||||
);
|
);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|
Loading…
Reference in New Issue