mirror of
https://github.com/QwenLM/qwen-code.git
synced 2025-12-20 08:47:44 +00:00
feat(cli): Improve @ autocompletion for mid-sentence edits (#5321)
This commit is contained in:
@@ -4,7 +4,7 @@
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import { useState, useEffect, useCallback, useMemo } from 'react';
|
||||
import { useState, useEffect, useCallback, useMemo, useRef } from 'react';
|
||||
import * as fs from 'fs/promises';
|
||||
import * as path from 'path';
|
||||
import { glob } from 'glob';
|
||||
@@ -22,7 +22,10 @@ import {
|
||||
Suggestion,
|
||||
} from '../components/SuggestionsDisplay.js';
|
||||
import { CommandContext, SlashCommand } from '../commands/types.js';
|
||||
import { TextBuffer } from '../components/shared/text-buffer.js';
|
||||
import {
|
||||
logicalPosToOffset,
|
||||
TextBuffer,
|
||||
} from '../components/shared/text-buffer.js';
|
||||
import { isSlashCommand } from '../utils/commandUtils.js';
|
||||
import { toCodePoints } from '../utils/textUtils.js';
|
||||
|
||||
@@ -57,6 +60,11 @@ export function useCompletion(
|
||||
const [isLoadingSuggestions, setIsLoadingSuggestions] =
|
||||
useState<boolean>(false);
|
||||
const [isPerfectMatch, setIsPerfectMatch] = useState<boolean>(false);
|
||||
const completionStart = useRef(-1);
|
||||
const completionEnd = useRef(-1);
|
||||
|
||||
const cursorRow = buffer.cursor[0];
|
||||
const cursorCol = buffer.cursor[1];
|
||||
|
||||
const resetCompletionState = useCallback(() => {
|
||||
setSuggestions([]);
|
||||
@@ -127,17 +135,15 @@ export function useCompletion(
|
||||
}, [suggestions.length]);
|
||||
|
||||
// Check if cursor is after @ or / without unescaped spaces
|
||||
const isActive = useMemo(() => {
|
||||
const commandIndex = useMemo(() => {
|
||||
if (isSlashCommand(buffer.text.trim())) {
|
||||
return true;
|
||||
return 0;
|
||||
}
|
||||
|
||||
// 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 codePoints = toCodePoints(buffer.lines[cursorRow] || '');
|
||||
for (let i = cursorCol - 1; i >= 0; i--) {
|
||||
const char = codePoints[i];
|
||||
|
||||
if (char === ' ') {
|
||||
@@ -147,19 +153,19 @@ export function useCompletion(
|
||||
backslashCount++;
|
||||
}
|
||||
if (backslashCount % 2 === 0) {
|
||||
return false; // Inactive on unescaped space.
|
||||
return -1; // Inactive on unescaped space.
|
||||
}
|
||||
} else if (char === '@') {
|
||||
// Active if we find an '@' before any unescaped space.
|
||||
return true;
|
||||
return i;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}, [buffer.text, buffer.cursor, buffer.lines]);
|
||||
return -1;
|
||||
}, [buffer.text, cursorRow, cursorCol, buffer.lines]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isActive) {
|
||||
if (commandIndex === -1) {
|
||||
resetCompletionState();
|
||||
return;
|
||||
}
|
||||
@@ -311,14 +317,29 @@ export function useCompletion(
|
||||
}
|
||||
|
||||
// Handle At Command Completion
|
||||
const atIndex = buffer.text.lastIndexOf('@');
|
||||
if (atIndex === -1) {
|
||||
resetCompletionState();
|
||||
return;
|
||||
const currentLine = buffer.lines[cursorRow] || '';
|
||||
const codePoints = toCodePoints(currentLine);
|
||||
|
||||
completionEnd.current = codePoints.length;
|
||||
for (let i = cursorCol; i < codePoints.length; i++) {
|
||||
if (codePoints[i] === ' ') {
|
||||
let backslashCount = 0;
|
||||
for (let j = i - 1; j >= 0 && codePoints[j] === '\\'; j--) {
|
||||
backslashCount++;
|
||||
}
|
||||
|
||||
if (backslashCount % 2 === 0) {
|
||||
completionEnd.current = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const partialPath = buffer.text.substring(atIndex + 1);
|
||||
const pathStart = commandIndex + 1;
|
||||
const partialPath = currentLine.substring(pathStart, completionEnd.current);
|
||||
const lastSlashIndex = partialPath.lastIndexOf('/');
|
||||
completionStart.current =
|
||||
lastSlashIndex === -1 ? pathStart : pathStart + lastSlashIndex + 1;
|
||||
const baseDirRelative =
|
||||
lastSlashIndex === -1
|
||||
? '.'
|
||||
@@ -601,9 +622,12 @@ export function useCompletion(
|
||||
};
|
||||
}, [
|
||||
buffer.text,
|
||||
cursorRow,
|
||||
cursorCol,
|
||||
buffer.lines,
|
||||
dirs,
|
||||
cwd,
|
||||
isActive,
|
||||
commandIndex,
|
||||
resetCompletionState,
|
||||
slashCommands,
|
||||
commandContext,
|
||||
@@ -669,23 +693,19 @@ export function useCompletion(
|
||||
|
||||
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;
|
||||
if (completionStart.current === -1 || completionEnd.current === -1) {
|
||||
return;
|
||||
}
|
||||
|
||||
buffer.replaceRangeByOffset(
|
||||
autoCompleteStartIndex,
|
||||
buffer.text.length,
|
||||
logicalPosToOffset(buffer.lines, cursorRow, completionStart.current),
|
||||
logicalPosToOffset(buffer.lines, cursorRow, completionEnd.current),
|
||||
suggestion,
|
||||
);
|
||||
}
|
||||
resetCompletionState();
|
||||
},
|
||||
[resetCompletionState, buffer, suggestions, slashCommands],
|
||||
[cursorRow, resetCompletionState, buffer, suggestions, slashCommands],
|
||||
);
|
||||
|
||||
return {
|
||||
|
||||
Reference in New Issue
Block a user