mirror of
https://github.com/QwenLM/qwen-code.git
synced 2025-12-19 09:33:53 +00:00
Fix(command): line/block Comments Incorrectly Parsed as Slash Commands (#6957)
Co-authored-by: Abhi <43648792+abhipatel12@users.noreply.github.com>
This commit is contained in:
@@ -7,6 +7,7 @@
|
|||||||
import { Box, Text } from 'ink';
|
import { Box, Text } from 'ink';
|
||||||
import { Colors } from '../colors.js';
|
import { Colors } from '../colors.js';
|
||||||
import { PrepareLabel } from './PrepareLabel.js';
|
import { PrepareLabel } from './PrepareLabel.js';
|
||||||
|
import { isSlashCommand } from '../utils/commandUtils.js';
|
||||||
export interface Suggestion {
|
export interface Suggestion {
|
||||||
label: string;
|
label: string;
|
||||||
value: string;
|
value: string;
|
||||||
@@ -52,7 +53,7 @@ export function SuggestionsDisplay({
|
|||||||
);
|
);
|
||||||
const visibleSuggestions = suggestions.slice(startIndex, endIndex);
|
const visibleSuggestions = suggestions.slice(startIndex, endIndex);
|
||||||
|
|
||||||
const isSlashCommandMode = userInput.startsWith('/');
|
const isSlashCommandMode = isSlashCommand(userInput);
|
||||||
let commandNameWidth = 0;
|
let commandNameWidth = 0;
|
||||||
|
|
||||||
if (isSlashCommandMode) {
|
if (isSlashCommandMode) {
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import type React from 'react';
|
|||||||
import { Text, Box } from 'ink';
|
import { Text, Box } from 'ink';
|
||||||
import { Colors } from '../../colors.js';
|
import { Colors } from '../../colors.js';
|
||||||
import { SCREEN_READER_USER_PREFIX } from '../../constants.js';
|
import { SCREEN_READER_USER_PREFIX } from '../../constants.js';
|
||||||
|
import { isSlashCommand as checkIsSlashCommand } from '../../utils/commandUtils.js';
|
||||||
|
|
||||||
interface UserMessageProps {
|
interface UserMessageProps {
|
||||||
text: string;
|
text: string;
|
||||||
@@ -16,7 +17,7 @@ interface UserMessageProps {
|
|||||||
export const UserMessage: React.FC<UserMessageProps> = ({ text }) => {
|
export const UserMessage: React.FC<UserMessageProps> = ({ text }) => {
|
||||||
const prefix = '> ';
|
const prefix = '> ';
|
||||||
const prefixWidth = prefix.length;
|
const prefixWidth = prefix.length;
|
||||||
const isSlashCommand = text.startsWith('/');
|
const isSlashCommand = checkIsSlashCommand(text);
|
||||||
|
|
||||||
const textColor = isSlashCommand ? Colors.AccentPurple : Colors.Gray;
|
const textColor = isSlashCommand ? Colors.AccentPurple : Colors.Gray;
|
||||||
const borderColor = isSlashCommand ? Colors.AccentPurple : Colors.Gray;
|
const borderColor = isSlashCommand ? Colors.AccentPurple : Colors.Gray;
|
||||||
|
|||||||
@@ -516,4 +516,81 @@ describe('useCommandCompletion', () => {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('prompt completion filtering', () => {
|
||||||
|
it('should not trigger prompt completion for line comments', async () => {
|
||||||
|
const mockConfig = {
|
||||||
|
getEnablePromptCompletion: () => true,
|
||||||
|
} as Config;
|
||||||
|
|
||||||
|
const { result } = renderHook(() => {
|
||||||
|
const textBuffer = useTextBufferForTest('// This is a line comment');
|
||||||
|
const completion = useCommandCompletion(
|
||||||
|
textBuffer,
|
||||||
|
testDirs,
|
||||||
|
testRootDir,
|
||||||
|
[],
|
||||||
|
mockCommandContext,
|
||||||
|
false,
|
||||||
|
mockConfig,
|
||||||
|
);
|
||||||
|
return { ...completion, textBuffer };
|
||||||
|
});
|
||||||
|
|
||||||
|
// Should not trigger prompt completion for comments
|
||||||
|
expect(result.current.suggestions.length).toBe(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not trigger prompt completion for block comments', async () => {
|
||||||
|
const mockConfig = {
|
||||||
|
getEnablePromptCompletion: () => true,
|
||||||
|
} as Config;
|
||||||
|
|
||||||
|
const { result } = renderHook(() => {
|
||||||
|
const textBuffer = useTextBufferForTest(
|
||||||
|
'/* This is a block comment */',
|
||||||
|
);
|
||||||
|
const completion = useCommandCompletion(
|
||||||
|
textBuffer,
|
||||||
|
testDirs,
|
||||||
|
testRootDir,
|
||||||
|
[],
|
||||||
|
mockCommandContext,
|
||||||
|
false,
|
||||||
|
mockConfig,
|
||||||
|
);
|
||||||
|
return { ...completion, textBuffer };
|
||||||
|
});
|
||||||
|
|
||||||
|
// Should not trigger prompt completion for comments
|
||||||
|
expect(result.current.suggestions.length).toBe(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should trigger prompt completion for regular text when enabled', async () => {
|
||||||
|
const mockConfig = {
|
||||||
|
getEnablePromptCompletion: () => true,
|
||||||
|
} as Config;
|
||||||
|
|
||||||
|
const { result } = renderHook(() => {
|
||||||
|
const textBuffer = useTextBufferForTest(
|
||||||
|
'This is regular text that should trigger completion',
|
||||||
|
);
|
||||||
|
const completion = useCommandCompletion(
|
||||||
|
textBuffer,
|
||||||
|
testDirs,
|
||||||
|
testRootDir,
|
||||||
|
[],
|
||||||
|
mockCommandContext,
|
||||||
|
false,
|
||||||
|
mockConfig,
|
||||||
|
);
|
||||||
|
return { ...completion, textBuffer };
|
||||||
|
});
|
||||||
|
|
||||||
|
// This test verifies that comments are filtered out while regular text is not
|
||||||
|
expect(result.current.textBuffer.text).toBe(
|
||||||
|
'This is regular text that should trigger completion',
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -134,7 +134,7 @@ export function useCommandCompletion(
|
|||||||
if (
|
if (
|
||||||
isPromptCompletionEnabled &&
|
isPromptCompletionEnabled &&
|
||||||
trimmedText.length >= PROMPT_COMPLETION_MIN_LENGTH &&
|
trimmedText.length >= PROMPT_COMPLETION_MIN_LENGTH &&
|
||||||
!trimmedText.startsWith('/') &&
|
!isSlashCommand(trimmedText) &&
|
||||||
!trimmedText.includes('@')
|
!trimmedText.includes('@')
|
||||||
) {
|
) {
|
||||||
return {
|
return {
|
||||||
|
|||||||
@@ -1089,6 +1089,42 @@ describe('useGeminiStream', () => {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should not call handleSlashCommand for line comments', async () => {
|
||||||
|
const { result, mockSendMessageStream: localMockSendMessageStream } =
|
||||||
|
renderTestHook();
|
||||||
|
|
||||||
|
await act(async () => {
|
||||||
|
await result.current.submitQuery('// This is a line comment');
|
||||||
|
});
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(mockHandleSlashCommand).not.toHaveBeenCalled();
|
||||||
|
expect(localMockSendMessageStream).toHaveBeenCalledWith(
|
||||||
|
'// This is a line comment',
|
||||||
|
expect.any(AbortSignal),
|
||||||
|
expect.any(String),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not call handleSlashCommand for block comments', async () => {
|
||||||
|
const { result, mockSendMessageStream: localMockSendMessageStream } =
|
||||||
|
renderTestHook();
|
||||||
|
|
||||||
|
await act(async () => {
|
||||||
|
await result.current.submitQuery('/* This is a block comment */');
|
||||||
|
});
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(mockHandleSlashCommand).not.toHaveBeenCalled();
|
||||||
|
expect(localMockSendMessageStream).toHaveBeenCalledWith(
|
||||||
|
'/* This is a block comment */',
|
||||||
|
expect.any(AbortSignal),
|
||||||
|
expect.any(String),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('Memory Refresh on save_memory', () => {
|
describe('Memory Refresh on save_memory', () => {
|
||||||
|
|||||||
@@ -40,7 +40,7 @@ import type {
|
|||||||
SlashCommandProcessorResult,
|
SlashCommandProcessorResult,
|
||||||
} from '../types.js';
|
} from '../types.js';
|
||||||
import { StreamingState, MessageType, ToolCallStatus } from '../types.js';
|
import { StreamingState, MessageType, ToolCallStatus } from '../types.js';
|
||||||
import { isAtCommand } from '../utils/commandUtils.js';
|
import { isAtCommand, isSlashCommand } from '../utils/commandUtils.js';
|
||||||
import { useShellCommandProcessor } from './shellCommandProcessor.js';
|
import { useShellCommandProcessor } from './shellCommandProcessor.js';
|
||||||
import { handleAtCommand } from './atCommandProcessor.js';
|
import { handleAtCommand } from './atCommandProcessor.js';
|
||||||
import { findLastSafeSplitPoint } from '../utils/markdownUtilities.js';
|
import { findLastSafeSplitPoint } from '../utils/markdownUtilities.js';
|
||||||
@@ -271,7 +271,9 @@ export const useGeminiStream = (
|
|||||||
await logger?.logMessage(MessageSenderType.USER, trimmedQuery);
|
await logger?.logMessage(MessageSenderType.USER, trimmedQuery);
|
||||||
|
|
||||||
// Handle UI-only commands first
|
// Handle UI-only commands first
|
||||||
const slashCommandResult = await handleSlashCommand(trimmedQuery);
|
const slashCommandResult = isSlashCommand(trimmedQuery)
|
||||||
|
? await handleSlashCommand(trimmedQuery)
|
||||||
|
: false;
|
||||||
|
|
||||||
if (slashCommandResult) {
|
if (slashCommandResult) {
|
||||||
switch (slashCommandResult.type) {
|
switch (slashCommandResult.type) {
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import {
|
|||||||
} from '@google/gemini-cli-core';
|
} from '@google/gemini-cli-core';
|
||||||
import type { Content, GenerateContentConfig } from '@google/genai';
|
import type { Content, GenerateContentConfig } from '@google/genai';
|
||||||
import type { TextBuffer } from '../components/shared/text-buffer.js';
|
import type { TextBuffer } from '../components/shared/text-buffer.js';
|
||||||
|
import { isSlashCommand } from '../utils/commandUtils.js';
|
||||||
|
|
||||||
export const PROMPT_COMPLETION_MIN_LENGTH = 5;
|
export const PROMPT_COMPLETION_MIN_LENGTH = 5;
|
||||||
export const PROMPT_COMPLETION_DEBOUNCE_MS = 250;
|
export const PROMPT_COMPLETION_DEBOUNCE_MS = 250;
|
||||||
@@ -81,7 +82,7 @@ export function usePromptCompletion({
|
|||||||
if (
|
if (
|
||||||
trimmedText.length < PROMPT_COMPLETION_MIN_LENGTH ||
|
trimmedText.length < PROMPT_COMPLETION_MIN_LENGTH ||
|
||||||
!geminiClient ||
|
!geminiClient ||
|
||||||
trimmedText.startsWith('/') ||
|
isSlashCommand(trimmedText) ||
|
||||||
trimmedText.includes('@') ||
|
trimmedText.includes('@') ||
|
||||||
!isPromptCompletionEnabled
|
!isPromptCompletionEnabled
|
||||||
) {
|
) {
|
||||||
@@ -237,7 +238,7 @@ export function usePromptCompletion({
|
|||||||
const trimmedText = buffer.text.trim();
|
const trimmedText = buffer.text.trim();
|
||||||
return (
|
return (
|
||||||
trimmedText.length >= PROMPT_COMPLETION_MIN_LENGTH &&
|
trimmedText.length >= PROMPT_COMPLETION_MIN_LENGTH &&
|
||||||
!trimmedText.startsWith('/') &&
|
!isSlashCommand(trimmedText) &&
|
||||||
!trimmedText.includes('@')
|
!trimmedText.includes('@')
|
||||||
);
|
);
|
||||||
}, [buffer.text, isPromptCompletionEnabled, isCursorAtEnd]);
|
}, [buffer.text, isPromptCompletionEnabled, isCursorAtEnd]);
|
||||||
|
|||||||
@@ -102,6 +102,20 @@ describe('commandUtils', () => {
|
|||||||
expect(isSlashCommand('path/to/file')).toBe(false);
|
expect(isSlashCommand('path/to/file')).toBe(false);
|
||||||
expect(isSlashCommand(' /help')).toBe(false);
|
expect(isSlashCommand(' /help')).toBe(false);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should return false for line comments starting with //', () => {
|
||||||
|
expect(isSlashCommand('// This is a comment')).toBe(false);
|
||||||
|
expect(isSlashCommand('// check if variants base info all filled.')).toBe(
|
||||||
|
false,
|
||||||
|
);
|
||||||
|
expect(isSlashCommand('//comment without space')).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return false for block comments starting with /*', () => {
|
||||||
|
expect(isSlashCommand('/* This is a block comment */')).toBe(false);
|
||||||
|
expect(isSlashCommand('/*\n * Multi-line comment\n */')).toBe(false);
|
||||||
|
expect(isSlashCommand('/*comment without space*/')).toBe(false);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('copyToClipboard', () => {
|
describe('copyToClipboard', () => {
|
||||||
|
|||||||
@@ -21,12 +21,28 @@ export const isAtCommand = (query: string): boolean =>
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Checks if a query string potentially represents an '/' command.
|
* Checks if a query string potentially represents an '/' command.
|
||||||
* It triggers if the query starts with '/'
|
* It triggers if the query starts with '/' but excludes code comments like '//' and '/*'.
|
||||||
*
|
*
|
||||||
* @param query The input query string.
|
* @param query The input query string.
|
||||||
* @returns True if the query looks like an '/' command, false otherwise.
|
* @returns True if the query looks like an '/' command, false otherwise.
|
||||||
*/
|
*/
|
||||||
export const isSlashCommand = (query: string): boolean => query.startsWith('/');
|
export const isSlashCommand = (query: string): boolean => {
|
||||||
|
if (!query.startsWith('/')) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Exclude line comments that start with '//'
|
||||||
|
if (query.startsWith('//')) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Exclude block comments that start with '/*'
|
||||||
|
if (query.startsWith('/*')) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
|
||||||
// Copies a string snippet to the clipboard for different platforms
|
// Copies a string snippet to the clipboard for different platforms
|
||||||
export const copyToClipboard = async (text: string): Promise<void> => {
|
export const copyToClipboard = async (text: string): Promise<void> => {
|
||||||
|
|||||||
Reference in New Issue
Block a user