mirror of
https://github.com/google-gemini/gemini-cli.git
synced 2026-05-12 12:54:07 -07:00
refactor: refactor slash command parsing to a util function. (#8381)
This commit is contained in:
@@ -33,6 +33,7 @@ import { CommandService } from '../../services/CommandService.js';
|
|||||||
import { BuiltinCommandLoader } from '../../services/BuiltinCommandLoader.js';
|
import { BuiltinCommandLoader } from '../../services/BuiltinCommandLoader.js';
|
||||||
import { FileCommandLoader } from '../../services/FileCommandLoader.js';
|
import { FileCommandLoader } from '../../services/FileCommandLoader.js';
|
||||||
import { McpPromptLoader } from '../../services/McpPromptLoader.js';
|
import { McpPromptLoader } from '../../services/McpPromptLoader.js';
|
||||||
|
import { parseSlashCommand } from '../../utils/commands.js';
|
||||||
import type { ExtensionUpdateState } from '../state/extensions.js';
|
import type { ExtensionUpdateState } from '../state/extensions.js';
|
||||||
|
|
||||||
interface SlashCommandProcessorActions {
|
interface SlashCommandProcessorActions {
|
||||||
@@ -287,47 +288,13 @@ export const useSlashCommandProcessor = (
|
|||||||
const userMessageTimestamp = Date.now();
|
const userMessageTimestamp = Date.now();
|
||||||
addItem({ type: MessageType.USER, text: trimmed }, userMessageTimestamp);
|
addItem({ type: MessageType.USER, text: trimmed }, userMessageTimestamp);
|
||||||
|
|
||||||
const parts = trimmed.substring(1).trim().split(/\s+/);
|
|
||||||
const commandPath = parts.filter((p) => p); // The parts of the command, e.g., ['memory', 'add']
|
|
||||||
|
|
||||||
let currentCommands = commands;
|
|
||||||
let commandToExecute: SlashCommand | undefined;
|
|
||||||
let pathIndex = 0;
|
|
||||||
let hasError = false;
|
let hasError = false;
|
||||||
const canonicalPath: string[] = [];
|
const {
|
||||||
|
commandToExecute,
|
||||||
|
args,
|
||||||
|
canonicalPath: resolvedCommandPath,
|
||||||
|
} = parseSlashCommand(trimmed, commands);
|
||||||
|
|
||||||
for (const part of commandPath) {
|
|
||||||
// TODO: For better performance and architectural clarity, this two-pass
|
|
||||||
// search could be replaced. A more optimal approach would be to
|
|
||||||
// pre-compute a single lookup map in `CommandService.ts` that resolves
|
|
||||||
// all name and alias conflicts during the initial loading phase. The
|
|
||||||
// processor would then perform a single, fast lookup on that map.
|
|
||||||
|
|
||||||
// First pass: check for an exact match on the primary command name.
|
|
||||||
let foundCommand = currentCommands.find((cmd) => cmd.name === part);
|
|
||||||
|
|
||||||
// Second pass: if no primary name matches, check for an alias.
|
|
||||||
if (!foundCommand) {
|
|
||||||
foundCommand = currentCommands.find((cmd) =>
|
|
||||||
cmd.altNames?.includes(part),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (foundCommand) {
|
|
||||||
commandToExecute = foundCommand;
|
|
||||||
canonicalPath.push(foundCommand.name);
|
|
||||||
pathIndex++;
|
|
||||||
if (foundCommand.subCommands) {
|
|
||||||
currentCommands = foundCommand.subCommands;
|
|
||||||
} else {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const resolvedCommandPath = canonicalPath;
|
|
||||||
const subcommand =
|
const subcommand =
|
||||||
resolvedCommandPath.length > 1
|
resolvedCommandPath.length > 1
|
||||||
? resolvedCommandPath.slice(1).join(' ')
|
? resolvedCommandPath.slice(1).join(' ')
|
||||||
@@ -335,8 +302,6 @@ export const useSlashCommandProcessor = (
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
if (commandToExecute) {
|
if (commandToExecute) {
|
||||||
const args = parts.slice(pathIndex).join(' ');
|
|
||||||
|
|
||||||
if (commandToExecute.action) {
|
if (commandToExecute.action) {
|
||||||
const fullCommandContext: CommandContext = {
|
const fullCommandContext: CommandContext = {
|
||||||
...commandContext,
|
...commandContext,
|
||||||
|
|||||||
@@ -0,0 +1,140 @@
|
|||||||
|
/**
|
||||||
|
* @license
|
||||||
|
* Copyright 2025 Google LLC
|
||||||
|
* SPDX-License-Identifier: Apache-2.0
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { describe, it, expect } from 'vitest';
|
||||||
|
import { parseSlashCommand } from './commands.js';
|
||||||
|
import { CommandKind, type SlashCommand } from '../ui/commands/types.js';
|
||||||
|
|
||||||
|
// Mock command structure for testing
|
||||||
|
const mockCommands: readonly SlashCommand[] = [
|
||||||
|
{
|
||||||
|
name: 'help',
|
||||||
|
description: 'Show help',
|
||||||
|
action: async () => {},
|
||||||
|
kind: CommandKind.BUILT_IN,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'commit',
|
||||||
|
description: 'Commit changes',
|
||||||
|
action: async () => {},
|
||||||
|
kind: CommandKind.FILE,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'memory',
|
||||||
|
description: 'Manage memory',
|
||||||
|
altNames: ['mem'],
|
||||||
|
subCommands: [
|
||||||
|
{
|
||||||
|
name: 'add',
|
||||||
|
description: 'Add to memory',
|
||||||
|
action: async () => {},
|
||||||
|
kind: CommandKind.BUILT_IN,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'clear',
|
||||||
|
description: 'Clear memory',
|
||||||
|
altNames: ['c'],
|
||||||
|
action: async () => {},
|
||||||
|
kind: CommandKind.BUILT_IN,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
kind: CommandKind.BUILT_IN,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
describe('parseSlashCommand', () => {
|
||||||
|
it('should parse a simple command without arguments', () => {
|
||||||
|
const result = parseSlashCommand('/help', mockCommands);
|
||||||
|
expect(result.commandToExecute?.name).toBe('help');
|
||||||
|
expect(result.args).toBe('');
|
||||||
|
expect(result.canonicalPath).toEqual(['help']);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should parse a simple command with arguments', () => {
|
||||||
|
const result = parseSlashCommand(
|
||||||
|
'/commit -m "Initial commit"',
|
||||||
|
mockCommands,
|
||||||
|
);
|
||||||
|
expect(result.commandToExecute?.name).toBe('commit');
|
||||||
|
expect(result.args).toBe('-m "Initial commit"');
|
||||||
|
expect(result.canonicalPath).toEqual(['commit']);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should parse a subcommand', () => {
|
||||||
|
const result = parseSlashCommand('/memory add', mockCommands);
|
||||||
|
expect(result.commandToExecute?.name).toBe('add');
|
||||||
|
expect(result.args).toBe('');
|
||||||
|
expect(result.canonicalPath).toEqual(['memory', 'add']);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should parse a subcommand with arguments', () => {
|
||||||
|
const result = parseSlashCommand(
|
||||||
|
'/memory add some important data',
|
||||||
|
mockCommands,
|
||||||
|
);
|
||||||
|
expect(result.commandToExecute?.name).toBe('add');
|
||||||
|
expect(result.args).toBe('some important data');
|
||||||
|
expect(result.canonicalPath).toEqual(['memory', 'add']);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle a command alias', () => {
|
||||||
|
const result = parseSlashCommand('/mem add some data', mockCommands);
|
||||||
|
expect(result.commandToExecute?.name).toBe('add');
|
||||||
|
expect(result.args).toBe('some data');
|
||||||
|
expect(result.canonicalPath).toEqual(['memory', 'add']);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle a subcommand alias', () => {
|
||||||
|
const result = parseSlashCommand('/memory c', mockCommands);
|
||||||
|
expect(result.commandToExecute?.name).toBe('clear');
|
||||||
|
expect(result.args).toBe('');
|
||||||
|
expect(result.canonicalPath).toEqual(['memory', 'clear']);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return undefined for an unknown command', () => {
|
||||||
|
const result = parseSlashCommand('/unknown', mockCommands);
|
||||||
|
expect(result.commandToExecute).toBeUndefined();
|
||||||
|
expect(result.args).toBe('unknown');
|
||||||
|
expect(result.canonicalPath).toEqual([]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return the parent command if subcommand is unknown', () => {
|
||||||
|
const result = parseSlashCommand(
|
||||||
|
'/memory unknownsub some args',
|
||||||
|
mockCommands,
|
||||||
|
);
|
||||||
|
expect(result.commandToExecute?.name).toBe('memory');
|
||||||
|
expect(result.args).toBe('unknownsub some args');
|
||||||
|
expect(result.canonicalPath).toEqual(['memory']);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle extra whitespace', () => {
|
||||||
|
const result = parseSlashCommand(
|
||||||
|
' /memory add some data ',
|
||||||
|
mockCommands,
|
||||||
|
);
|
||||||
|
expect(result.commandToExecute?.name).toBe('add');
|
||||||
|
expect(result.args).toBe('some data');
|
||||||
|
expect(result.canonicalPath).toEqual(['memory', 'add']);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return undefined if query does not start with a slash', () => {
|
||||||
|
const result = parseSlashCommand('help', mockCommands);
|
||||||
|
expect(result.commandToExecute).toBeUndefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle an empty query', () => {
|
||||||
|
const result = parseSlashCommand('', mockCommands);
|
||||||
|
expect(result.commandToExecute).toBeUndefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle a query with only a slash', () => {
|
||||||
|
const result = parseSlashCommand('/', mockCommands);
|
||||||
|
expect(result.commandToExecute).toBeUndefined();
|
||||||
|
expect(result.args).toBe('');
|
||||||
|
expect(result.canonicalPath).toEqual([]);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,71 @@
|
|||||||
|
/**
|
||||||
|
* @license
|
||||||
|
* Copyright 2025 Google LLC
|
||||||
|
* SPDX-License-Identifier: Apache-2.0
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { type SlashCommand } from '../ui/commands/types.js';
|
||||||
|
|
||||||
|
export type ParsedSlashCommand = {
|
||||||
|
commandToExecute: SlashCommand | undefined;
|
||||||
|
args: string;
|
||||||
|
canonicalPath: string[];
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parses a raw slash command string into its command, arguments, and canonical path.
|
||||||
|
* If no valid command is found, the `commandToExecute` property will be `undefined`.
|
||||||
|
*
|
||||||
|
* @param query The raw input string, e.g., "/memory add some data" or "/help".
|
||||||
|
* @param commands The list of available top-level slash commands.
|
||||||
|
* @returns An object containing the resolved command, its arguments, and its canonical path.
|
||||||
|
*/
|
||||||
|
export const parseSlashCommand = (
|
||||||
|
query: string,
|
||||||
|
commands: readonly SlashCommand[],
|
||||||
|
): ParsedSlashCommand => {
|
||||||
|
const trimmed = query.trim();
|
||||||
|
|
||||||
|
const parts = trimmed.substring(1).trim().split(/\s+/);
|
||||||
|
const commandPath = parts.filter((p) => p); // The parts of the command, e.g., ['memory', 'add']
|
||||||
|
|
||||||
|
let currentCommands = commands;
|
||||||
|
let commandToExecute: SlashCommand | undefined;
|
||||||
|
let pathIndex = 0;
|
||||||
|
const canonicalPath: string[] = [];
|
||||||
|
|
||||||
|
for (const part of commandPath) {
|
||||||
|
// TODO: For better performance and architectural clarity, this two-pass
|
||||||
|
// search could be replaced. A more optimal approach would be to
|
||||||
|
// pre-compute a single lookup map in `CommandService.ts` that resolves
|
||||||
|
// all name and alias conflicts during the initial loading phase. The
|
||||||
|
// processor would then perform a single, fast lookup on that map.
|
||||||
|
|
||||||
|
// First pass: check for an exact match on the primary command name.
|
||||||
|
let foundCommand = currentCommands.find((cmd) => cmd.name === part);
|
||||||
|
|
||||||
|
// Second pass: if no primary name matches, check for an alias.
|
||||||
|
if (!foundCommand) {
|
||||||
|
foundCommand = currentCommands.find((cmd) =>
|
||||||
|
cmd.altNames?.includes(part),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (foundCommand) {
|
||||||
|
commandToExecute = foundCommand;
|
||||||
|
canonicalPath.push(foundCommand.name);
|
||||||
|
pathIndex++;
|
||||||
|
if (foundCommand.subCommands) {
|
||||||
|
currentCommands = foundCommand.subCommands;
|
||||||
|
} else {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const args = parts.slice(pathIndex).join(' ');
|
||||||
|
|
||||||
|
return { commandToExecute, args, canonicalPath };
|
||||||
|
};
|
||||||
Reference in New Issue
Block a user