mirror of
https://github.com/QwenLM/qwen-code.git
synced 2025-12-20 08:47:44 +00:00
[Refactor] Centralizes autocompletion logic within useCompletion (#4740)
This commit is contained in:
@@ -4,7 +4,7 @@
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import { useState, useEffect, useCallback } from 'react';
|
||||
import { useState, useEffect, useCallback, useMemo } from 'react';
|
||||
import * as fs from 'fs/promises';
|
||||
import * as path from 'path';
|
||||
import { glob } from 'glob';
|
||||
@@ -22,6 +22,9 @@ import {
|
||||
Suggestion,
|
||||
} from '../components/SuggestionsDisplay.js';
|
||||
import { CommandContext, SlashCommand } from '../commands/types.js';
|
||||
import { TextBuffer } from '../components/shared/text-buffer.js';
|
||||
import { isSlashCommand } from '../utils/commandUtils.js';
|
||||
import { toCodePoints } from '../utils/textUtils.js';
|
||||
|
||||
export interface UseCompletionReturn {
|
||||
suggestions: Suggestion[];
|
||||
@@ -35,12 +38,12 @@ export interface UseCompletionReturn {
|
||||
resetCompletionState: () => void;
|
||||
navigateUp: () => void;
|
||||
navigateDown: () => void;
|
||||
handleAutocomplete: (indexToUse: number) => void;
|
||||
}
|
||||
|
||||
export function useCompletion(
|
||||
query: string,
|
||||
buffer: TextBuffer,
|
||||
cwd: string,
|
||||
isActive: boolean,
|
||||
slashCommands: readonly SlashCommand[],
|
||||
commandContext: CommandContext,
|
||||
config?: Config,
|
||||
@@ -122,13 +125,45 @@ export function useCompletion(
|
||||
});
|
||||
}, [suggestions.length]);
|
||||
|
||||
// Check if cursor is after @ or / without unescaped spaces
|
||||
const isActive = useMemo(() => {
|
||||
if (isSlashCommand(buffer.text.trim())) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// For other completions like '@', we search backwards from the cursor.
|
||||
const [row, col] = buffer.cursor;
|
||||
const currentLine = buffer.lines[row] || '';
|
||||
const codePoints = toCodePoints(currentLine);
|
||||
|
||||
for (let i = col - 1; i >= 0; i--) {
|
||||
const char = codePoints[i];
|
||||
|
||||
if (char === ' ') {
|
||||
// Check for unescaped spaces.
|
||||
let backslashCount = 0;
|
||||
for (let j = i - 1; j >= 0 && codePoints[j] === '\\'; j--) {
|
||||
backslashCount++;
|
||||
}
|
||||
if (backslashCount % 2 === 0) {
|
||||
return false; // Inactive on unescaped space.
|
||||
}
|
||||
} else if (char === '@') {
|
||||
// Active if we find an '@' before any unescaped space.
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}, [buffer.text, buffer.cursor, buffer.lines]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isActive) {
|
||||
resetCompletionState();
|
||||
return;
|
||||
}
|
||||
|
||||
const trimmedQuery = query.trimStart();
|
||||
const trimmedQuery = buffer.text.trimStart();
|
||||
|
||||
if (trimmedQuery.startsWith('/')) {
|
||||
// Always reset perfect match at the beginning of processing.
|
||||
@@ -275,13 +310,13 @@ export function useCompletion(
|
||||
}
|
||||
|
||||
// Handle At Command Completion
|
||||
const atIndex = query.lastIndexOf('@');
|
||||
const atIndex = buffer.text.lastIndexOf('@');
|
||||
if (atIndex === -1) {
|
||||
resetCompletionState();
|
||||
return;
|
||||
}
|
||||
|
||||
const partialPath = query.substring(atIndex + 1);
|
||||
const partialPath = buffer.text.substring(atIndex + 1);
|
||||
const lastSlashIndex = partialPath.lastIndexOf('/');
|
||||
const baseDirRelative =
|
||||
lastSlashIndex === -1
|
||||
@@ -545,7 +580,7 @@ export function useCompletion(
|
||||
clearTimeout(debounceTimeout);
|
||||
};
|
||||
}, [
|
||||
query,
|
||||
buffer.text,
|
||||
cwd,
|
||||
isActive,
|
||||
resetCompletionState,
|
||||
@@ -554,6 +589,77 @@ export function useCompletion(
|
||||
config,
|
||||
]);
|
||||
|
||||
const handleAutocomplete = useCallback(
|
||||
(indexToUse: number) => {
|
||||
if (indexToUse < 0 || indexToUse >= suggestions.length) {
|
||||
return;
|
||||
}
|
||||
const query = buffer.text;
|
||||
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.
|
||||
// - Otherwise, the base is everything EXCEPT the last partial part.
|
||||
const basePath =
|
||||
hasTrailingSpace || isParentPath ? parts : parts.slice(0, -1);
|
||||
const newValue = `/${[...basePath, suggestion].join(' ')}`;
|
||||
|
||||
buffer.setText(newValue);
|
||||
} else {
|
||||
const atIndex = query.lastIndexOf('@');
|
||||
if (atIndex === -1) return;
|
||||
const pathPart = query.substring(atIndex + 1);
|
||||
const lastSlashIndexInPath = pathPart.lastIndexOf('/');
|
||||
let autoCompleteStartIndex = atIndex + 1;
|
||||
if (lastSlashIndexInPath !== -1) {
|
||||
autoCompleteStartIndex += lastSlashIndexInPath + 1;
|
||||
}
|
||||
buffer.replaceRangeByOffset(
|
||||
autoCompleteStartIndex,
|
||||
buffer.text.length,
|
||||
suggestion,
|
||||
);
|
||||
}
|
||||
resetCompletionState();
|
||||
},
|
||||
[resetCompletionState, buffer, suggestions, slashCommands],
|
||||
);
|
||||
|
||||
return {
|
||||
suggestions,
|
||||
activeSuggestionIndex,
|
||||
@@ -566,5 +672,6 @@ export function useCompletion(
|
||||
resetCompletionState,
|
||||
navigateUp,
|
||||
navigateDown,
|
||||
handleAutocomplete,
|
||||
};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user