Files
Claude-Code-Workflow/ccw/src/tools/vscode-lsp.ts
catlog22 eea859dd6f fix(cli): 修复 Windows 路径反斜杠被吞掉的问题并添加跨平台路径支持
- 重写 escapeWindowsArg 函数,正确处理反斜杠和引号转义
- 添加 escapeUnixArg 函数支持 Linux/macOS shell 转义
- 添加 normalizePathSeparators 函数自动转换路径分隔符
- 修复 vscode-lsp.ts 中的 TypeScript 类型错误
2026-01-20 09:44:49 +08:00

318 lines
8.0 KiB
TypeScript

/**
* VSCode LSP Tool - Provides LSP-like code intelligence via VSCode Bridge Extension
*
* Features:
* - get_definition: Find symbol definition
* - get_references: Find all symbol references
* - get_hover: Get hover information for symbols
* - get_document_symbols: List all symbols in file
*
* Requirements:
* - ccw-vscode-bridge extension must be running in VSCode
* - File must be open/accessible in VSCode workspace
*/
import { z } from 'zod';
import type { ToolSchema, ToolResult } from '../types/tool.js';
// VSCode Bridge configuration
const BRIDGE_URL = 'http://127.0.0.1:3457';
const REQUEST_TIMEOUT = 10000; // 10 seconds
// Define Zod schema for validation
const ParamsSchema = z.object({
action: z.enum(['get_definition', 'get_references', 'get_hover', 'get_document_symbols']),
file_path: z.string().describe('Absolute path to the file'),
line: z.number().optional().describe('Line number (1-based)'),
character: z.number().optional().describe('Character position (1-based)'),
});
type Params = z.infer<typeof ParamsSchema>;
/**
* Result types from VSCode LSP
*/
interface Position {
line: number;
character: number;
}
interface Range {
start: Position;
end: Position;
}
interface Location {
uri: string;
range: Range;
}
interface HoverContent {
contents: string[];
range?: Range;
}
interface DocumentSymbol {
name: string;
kind: string;
range: Range;
selectionRange: Range;
detail?: string;
parent?: string;
}
type LSPResult = {
success: boolean;
result?: Location | Location[] | HoverContent[] | DocumentSymbol[];
error?: string;
action: string;
};
/**
* Execute HTTP request to VSCode Bridge
*/
async function callVSCodeBridge(
endpoint: string,
payload: Record<string, unknown>
): Promise<LSPResult> {
const url = `${BRIDGE_URL}${endpoint}`;
try {
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), REQUEST_TIMEOUT);
const response = await fetch(url, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(payload),
signal: controller.signal,
});
clearTimeout(timeoutId);
if (!response.ok) {
const errorBody = await response.json() as any;
return {
success: false,
error: errorBody.error || `HTTP ${response.status}: ${response.statusText}`,
action: endpoint,
};
}
const data = await response.json() as any;
return {
success: data.success !== false,
result: data.result,
action: endpoint,
};
} catch (err) {
const error = err as Error;
if (error.name === 'AbortError') {
return {
success: false,
error: `Request timed out after ${REQUEST_TIMEOUT}ms`,
action: endpoint,
};
}
if ((error as any).code === 'ECONNREFUSED') {
return {
success: false,
error: `Could not connect to VSCode Bridge at ${BRIDGE_URL}. Is the ccw-vscode-bridge extension running in VSCode?`,
action: endpoint,
};
}
return {
success: false,
error: `Request failed: ${error.message}`,
action: endpoint,
};
}
}
/**
* Handler: get_definition
*/
async function handleGetDefinition(params: Params): Promise<LSPResult> {
if (params.line === undefined || params.character === undefined) {
return {
success: false,
error: 'line and character are required for get_definition',
action: 'get_definition',
};
}
return callVSCodeBridge('/get_definition', {
file_path: params.file_path,
line: params.line,
character: params.character,
});
}
/**
* Handler: get_references
*/
async function handleGetReferences(params: Params): Promise<LSPResult> {
if (params.line === undefined || params.character === undefined) {
return {
success: false,
error: 'line and character are required for get_references',
action: 'get_references',
};
}
return callVSCodeBridge('/get_references', {
file_path: params.file_path,
line: params.line,
character: params.character,
});
}
/**
* Handler: get_hover
*/
async function handleGetHover(params: Params): Promise<LSPResult> {
if (params.line === undefined || params.character === undefined) {
return {
success: false,
error: 'line and character are required for get_hover',
action: 'get_hover',
};
}
return callVSCodeBridge('/get_hover', {
file_path: params.file_path,
line: params.line,
character: params.character,
});
}
/**
* Handler: get_document_symbols
*/
async function handleGetDocumentSymbols(params: Params): Promise<LSPResult> {
return callVSCodeBridge('/get_document_symbols', {
file_path: params.file_path,
});
}
/**
* Main handler function
*/
export async function handler(params: Record<string, unknown>): Promise<ToolResult<LSPResult>> {
try {
// Validate parameters
const validatedParams = ParamsSchema.parse(params);
// Route to appropriate handler based on action
let result: LSPResult;
switch (validatedParams.action) {
case 'get_definition':
result = await handleGetDefinition(validatedParams);
break;
case 'get_references':
result = await handleGetReferences(validatedParams);
break;
case 'get_hover':
result = await handleGetHover(validatedParams);
break;
case 'get_document_symbols':
result = await handleGetDocumentSymbols(validatedParams);
break;
default:
return {
success: false,
error: `Unknown action: ${(validatedParams as any).action}`,
result: null as any,
};
}
if (!result.success) {
return {
success: false,
error: result.error || 'Unknown error',
result: null as any,
};
}
return {
success: true,
result,
};
} catch (err) {
if (err instanceof z.ZodError) {
return {
success: false,
error: `Parameter validation failed: ${err.issues.map((e: z.ZodIssue) => `${e.path.join('.')}: ${e.message}`).join(', ')}`,
result: null as any,
};
}
return {
success: false,
error: `Execution failed: ${(err as Error).message}`,
result: null as any,
};
}
}
/**
* Tool schema for MCP
*/
export const schema: ToolSchema = {
name: 'vscode_lsp',
description: `Access live VSCode LSP features via ccw-vscode-bridge extension.
**Actions:**
- get_definition: Find the definition of a symbol at a given position
- get_references: Find all references to a symbol at a given position
- get_hover: Get hover information for a symbol at a given position
- get_document_symbols: List all symbols in a given file
**Usage Examples:**
Find definition:
vscode_lsp(action="get_definition", file_path="/path/to/file.ts", line=10, character=5)
Find references:
vscode_lsp(action="get_references", file_path="/path/to/file.py", line=42, character=15)
Get hover info:
vscode_lsp(action="get_hover", file_path="/path/to/file.go", line=100, character=20)
List document symbols:
vscode_lsp(action="get_document_symbols", file_path="/path/to/file.rs")
**Requirements:**
- ccw-vscode-bridge extension must be installed and active in VSCode
- File must be part of the VSCode workspace
- VSCode Bridge HTTP server running on http://127.0.0.1:3457`,
inputSchema: {
type: 'object',
properties: {
action: {
type: 'string',
enum: ['get_definition', 'get_references', 'get_hover', 'get_document_symbols'],
description: 'LSP action to perform',
},
file_path: {
type: 'string',
description: 'Absolute path to the file (required)',
},
line: {
type: 'number',
description: 'Line number (1-based, required for definition/references/hover)',
},
character: {
type: 'number',
description: 'Character position (1-based, required for definition/references/hover)',
},
},
required: ['action', 'file_path'],
},
};