fix(cli): preserve Unicode queries in at completion (#13866)

This commit is contained in:
Lucien authored and GitHub committed 2026-10-14 14:02:55 +00:00
1 parent bab0e99d89
commit ec76c3d44e
5 files changed
+96 -2

No files matched your search

+2
View File
@@ -522,6 +522,8 @@ Use `/auth` inside a Qwen Code session to configure authentication. Use `/doctor
Note: Spaces in paths need to be escaped with backslash (e.g., `@My\ Documents/file.txt`)
In the interactive prompt, `@` completion supports Unicode filenames, including emoji. Emoji elsewhere in the prompt do not change the file path used for completion.
## 3. Exclamation Commands (`!`) - Shell Command Execution
Exclamation commands allow you to execute system commands directly within Qwen Code.
@@ -487,6 +487,61 @@ describe('useCommandCompletion', () => {
});
describe('Completion mode detection', () => {
it.each([
['hello @README.md', 'README.md'],
['中文 @文件.ts', '文件.ts'],
['😀 @README.md', 'README.md'],
['@😀file.ts', '😀file.ts'],
['😀😀 @src/𠮷file.ts', 'src/𠮷file.ts'],
['😀 @src/😀file.ts', 'src/😀file.ts'],
])('uses the complete file query from %s', (text, pattern) => {
renderHook(() =>
useCommandCompletion(
useTextBufferForTest(text, Array.from(text).length),
testRootDir,
[],
mockCommandContext,
false,
mockConfig,
),
);
expect(useAtCompletion).toHaveBeenLastCalledWith(
expect.objectContaining({ enabled: true, pattern }),
);
});
it('keeps code-point replacement bounds with the cursor inside a Unicode path', async () => {
setupMocks({
atSuggestions: [{ label: '😀done.ts', value: '😀done.ts' }],
});
const { result } = renderHook(() => {
const textBuffer = useTextBufferForTest('😀 @src/😀file.ts tail', 7);
const completion = useCommandCompletion(
textBuffer,
testRootDir,
[],
mockCommandContext,
false,
mockConfig,
);
return { ...completion, textBuffer };
});
expect(useAtCompletion).toHaveBeenLastCalledWith(
expect.objectContaining({ enabled: true, pattern: 'src/😀file.ts' }),
);
await waitFor(() => {
expect(result.current.suggestions).toHaveLength(1);
});
act(() => {
result.current.handleAutocomplete(0);
});
expect(result.current.textBuffer.text).toBe('😀 @😀done.ts tail');
});
it('should switch to AT mode when typing @ after a slash command (#2518)', async () => {
setupMocks({
atSuggestions: [{ label: 'src/file.txt', value: 'src/file.txt' }],
@@ -143,7 +143,7 @@ export function useCommandCompletion(
}
}
const pathStart = i + 1;
const partialPath = currentLine.substring(pathStart, end);
const partialPath = codePoints.slice(pathStart, end).join('');
return {
completionMode: CompletionMode.AT,
query: partialPath,
@@ -97,6 +97,43 @@ const GATING_COMMANDS: readonly SlashCommand[] = [
cmd({ name: 'review' }),
];
describe('detectCompletionTarget AT queries', () => {
it.each([
['hello @README.md', 'README.md'],
['中文 @文件.ts', '文件.ts'],
['😀 @README.md', 'README.md'],
['@😀file.ts', '😀file.ts'],
['😀😀 @src/𠮷file.ts', 'src/𠮷file.ts'],
['😀 @src/😀file.ts', 'src/😀file.ts'],
])('uses the complete file query from %s', (text, query) => {
const cursor = Array.from(text).length;
expect(detectCompletionTarget([text], 0, cursor, text, cursor)).toEqual({
mode: CompletionMode.AT,
query,
start: Array.from(text).indexOf('@') + 1,
end: cursor,
});
});
it('keeps code-point replacement bounds with the cursor inside a Unicode path', () => {
const text = '😀 @src/😀file.ts tail';
const target = detectCompletionTarget([text], 0, 7, text, 7);
expect(target).toEqual({
mode: CompletionMode.AT,
query: 'src/😀file.ts',
start: 3,
end: 15,
});
const applied = applyCompletion(
text,
target!,
{ label: '😀done.ts', value: '😀done.ts' },
false,
);
expect(applied.line).toBe('😀 @😀done.ts tail');
});
});
describe('detectCompletionTarget mid-input gating (useCommandCompletion port, R1-86)', () => {
const detect = (text: string, cursorOffset: number) => {
const lines = text.split('\n');
@@ -216,7 +216,7 @@ export function detectCompletionTarget(
const pathStart = i + 1;
return {
mode: CompletionMode.AT,
query: currentLine.substring(pathStart, end),
query: codePoints.slice(pathStart, end).join(''),
start: pathStart,
end,
};