From b34a64d4beb2fefa95b7b1d596adf2b8de3350fb Mon Sep 17 00:00:00 2001 From: Phodal Huang Date: Thu, 4 Dec 2025 19:46:26 +0800 Subject: [PATCH 1/4] feat(mpp-vscode): implement CodeLens with Tree-sitter and fix CodeFence parsing ## CodeLens Implementation - Add Tree-sitter based code element parser (code-element-parser.ts) - Support 7 languages: TypeScript, JavaScript, Python, Java, Kotlin, Go, Rust - Implement AutoComment, AutoTest, AutoMethod actions - Add prompt templates for code generation ## CodeFence Fix - Rewrite codeFence.ts to match mpp-core's CodeFence.parseAll logic - Add preProcessDevinBlock to convert ```devin blocks to tags - Handle , , and tags - Fix issue where devin instructions showed extra CodeBlockRenderer ## Other Changes - Remove auto-add current file feature from ChatInput - Add getCurrentModelConfig to ChatViewProvider for CodeLens actions - Add web-tree-sitter and @unit-mesh/treesitter-artifacts dependencies --- mpp-vscode/package.json | 64 ++ mpp-vscode/src/actions/auto-actions.ts | 254 +++++++ mpp-vscode/src/commands/codelens-commands.ts | 253 +++++++ mpp-vscode/src/extension.ts | 30 + mpp-vscode/src/prompts/prompt-templates.ts | 166 +++++ mpp-vscode/src/providers/chat-view.ts | 53 +- .../src/providers/code-element-parser.ts | 645 ++++++++++++++++++ mpp-vscode/src/providers/codelens-provider.ts | 227 ++++++ mpp-vscode/webview/src/App.tsx | 16 - .../webview/src/components/ChatInput.tsx | 15 - .../webview/src/components/TopToolbar.tsx | 20 +- mpp-vscode/webview/src/utils/codeFence.ts | 257 +++++-- 12 files changed, 1895 insertions(+), 105 deletions(-) create mode 100644 mpp-vscode/src/actions/auto-actions.ts create mode 100644 mpp-vscode/src/commands/codelens-commands.ts create mode 100644 mpp-vscode/src/prompts/prompt-templates.ts create mode 100644 mpp-vscode/src/providers/code-element-parser.ts create mode 100644 mpp-vscode/src/providers/codelens-provider.ts diff --git a/mpp-vscode/package.json b/mpp-vscode/package.json index 86f202abfb..8059a166b9 100644 --- a/mpp-vscode/package.json +++ b/mpp-vscode/package.json @@ -44,6 +44,30 @@ { "command": "autodev.runAgent", "title": "AutoDev: Run Coding Agent" + }, + { + "command": "autodev.codelens.quickChat", + "title": "AutoDev: Quick Chat" + }, + { + "command": "autodev.codelens.explainCode", + "title": "AutoDev: Explain Code" + }, + { + "command": "autodev.codelens.optimizeCode", + "title": "AutoDev: Optimize Code" + }, + { + "command": "autodev.codelens.autoComment", + "title": "AutoDev: Auto Comment" + }, + { + "command": "autodev.codelens.autoTest", + "title": "AutoDev: Auto Test" + }, + { + "command": "autodev.codelens.autoMethod", + "title": "AutoDev: Auto Method" } ], "viewsContainers": { @@ -99,6 +123,44 @@ "type": "number", "default": 23120, "description": "Port for the IDE server (MCP protocol)" + }, + "autodev.codelens.enable": { + "type": "boolean", + "default": true, + "description": "Enable CodeLens to show AI actions above functions and classes" + }, + "autodev.codelens.displayMode": { + "type": "string", + "default": "expand", + "enum": [ + "expand", + "collapse" + ], + "enumDescriptions": [ + "Show all actions separately", + "Show a collapsed menu" + ], + "description": "CodeLens display mode" + }, + "autodev.codelens.items": { + "type": "array", + "default": [ + "quickChat", + "autoTest", + "autoComment" + ], + "items": { + "type": "string", + "enum": [ + "quickChat", + "explainCode", + "optimizeCode", + "autoComment", + "autoTest", + "autoMethod" + ] + }, + "description": "CodeLens items to display" } } }, @@ -162,8 +224,10 @@ "dependencies": { "@autodev/mpp-core": "file:../mpp-core/build/packages/js", "@modelcontextprotocol/sdk": "^1.0.0", + "@unit-mesh/treesitter-artifacts": "^1.7.7", "cors": "^2.8.5", "express": "^4.18.2", + "web-tree-sitter": "^0.25.10", "yaml": "^2.8.2", "zod": "^3.22.4" } diff --git a/mpp-vscode/src/actions/auto-actions.ts b/mpp-vscode/src/actions/auto-actions.ts new file mode 100644 index 0000000000..d3dfef876d --- /dev/null +++ b/mpp-vscode/src/actions/auto-actions.ts @@ -0,0 +1,254 @@ +/** + * Auto Actions - AutoComment, AutoTest, AutoMethod implementations + * + * Based on autodev-vscode's action executors, adapted for mpp-vscode. + */ + +import * as vscode from 'vscode'; +import { CodeElement } from '../providers/code-element-parser'; +import { LLMService, ModelConfig } from '../bridge/mpp-core'; +import { DiffManager } from '../services/diff-manager'; +import { + generateAutoDocPrompt, + generateAutoTestPrompt, + generateAutoMethodPrompt, + parseCodeBlock, + LANGUAGE_COMMENT_MAP, + getTestFramework, + getTestFilePath, + AutoDocContext, + AutoTestContext, + AutoMethodContext +} from '../prompts/prompt-templates'; + +export interface ActionContext { + document: vscode.TextDocument; + element: CodeElement; + config: ModelConfig; + log: (message: string) => void; +} + +/** + * Execute AutoComment action - generates documentation comments + */ +export async function executeAutoComment(context: ActionContext): Promise { + const { document, element, config, log } = context; + const language = document.languageId; + + log(`AutoComment: Generating documentation for ${element.name}`); + + const commentSymbols = LANGUAGE_COMMENT_MAP[language] || { start: '/**', end: '*/' }; + + const promptContext: AutoDocContext = { + language, + code: element.code, + startSymbol: commentSymbols.start, + endSymbol: commentSymbols.end, + }; + + const prompt = generateAutoDocPrompt(promptContext); + + try { + await vscode.window.withProgress({ + location: vscode.ProgressLocation.Notification, + title: `Generating documentation for ${element.name}...`, + cancellable: true + }, async (progress, token) => { + const llmService = new LLMService(config); + + let response = ''; + await llmService.streamMessage(prompt, (chunk) => { + response += chunk; + progress.report({ message: 'Generating...' }); + }); + + if (token.isCancellationRequested) return; + + const docComment = parseCodeBlock(response, language); + if (!docComment) { + vscode.window.showWarningMessage('Failed to generate documentation'); + return; + } + + const formattedDoc = formatDocComment(docComment, document, element); + const insertPosition = new vscode.Position(element.bodyRange.start.line, 0); + + const diffManager = new DiffManager(log); + const originalContent = document.getText(); + const newContent = insertTextAtPosition(originalContent, formattedDoc, document.offsetAt(insertPosition)); + + await diffManager.showDiff(document.uri.fsPath, originalContent, newContent); + log(`AutoComment: Documentation generated for ${element.name}`); + }); + } catch (error) { + const message = error instanceof Error ? error.message : String(error); + log(`AutoComment error: ${message}`); + vscode.window.showErrorMessage(`Failed to generate documentation: ${message}`); + } +} + +/** + * Execute AutoTest action - generates unit tests + */ +export async function executeAutoTest(context: ActionContext): Promise { + const { document, element, config, log } = context; + const language = document.languageId; + + log(`AutoTest: Generating tests for ${element.name}`); + + const promptContext: AutoTestContext = { + language, + sourceCode: element.code, + className: element.type === 'structure' ? element.name : undefined, + methodName: element.type === 'method' ? element.name : undefined, + testFramework: getTestFramework(language), + isNewFile: true, + }; + + const prompt = generateAutoTestPrompt(promptContext); + + try { + await vscode.window.withProgress({ + location: vscode.ProgressLocation.Notification, + title: `Generating tests for ${element.name}...`, + cancellable: true + }, async (progress, token) => { + const llmService = new LLMService(config); + + let response = ''; + await llmService.streamMessage(prompt, (chunk) => { + response += chunk; + progress.report({ message: 'Generating...' }); + }); + + if (token.isCancellationRequested) return; + + const testCode = parseCodeBlock(response, language); + if (!testCode) { + vscode.window.showWarningMessage('Failed to generate test code'); + return; + } + + const testFilePath = getTestFilePath(document.uri.fsPath, language); + const testFileUri = vscode.Uri.file(testFilePath); + + let existingContent = ''; + try { + const existingDoc = await vscode.workspace.openTextDocument(testFileUri); + existingContent = existingDoc.getText(); + } catch { /* File doesn't exist */ } + + const diffManager = new DiffManager(log); + if (existingContent) { + const newContent = existingContent + '\n\n' + testCode; + await diffManager.showDiff(testFilePath, existingContent, newContent); + } else { + await diffManager.showDiff(testFilePath, '', testCode); + } + + log(`AutoTest: Tests generated for ${element.name}`); + }); + } catch (error) { + const message = error instanceof Error ? error.message : String(error); + log(`AutoTest error: ${message}`); + vscode.window.showErrorMessage(`Failed to generate tests: ${message}`); + } +} + +/** + * Execute AutoMethod action - generates method implementation + */ +export async function executeAutoMethod(context: ActionContext): Promise { + const { document, element, config, log } = context; + const language = document.languageId; + + log(`AutoMethod: Generating implementation for ${element.name}`); + + const promptContext: AutoMethodContext = { + language, + code: element.code, + methodSignature: extractMethodSignature(element.code), + className: findContainingClass(document, element), + }; + + const prompt = generateAutoMethodPrompt(promptContext); + + try { + await vscode.window.withProgress({ + location: vscode.ProgressLocation.Notification, + title: `Generating implementation for ${element.name}...`, + cancellable: true + }, async (progress, token) => { + const llmService = new LLMService(config); + + let response = ''; + await llmService.streamMessage(prompt, (chunk) => { + response += chunk; + progress.report({ message: 'Generating...' }); + }); + + if (token.isCancellationRequested) return; + + const methodCode = parseCodeBlock(response, language); + if (!methodCode) { + vscode.window.showWarningMessage('Failed to generate method implementation'); + return; + } + + const diffManager = new DiffManager(log); + const originalContent = document.getText(); + const newContent = replaceMethodBody(originalContent, element, methodCode, document); + + await diffManager.showDiff(document.uri.fsPath, originalContent, newContent); + log(`AutoMethod: Implementation generated for ${element.name}`); + }); + } catch (error) { + const message = error instanceof Error ? error.message : String(error); + log(`AutoMethod error: ${message}`); + vscode.window.showErrorMessage(`Failed to generate implementation: ${message}`); + } +} + +// Helper functions + +function formatDocComment(docComment: string, document: vscode.TextDocument, element: CodeElement): string { + const line = document.lineAt(element.bodyRange.start.line); + const indent = line.text.substring(0, line.firstNonWhitespaceCharacterIndex); + + let formatted = docComment.trim(); + if (!formatted.endsWith('\n')) formatted += '\n'; + + const lines = formatted.split('\n'); + return lines.map(l => l ? indent + l : l).join('\n'); +} + +function insertTextAtPosition(content: string, text: string, position: number): string { + return content.substring(0, position) + text + content.substring(position); +} + +function extractMethodSignature(code: string): string { + const lines = code.split('\n'); + const signatureLines: string[] = []; + for (const line of lines) { + signatureLines.push(line); + if (line.includes('{') || line.includes(':')) break; + } + return signatureLines.join('\n').trim(); +} + +function findContainingClass(document: vscode.TextDocument, element: CodeElement): string | undefined { + const text = document.getText(new vscode.Range(new vscode.Position(0, 0), element.bodyRange.start)); + const classMatch = text.match(/class\s+(\w+)/g); + if (classMatch && classMatch.length > 0) { + const lastMatch = classMatch[classMatch.length - 1]; + const nameMatch = lastMatch.match(/class\s+(\w+)/); + return nameMatch ? nameMatch[1] : undefined; + } + return undefined; +} + +function replaceMethodBody(content: string, element: CodeElement, newMethodCode: string, document: vscode.TextDocument): string { + const startOffset = document.offsetAt(element.bodyRange.start); + const endOffset = document.offsetAt(element.bodyRange.end); + return content.substring(0, startOffset) + newMethodCode + content.substring(endOffset); +} diff --git a/mpp-vscode/src/commands/codelens-commands.ts b/mpp-vscode/src/commands/codelens-commands.ts new file mode 100644 index 0000000000..030544d081 --- /dev/null +++ b/mpp-vscode/src/commands/codelens-commands.ts @@ -0,0 +1,253 @@ +/** + * CodeLens 命令实现 + * + * 实现 CodeLens 点击后的各种操作 + */ + +import * as vscode from 'vscode'; +import { CodeElement } from '../providers/code-element-parser'; +import { executeAutoComment, executeAutoTest, executeAutoMethod, ActionContext } from '../actions/auto-actions'; +import { ModelConfig } from '../bridge/mpp-core'; + +export class CodeLensCommands { + constructor( + private log: (message: string) => void, + private getChatViewProvider: () => any | undefined, + private getModelConfig: () => ModelConfig | undefined + ) {} + + /** + * 注册所有 CodeLens 命令 + */ + register(context: vscode.ExtensionContext): vscode.Disposable[] { + return [ + // Quick Chat - 将代码发送到聊天 + vscode.commands.registerCommand( + 'autodev.codelens.quickChat', + async (document: vscode.TextDocument, element: CodeElement) => { + await this.handleQuickChat(document, element); + } + ), + + // Explain Code - 解释代码 + vscode.commands.registerCommand( + 'autodev.codelens.explainCode', + async (document: vscode.TextDocument, element: CodeElement) => { + await this.handleExplainCode(document, element); + } + ), + + // Optimize Code - 优化代码 + vscode.commands.registerCommand( + 'autodev.codelens.optimizeCode', + async (document: vscode.TextDocument, element: CodeElement) => { + await this.handleOptimizeCode(document, element); + } + ), + + // AutoComment - 生成文档注释 + vscode.commands.registerCommand( + 'autodev.codelens.autoComment', + async (document: vscode.TextDocument, element: CodeElement) => { + await this.handleAutoComment(document, element); + } + ), + + // AutoTest - 生成测试 + vscode.commands.registerCommand( + 'autodev.codelens.autoTest', + async (document: vscode.TextDocument, element: CodeElement) => { + await this.handleAutoTest(document, element); + } + ), + + // AutoMethod - 方法补全 + vscode.commands.registerCommand( + 'autodev.codelens.autoMethod', + async (document: vscode.TextDocument, element: CodeElement) => { + await this.handleAutoMethod(document, element); + } + ), + + // Show Menu - 显示折叠菜单 + vscode.commands.registerCommand( + 'autodev.codelens.showMenu', + async (commands: vscode.Command[]) => { + await this.handleShowMenu(commands); + } + ) + ]; + } + + /** + * Quick Chat: 将代码发送到聊天 + */ + private async handleQuickChat(document: vscode.TextDocument, element: CodeElement) { + this.log(`Quick Chat: ${element.type} ${element.name}`); + + const chatView = this.getChatViewProvider(); + if (!chatView) { + vscode.window.showWarningMessage('Chat view not available'); + return; + } + + await vscode.commands.executeCommand('autodev.chatView.focus'); + const codeContext = this.buildCodeContext(document, element); + chatView.sendCodeContext(codeContext); + } + + /** + * Explain Code: 解释代码 + */ + private async handleExplainCode(document: vscode.TextDocument, element: CodeElement) { + this.log(`Explain Code: ${element.type} ${element.name}`); + + const chatView = this.getChatViewProvider(); + if (!chatView) { + vscode.window.showWarningMessage('Chat view not available'); + return; + } + + await vscode.commands.executeCommand('autodev.chatView.focus'); + const codeContext = this.buildCodeContext(document, element); + chatView.sendCodeContext(codeContext); + + setTimeout(() => { + chatView.sendMessage('Explain this code in detail, including:\n1. What it does\n2. How it works\n3. Any potential issues or improvements'); + }, 300); + } + + /** + * Optimize Code: 优化代码 + */ + private async handleOptimizeCode(document: vscode.TextDocument, element: CodeElement) { + this.log(`Optimize Code: ${element.type} ${element.name}`); + + const chatView = this.getChatViewProvider(); + if (!chatView) { + vscode.window.showWarningMessage('Chat view not available'); + return; + } + + await vscode.commands.executeCommand('autodev.chatView.focus'); + const codeContext = this.buildCodeContext(document, element); + chatView.sendCodeContext(codeContext); + + setTimeout(() => { + chatView.sendMessage('Optimize this code for better performance, readability, and maintainability'); + }, 300); + } + + /** + * AutoComment: 生成文档注释 + */ + private async handleAutoComment(document: vscode.TextDocument, element: CodeElement) { + this.log(`AutoComment: ${element.type} ${element.name}`); + + const config = this.getModelConfig(); + if (!config) { + vscode.window.showWarningMessage('Please configure a model first'); + return; + } + + const context: ActionContext = { + document, + element, + config, + log: this.log + }; + + await executeAutoComment(context); + } + + /** + * AutoTest: 生成测试 + */ + private async handleAutoTest(document: vscode.TextDocument, element: CodeElement) { + this.log(`AutoTest: ${element.type} ${element.name}`); + + const config = this.getModelConfig(); + if (!config) { + vscode.window.showWarningMessage('Please configure a model first'); + return; + } + + const context: ActionContext = { + document, + element, + config, + log: this.log + }; + + await executeAutoTest(context); + } + + /** + * AutoMethod: 方法补全 + */ + private async handleAutoMethod(document: vscode.TextDocument, element: CodeElement) { + this.log(`AutoMethod: ${element.type} ${element.name}`); + + const config = this.getModelConfig(); + if (!config) { + vscode.window.showWarningMessage('Please configure a model first'); + return; + } + + const context: ActionContext = { + document, + element, + config, + log: this.log + }; + + await executeAutoMethod(context); + } + + /** + * Show Menu: 显示折叠菜单 + */ + private async handleShowMenu(commands: vscode.Command[]) { + const items = commands.map(cmd => ({ + label: cmd.title, + description: cmd.tooltip, + command: cmd + })); + + const selected = await vscode.window.showQuickPick(items, { + placeHolder: 'Select an action' + }); + + if (selected && selected.command.command) { + await vscode.commands.executeCommand( + selected.command.command, + ...(selected.command.arguments || []) + ); + } + } + + /** + * 构建代码上下文 + */ + private buildCodeContext(document: vscode.TextDocument, element: CodeElement) { + return { + filepath: document.uri.fsPath, + language: document.languageId, + element: { + type: element.type, + name: element.name, + code: element.code, + range: { + start: { + line: element.bodyRange.start.line, + character: element.bodyRange.start.character + }, + end: { + line: element.bodyRange.end.line, + character: element.bodyRange.end.character + } + } + } + }; + } +} diff --git a/mpp-vscode/src/extension.ts b/mpp-vscode/src/extension.ts index 1ec3be2c86..7905971d5e 100644 --- a/mpp-vscode/src/extension.ts +++ b/mpp-vscode/src/extension.ts @@ -10,6 +10,8 @@ import { DiffManager, DiffContentProvider } from './services/diff-manager'; import { ChatViewProvider } from './providers/chat-view'; import { StatusBarManager } from './services/status-bar'; import { registerDevInsCompletionProvider } from './providers/devins-completion'; +import { AutoDevCodeLensProvider } from './providers/codelens-provider'; +import { CodeLensCommands } from './commands/codelens-commands'; import { createLogger } from './utils/logger'; export const DIFF_SCHEME = 'autodev-diff'; @@ -124,6 +126,34 @@ export async function activate(context: vscode.ExtensionContext) { context.subscriptions.push(registerDevInsCompletionProvider(context)); log('DevIns language support registered'); + // Register CodeLens Provider + const codeLensProvider = new AutoDevCodeLensProvider(log); + const codeLensCommands = new CodeLensCommands(log, () => chatViewProvider, () => chatViewProvider.getCurrentModelConfig()); + + // Register for supported languages + const supportedLanguages = [ + 'typescript', 'javascript', 'typescriptreact', 'javascriptreact', + 'python', 'java', 'kotlin', 'go', 'rust', 'csharp', 'cpp', 'c' + ]; + + context.subscriptions.push( + vscode.languages.registerCodeLensProvider( + supportedLanguages.map(lang => ({ language: lang })), + codeLensProvider + ), + ...codeLensCommands.register(context) + ); + log('CodeLens Provider registered'); + + // Configuration change listener - refresh CodeLens + context.subscriptions.push( + vscode.workspace.onDidChangeConfiguration((e) => { + if (e.affectsConfiguration('autodev.codelens')) { + codeLensProvider.refresh(); + } + }) + ); + // Show welcome message on first install const welcomeShownKey = 'autodev.welcomeShown'; if (!context.globalState.get(welcomeShownKey)) { diff --git a/mpp-vscode/src/prompts/prompt-templates.ts b/mpp-vscode/src/prompts/prompt-templates.ts new file mode 100644 index 0000000000..4b0c115030 --- /dev/null +++ b/mpp-vscode/src/prompts/prompt-templates.ts @@ -0,0 +1,166 @@ +/** + * Prompt Templates for CodeLens Actions + * + * Based on autodev-vscode's Velocity templates, converted to TypeScript template strings. + */ + +export interface AutoDocContext { + language: string; + code: string; + startSymbol: string; + endSymbol: string; + originalComments?: string[]; + chatContext?: string; +} + +export interface AutoTestContext { + language: string; + sourceCode: string; + className?: string; + methodName?: string; + imports?: string; + relatedClasses?: string; + chatContext?: string; + isNewFile?: boolean; + testFramework?: string; +} + +export interface AutoMethodContext { + language: string; + code: string; + methodSignature: string; + className?: string; + chatContext?: string; +} + +export const LANGUAGE_COMMENT_MAP: Record = { + typescript: { start: '/**', end: '*/' }, + javascript: { start: '/**', end: '*/' }, + typescriptreact: { start: '/**', end: '*/' }, + javascriptreact: { start: '/**', end: '*/' }, + java: { start: '/**', end: '*/' }, + kotlin: { start: '/**', end: '*/' }, + python: { start: '"""', end: '"""' }, + go: { start: '/*', end: '*/' }, + rust: { start: '///', end: '' }, + csharp: { start: '///', end: '' }, +}; + +export function generateAutoDocPrompt(context: AutoDocContext): string { + let prompt = `Write documentation for the following ${context.language} code.\n\n`; + if (context.chatContext) prompt += `Additional context:\n${context.chatContext}\n\n`; + prompt += `Requirements: +- Start your documentation with \`${context.startSymbol}\` and end with \`${context.endSymbol}\` +- Include a brief description of what the code does +- Document parameters and return values if applicable +- Use proper ${context.language} documentation format + +Here is the code: +\`\`\`${context.language} +${context.code} +\`\`\` + +Please write the documentation inside a Markdown code block.`; + return prompt; +} + +export function generateAutoTestPrompt(context: AutoTestContext): string { + let prompt = `Write unit tests for the following ${context.language} code.\n\n`; + if (context.chatContext) prompt += `Additional context:\n${context.chatContext}\n\n`; + if (context.relatedClasses) prompt += `Related classes:\n${context.relatedClasses}\n\n`; + if (context.imports) prompt += `Imports used:\n${context.imports}\n\n`; + if (context.testFramework) prompt += `Use ${context.testFramework} testing framework.\n\n`; + prompt += `Source code to test: +\`\`\`${context.language} +${context.sourceCode} +\`\`\` + +Requirements: +- Write comprehensive unit tests covering main functionality +- Include edge cases and error handling tests +- Use descriptive test names +- Follow ${context.language} testing best practices +`; + if (context.className) prompt += `Generate tests for class: ${context.className}\n`; + if (context.methodName) prompt += `Focus on method: ${context.methodName}\n`; + prompt += `\nStart the test code with a ${context.language} Markdown code block:`; + return prompt; +} + +export function generateAutoMethodPrompt(context: AutoMethodContext): string { + let prompt = `Complete the implementation for the following ${context.language} method.\n\n`; + if (context.chatContext) prompt += `Additional context:\n${context.chatContext}\n\n`; + if (context.className) prompt += `Class: ${context.className}\n`; + prompt += `Method signature: +\`\`\`${context.language} +${context.methodSignature} +\`\`\` + +Current code context: +\`\`\`${context.language} +${context.code} +\`\`\` + +Requirements: +- Implement the method body based on the signature and context +- Follow ${context.language} best practices +- Handle edge cases appropriately +- Add inline comments for complex logic + +Please provide the complete method implementation inside a Markdown code block.`; + return prompt; +} + +export function parseCodeBlock(response: string, language?: string): string { + const codeBlockRegex = /```(?:\w+)?\n([\s\S]*?)```/g; + const matches = [...response.matchAll(codeBlockRegex)]; + if (matches.length > 0) return matches[0][1].trim(); + return response.trim(); +} + +export function getTestFramework(language: string): string { + const frameworks: Record = { + typescript: 'Jest or Vitest', + javascript: 'Jest or Vitest', + typescriptreact: 'Jest with React Testing Library', + javascriptreact: 'Jest with React Testing Library', + java: 'JUnit 5', + kotlin: 'JUnit 5 or Kotest', + python: 'pytest', + go: 'testing package', + rust: 'built-in test framework', + csharp: 'xUnit or NUnit', + }; + return frameworks[language] || 'appropriate testing framework'; +} + +export function getTestFilePath(sourcePath: string, language: string): string { + const pathParts = sourcePath.split('/'); + const fileName = pathParts.pop() || ''; + const dirPath = pathParts.join('/'); + const extIndex = fileName.lastIndexOf('.'); + const baseName = extIndex > 0 ? fileName.substring(0, extIndex) : fileName; + const ext = extIndex > 0 ? fileName.substring(extIndex) : ''; + + const testSuffixes: Record = { + typescript: '.test.ts', + javascript: '.test.js', + typescriptreact: '.test.tsx', + javascriptreact: '.test.jsx', + java: 'Test.java', + kotlin: 'Test.kt', + python: '_test.py', + go: '_test.go', + rust: '', + csharp: 'Tests.cs', + }; + + const testSuffix = testSuffixes[language] || `.test${ext}`; + + if (language === 'java' || language === 'kotlin') { + const testDir = dirPath.replace('/src/main/', '/src/test/'); + return `${testDir}/${baseName}${testSuffix}`; + } + if (language === 'python') return `${dirPath}/test_${baseName}.py`; + return `${dirPath}/${baseName}${testSuffix}`; +} diff --git a/mpp-vscode/src/providers/chat-view.ts b/mpp-vscode/src/providers/chat-view.ts index 6756e7ec02..9f7ddb93dc 100644 --- a/mpp-vscode/src/providers/chat-view.ts +++ b/mpp-vscode/src/providers/chat-view.ts @@ -228,6 +228,39 @@ export class ChatViewProvider implements vscode.WebviewViewProvider, vscode.Disp await this.handleUserMessage(content); } + /** + * Send code context to the chat (for CodeLens integration) + */ + sendCodeContext(context: { + filepath: string; + language: string; + element: { + type: string; + name: string; + code: string; + range: { + start: { line: number; character: number }; + end: { line: number; character: number }; + }; + }; + }) { + // Format code context message + const codeBlock = `\`\`\`${context.language}\n${context.element.code}\n\`\`\``; + const message = `**${context.element.type}: ${context.element.name}**\n\nFile: \`${context.filepath}\`\n\n${codeBlock}`; + + // Show the view if not visible + if (this.webviewView) { + this.webviewView.show(true); + } + + // Send the formatted message to webview + this.postMessage({ + type: 'addCodeContext', + content: message, + context: context + }); + } + /** * Post a message to the webview */ @@ -1050,5 +1083,23 @@ User's original prompt:`; `; } -} + /** + * Get current model config for CodeLens actions + */ + getCurrentModelConfig(): { provider: string; model: string; apiKey: string; temperature?: number; maxTokens?: number; baseUrl?: string } | undefined { + if (!this.configWrapper) return undefined; + + const activeConfig = this.configWrapper.getActiveConfig(); + if (!activeConfig) return undefined; + + return { + provider: activeConfig.provider, + model: activeConfig.model, + apiKey: activeConfig.apiKey || '', + temperature: activeConfig.temperature, + maxTokens: activeConfig.maxTokens, + baseUrl: activeConfig.baseUrl + }; + } +} diff --git a/mpp-vscode/src/providers/code-element-parser.ts b/mpp-vscode/src/providers/code-element-parser.ts new file mode 100644 index 0000000000..83dc8486f0 --- /dev/null +++ b/mpp-vscode/src/providers/code-element-parser.ts @@ -0,0 +1,645 @@ +/** + * Code Element Parser using Tree-sitter + * + * Based on autodev-vscode's NamedElementBuilder implementation. + * Uses web-tree-sitter for accurate AST-based code parsing. + */ + +import * as vscode from 'vscode'; +import type { Language, Query, SyntaxNode, Tree } from 'web-tree-sitter'; + +// Dynamic import for web-tree-sitter (CommonJS module) +let Parser: any = null; +async function getParser(): Promise { + if (!Parser) { + Parser = require('web-tree-sitter'); + } + return Parser; +} + +export enum CodeElementType { + Structure = 'structure', + Method = 'method', + Function = 'function' +} + +export interface CodeElement { + type: CodeElementType; + name: string; + nameRange: vscode.Range; + bodyRange: vscode.Range; + code: string; +} + +interface TextInRange { + text: string; + startLine: number; + startColumn: number; + endLine: number; + endColumn: number; +} + +/** + * Memoized query for caching compiled tree-sitter queries + */ +class MemoizedQuery { + private readonly queryStr: string; + private compiledQuery: Query | undefined; + + constructor(queryStr: string) { + this.queryStr = queryStr; + } + + query(language: Language): Query { + if (this.compiledQuery) { + return this.compiledQuery; + } + this.compiledQuery = language.query(this.queryStr); + return this.compiledQuery; + } +} + +/** + * Language profile for tree-sitter parsing + */ +interface LanguageProfile { + languageIds: string[]; + classQuery: MemoizedQuery; + methodQuery: MemoizedQuery; + autoSelectInsideParent: string[]; +} + +// Language profiles based on autodev-vscode +const LANGUAGE_PROFILES: Record = { + typescript: { + languageIds: ['typescript', 'typescriptreact'], + classQuery: new MemoizedQuery(` + (class_declaration + (type_identifier) @name.definition.class) @definition.class + `), + methodQuery: new MemoizedQuery(` + (function_declaration + (identifier) @name.definition.method) @definition.method + + (generator_function_declaration + name: (identifier) @name.identifier.method + ) @definition.method + + (export_statement + declaration: (lexical_declaration + (variable_declarator + name: (identifier) @name.identifier.method + value: (arrow_function) + ) + ) @definition.method + ) + + (class_declaration + name: (type_identifier) + body: (class_body + ((method_definition + name: (property_identifier) @name.definition.method + ) @definition.method) + ) + ) + `), + autoSelectInsideParent: ['export_statement'] + }, + javascript: { + languageIds: ['javascript', 'javascriptreact'], + classQuery: new MemoizedQuery(` + (class_declaration + (identifier) @name.definition.class) @definition.class + `), + methodQuery: new MemoizedQuery(` + (function_declaration + (identifier) @name.definition.method) @definition.method + + (generator_function_declaration + name: (identifier) @name.identifier.method + ) @definition.method + + (export_statement + declaration: (lexical_declaration + (variable_declarator + name: (identifier) @name.identifier.method + value: (arrow_function) + ) + ) @definition.method + ) + + (class_declaration + name: (identifier) + body: (class_body + ((method_definition + name: (property_identifier) @name.definition.method + ) @definition.method) + ) + ) + `), + autoSelectInsideParent: ['export_statement'] + }, + python: { + languageIds: ['python'], + classQuery: new MemoizedQuery(` + (class_definition + (identifier) @type_identifier) @type_declaration + `), + methodQuery: new MemoizedQuery(` + (function_definition + name: (identifier) @name.definition.method + ) @definition.method + `), + autoSelectInsideParent: [] + }, + java: { + languageIds: ['java'], + classQuery: new MemoizedQuery(` + (class_declaration + name: (identifier) @name.definition.class) @definition.class + `), + methodQuery: new MemoizedQuery(` + (method_declaration + name: (identifier) @name.definition.method) @definition.method + `), + autoSelectInsideParent: [] + }, + kotlin: { + languageIds: ['kotlin'], + classQuery: new MemoizedQuery(` + (class_declaration + (type_identifier) @name.definition.class) @definition.class + `), + methodQuery: new MemoizedQuery(` + (function_declaration + (simple_identifier) @name.definition.method) @definition.method + `), + autoSelectInsideParent: [] + }, + go: { + languageIds: ['go'], + classQuery: new MemoizedQuery(` + (type_declaration + (type_spec + name: (_) @type-name + type: (struct_type) + ) + ) @type_declaration + `), + methodQuery: new MemoizedQuery(` + (function_declaration + name: (identifier) @function-name) @function-body + + (method_declaration + receiver: (_)? @receiver-struct-name + name: (_)? @method-name) @method-body + `), + autoSelectInsideParent: [] + }, + rust: { + languageIds: ['rust'], + classQuery: new MemoizedQuery(` + (struct_item (type_identifier) @type_identifier) @type_declaration + `), + methodQuery: new MemoizedQuery(` + (function_item (identifier) @name.definition.method) @definition.method + `), + autoSelectInsideParent: [] + } +}; + +export class CodeElementParser { + private static parserInstance: any = null; + private static ParserClass: any = null; + private static languages: Map = new Map(); + private static initPromise: Promise | null = null; + + constructor(private log: (message: string) => void) {} + + /** + * Initialize tree-sitter parser + */ + private async initialize(): Promise { + if (CodeElementParser.parserInstance) { + return; + } + + if (CodeElementParser.initPromise) { + return CodeElementParser.initPromise; + } + + CodeElementParser.initPromise = this.doInitialize(); + return CodeElementParser.initPromise; + } + + private async doInitialize(): Promise { + try { + CodeElementParser.ParserClass = await getParser(); + await CodeElementParser.ParserClass.init(); + CodeElementParser.parserInstance = new CodeElementParser.ParserClass(); + this.log('Tree-sitter parser initialized'); + } catch (error) { + this.log(`Failed to initialize tree-sitter: ${error}`); + throw error; + } + } + + /** + * Get or load language grammar + */ + private async getLanguage(langId: string): Promise { + const cached = CodeElementParser.languages.get(langId); + if (cached) { + return cached; + } + + try { + // Try to load from extension's bundled WASM files + const wasmPath = this.getWasmPath(langId); + if (!wasmPath) { + return null; + } + + const language = await CodeElementParser.ParserClass.Language.load(wasmPath); + CodeElementParser.languages.set(langId, language); + return language; + } catch (error) { + this.log(`Failed to load language ${langId}: ${error}`); + return null; + } + } + + /** + * Get WASM file path for language + */ + private getWasmPath(langId: string): string | null { + // Map language IDs to tree-sitter grammar names + const grammarMap: Record = { + 'typescript': 'tree-sitter-typescript', + 'typescriptreact': 'tree-sitter-tsx', + 'javascript': 'tree-sitter-javascript', + 'javascriptreact': 'tree-sitter-javascript', + 'python': 'tree-sitter-python', + 'java': 'tree-sitter-java', + 'kotlin': 'tree-sitter-kotlin', + 'go': 'tree-sitter-go', + 'rust': 'tree-sitter-rust' + }; + + const grammarName = grammarMap[langId]; + if (!grammarName) { + return null; + } + + // In VSCode extension context, we need to resolve the path + // This assumes WASM files are bundled in the extension + try { + return require.resolve(`@unit-mesh/treesitter-artifacts/wasm/${grammarName}.wasm`); + } catch { + this.log(`WASM file not found for ${langId}`); + return null; + } + } + + /** + * Get language profile for a language ID + */ + private getProfile(langId: string): LanguageProfile | null { + // Check direct match + if (LANGUAGE_PROFILES[langId]) { + return LANGUAGE_PROFILES[langId]; + } + + // Check if any profile includes this language ID + for (const profile of Object.values(LANGUAGE_PROFILES)) { + if (profile.languageIds.includes(langId)) { + return profile; + } + } + + return null; + } + + /** + * Parse document and extract code elements + */ + async parseDocument(document: vscode.TextDocument): Promise { + const langId = document.languageId; + const profile = this.getProfile(langId); + + if (!profile) { + this.log(`Language ${langId} not supported for CodeLens`); + return []; + } + + try { + await this.initialize(); + + const language = await this.getLanguage(langId); + if (!language || !CodeElementParser.parserInstance) { + // Fallback to regex-based parsing + this.log(`Tree-sitter not available for ${langId}, using regex fallback`); + return this.parseWithRegex(document); + } + + CodeElementParser.parserInstance.setLanguage(language); + const tree = CodeElementParser.parserInstance.parse(document.getText()); + const elements: CodeElement[] = []; + + // Parse classes/structures + const classElements = this.buildBlock( + tree.rootNode, + profile.classQuery, + language, + CodeElementType.Structure, + profile.autoSelectInsideParent, + document + ); + elements.push(...classElements); + + // Parse methods/functions + const methodElements = this.buildBlock( + tree.rootNode, + profile.methodQuery, + language, + CodeElementType.Method, + profile.autoSelectInsideParent, + document + ); + elements.push(...methodElements); + + return elements; + } catch (error) { + this.log(`Error parsing document: ${error}`); + return this.parseWithRegex(document); + } + } + + /** + * Build code elements from tree-sitter query matches + */ + private buildBlock( + rootNode: SyntaxNode, + memoizedQuery: MemoizedQuery, + language: Language, + elementType: CodeElementType, + autoSelectInsideParent: string[], + document: vscode.TextDocument + ): CodeElement[] { + try { + const query = memoizedQuery.query(language); + const matches = query.matches(rootNode); + + return matches.flatMap(match => { + let blockNode = match.captures[0]?.node; + const idNode = match.captures[1]?.node; + + if (!blockNode || !idNode) { + return []; + } + + // Handle autoSelectInsideParent + if (autoSelectInsideParent.length > 0) { + for (const nodeType of autoSelectInsideParent) { + if (blockNode.parent?.type === nodeType) { + blockNode = blockNode.parent; + } + } + } + + const nameRange = new vscode.Range( + new vscode.Position(idNode.startPosition.row, idNode.startPosition.column), + new vscode.Position(idNode.endPosition.row, idNode.endPosition.column) + ); + + const bodyRange = new vscode.Range( + new vscode.Position(blockNode.startPosition.row, blockNode.startPosition.column), + new vscode.Position(blockNode.endPosition.row, blockNode.endPosition.column) + ); + + return [{ + type: elementType, + name: idNode.text, + nameRange, + bodyRange, + code: blockNode.text + }]; + }); + } catch (error) { + this.log(`Error building block: ${error}`); + return []; + } + } + + /** + * Fallback regex-based parsing for when tree-sitter is not available + */ + private parseWithRegex(document: vscode.TextDocument): CodeElement[] { + const language = document.languageId; + const text = document.getText(); + + switch (language) { + case 'typescript': + case 'javascript': + case 'typescriptreact': + case 'javascriptreact': + return this.parseTypeScriptRegex(text, document); + case 'python': + return this.parsePythonRegex(text, document); + case 'java': + case 'kotlin': + return this.parseJavaLikeRegex(text, document); + case 'go': + return this.parseGoRegex(text, document); + case 'rust': + return this.parseRustRegex(text, document); + default: + return []; + } + } + + private parseTypeScriptRegex(text: string, document: vscode.TextDocument): CodeElement[] { + const elements: CodeElement[] = []; + const classRegex = /^\s*(export\s+)?(abstract\s+)?class\s+(\w+)/gm; + let match; + while ((match = classRegex.exec(text)) !== null) { + const name = match[3]; + const startPos = document.positionAt(match.index); + const nameStartPos = document.positionAt(match.index + match[0].lastIndexOf(name)); + const bodyRange = this.findBlockRange(document, startPos); + elements.push({ + type: CodeElementType.Structure, + name, + nameRange: new vscode.Range(nameStartPos, nameStartPos.translate(0, name.length)), + bodyRange, + code: document.getText(bodyRange) + }); + } + + const functionRegex = /^\s*(export\s+)?(async\s+)?(?:function\s+(\w+)|(?:const|let|var)\s+(\w+)\s*=\s*(?:async\s+)?\([^)]*\)\s*=>)/gm; + while ((match = functionRegex.exec(text)) !== null) { + const name = match[3] || match[4]; + if (!name) continue; + const startPos = document.positionAt(match.index); + const nameStartPos = document.positionAt(match.index + match[0].lastIndexOf(name)); + const bodyRange = this.findBlockRange(document, startPos); + elements.push({ + type: CodeElementType.Method, + name, + nameRange: new vscode.Range(nameStartPos, nameStartPos.translate(0, name.length)), + bodyRange, + code: document.getText(bodyRange) + }); + } + return elements; + } + + private parsePythonRegex(text: string, document: vscode.TextDocument): CodeElement[] { + const elements: CodeElement[] = []; + const classRegex = /^class\s+(\w+)/gm; + let match; + while ((match = classRegex.exec(text)) !== null) { + const name = match[1]; + const startPos = document.positionAt(match.index); + const nameStartPos = document.positionAt(match.index + match[0].indexOf(name)); + const bodyRange = this.findPythonBlockRange(document, startPos); + elements.push({ + type: CodeElementType.Structure, + name, + nameRange: new vscode.Range(nameStartPos, nameStartPos.translate(0, name.length)), + bodyRange, + code: document.getText(bodyRange) + }); + } + + const functionRegex = /^(\s*)def\s+(\w+)\s*\(/gm; + while ((match = functionRegex.exec(text)) !== null) { + const name = match[2]; + const startPos = document.positionAt(match.index); + const nameStartPos = document.positionAt(match.index + match[0].indexOf(name)); + const bodyRange = this.findPythonBlockRange(document, startPos); + elements.push({ + type: CodeElementType.Method, + name, + nameRange: new vscode.Range(nameStartPos, nameStartPos.translate(0, name.length)), + bodyRange, + code: document.getText(bodyRange) + }); + } + return elements; + } + + private parseJavaLikeRegex(text: string, document: vscode.TextDocument): CodeElement[] { + const elements: CodeElement[] = []; + const classRegex = /^\s*(public|private|protected)?\s*(abstract|final)?\s*(class|interface)\s+(\w+)/gm; + let match; + while ((match = classRegex.exec(text)) !== null) { + const name = match[4]; + const startPos = document.positionAt(match.index); + const nameStartPos = document.positionAt(match.index + match[0].lastIndexOf(name)); + const bodyRange = this.findBlockRange(document, startPos); + elements.push({ + type: CodeElementType.Structure, + name, + nameRange: new vscode.Range(nameStartPos, nameStartPos.translate(0, name.length)), + bodyRange, + code: document.getText(bodyRange) + }); + } + + const methodRegex = /^\s*(public|private|protected)?\s*(static|final|abstract)?\s*(\w+)\s+(\w+)\s*\([^)]*\)\s*{/gm; + while ((match = methodRegex.exec(text)) !== null) { + const name = match[4]; + const startPos = document.positionAt(match.index); + const nameStartPos = document.positionAt(match.index + match[0].lastIndexOf(name)); + const bodyRange = this.findBlockRange(document, startPos); + elements.push({ + type: CodeElementType.Method, + name, + nameRange: new vscode.Range(nameStartPos, nameStartPos.translate(0, name.length)), + bodyRange, + code: document.getText(bodyRange) + }); + } + return elements; + } + + private parseGoRegex(text: string, document: vscode.TextDocument): CodeElement[] { + const elements: CodeElement[] = []; + const functionRegex = /^func\s+(?:\([^)]+\)\s+)?(\w+)\s*\(/gm; + let match; + while ((match = functionRegex.exec(text)) !== null) { + const name = match[1]; + const startPos = document.positionAt(match.index); + const nameStartPos = document.positionAt(match.index + match[0].lastIndexOf(name)); + const bodyRange = this.findBlockRange(document, startPos); + elements.push({ + type: CodeElementType.Method, + name, + nameRange: new vscode.Range(nameStartPos, nameStartPos.translate(0, name.length)), + bodyRange, + code: document.getText(bodyRange) + }); + } + return elements; + } + + private parseRustRegex(text: string, document: vscode.TextDocument): CodeElement[] { + const elements: CodeElement[] = []; + const functionRegex = /^\s*(pub\s+)?fn\s+(\w+)\s*\(/gm; + let match; + while ((match = functionRegex.exec(text)) !== null) { + const name = match[2]; + const startPos = document.positionAt(match.index); + const nameStartPos = document.positionAt(match.index + match[0].lastIndexOf(name)); + const bodyRange = this.findBlockRange(document, startPos); + elements.push({ + type: CodeElementType.Method, + name, + nameRange: new vscode.Range(nameStartPos, nameStartPos.translate(0, name.length)), + bodyRange, + code: document.getText(bodyRange) + }); + } + return elements; + } + + private findBlockRange(document: vscode.TextDocument, startPos: vscode.Position): vscode.Range { + const text = document.getText(); + let offset = document.offsetAt(startPos); + while (offset < text.length && text[offset] !== '{') { + offset++; + } + if (offset >= text.length) { + return new vscode.Range(startPos, startPos); + } + let depth = 1; + offset++; + while (offset < text.length && depth > 0) { + if (text[offset] === '{') depth++; + else if (text[offset] === '}') depth--; + offset++; + } + return new vscode.Range(startPos, document.positionAt(offset)); + } + + private findPythonBlockRange(document: vscode.TextDocument, startPos: vscode.Position): vscode.Range { + const startLine = startPos.line; + const startIndent = document.lineAt(startLine).firstNonWhitespaceCharacterIndex; + let endLine = startLine + 1; + while (endLine < document.lineCount) { + const line = document.lineAt(endLine); + if (line.isEmptyOrWhitespace) { + endLine++; + continue; + } + const indent = line.firstNonWhitespaceCharacterIndex; + if (indent <= startIndent) { + break; + } + endLine++; + } + return new vscode.Range(startPos, new vscode.Position(endLine - 1, document.lineAt(endLine - 1).text.length)); + } +} + diff --git a/mpp-vscode/src/providers/codelens-provider.ts b/mpp-vscode/src/providers/codelens-provider.ts new file mode 100644 index 0000000000..0d25ca0f50 --- /dev/null +++ b/mpp-vscode/src/providers/codelens-provider.ts @@ -0,0 +1,227 @@ +/** + * CodeLens Provider - 在函数/类上方显示操作按钮 + * + * 提供的操作: + * - Quick Chat: 将代码发送到聊天 + * - Explain Code: 解释代码 + * - Optimize Code: 优化代码 + * - AutoComment: 生成文档注释 + * - AutoTest: 生成测试代码 + * - AutoMethod: 方法补全 + */ + +import * as vscode from 'vscode'; +import { CodeElementParser, CodeElement, CodeElementType } from './code-element-parser'; + +export type CodeLensAction = + | 'quickChat' + | 'explainCode' + | 'optimizeCode' + | 'autoComment' + | 'autoTest' + | 'autoMethod'; + +export class AutoDevCodeLensProvider implements vscode.CodeLensProvider { + private parser: CodeElementParser; + private _onDidChangeCodeLenses: vscode.EventEmitter = new vscode.EventEmitter(); + public readonly onDidChangeCodeLenses: vscode.Event = this._onDidChangeCodeLenses.event; + + constructor( + private log: (message: string) => void + ) { + this.parser = new CodeElementParser(log); + } + + /** + * 刷新 CodeLens + */ + public refresh(): void { + this._onDidChangeCodeLenses.fire(); + } + + /** + * 提供 CodeLens + */ + async provideCodeLenses( + document: vscode.TextDocument, + token: vscode.CancellationToken + ): Promise { + const config = vscode.workspace.getConfiguration('autodev'); + + // 检查是否启用 + const enabled = config.get('codelens.enable', true); + if (!enabled) { + return []; + } + + // 检查文件大小限制 (避免大文件解析) + if (document.lineCount > 10000) { + this.log(`File too large (${document.lineCount} lines), skipping CodeLens`); + return []; + } + + try { + // 解析代码元素 + const elements = await this.parser.parseDocument(document); + if (token.isCancellationRequested || elements.length === 0) { + return []; + } + + // 获取配置的显示项 + const displayMode = config.get('codelens.displayMode', 'expand'); + const displayItems = new Set(config.get('codelens.items', [ + 'quickChat', + 'autoTest', + 'autoComment' + ])); + + // 构建 CodeLens 组 + const groups = this.buildCodeLensGroups(elements, displayItems, document); + + // 根据显示模式返回 + if (displayMode === 'collapse') { + return groups.map(group => this.buildCollapsedCodeLens(group)); + } + + return groups.flat(); + } catch (error) { + this.log(`Error providing CodeLens: ${error instanceof Error ? error.message : String(error)}`); + return []; + } + } + + /** + * 构建 CodeLens 组(每个元素一组) + */ + private buildCodeLensGroups( + elements: CodeElement[], + displayItems: Set, + document: vscode.TextDocument + ): vscode.CodeLens[][] { + const groups: vscode.CodeLens[][] = []; + + for (const element of elements) { + const codeLenses: vscode.CodeLens[] = []; + + for (const action of displayItems) { + // AutoTest 只在非测试文件中显示 + if (action === 'autoTest' && this.isTestFile(document.fileName)) { + continue; + } + + // AutoMethod 只在方法中显示 + if (action === 'autoMethod' && element.type !== CodeElementType.Method) { + continue; + } + + const lens = this.createCodeLens(element, action, document); + if (lens) { + codeLenses.push(lens); + } + } + + if (codeLenses.length > 0) { + groups.push(codeLenses); + } + } + + return groups; + } + + /** + * 创建单个 CodeLens + */ + private createCodeLens( + element: CodeElement, + action: CodeLensAction, + document: vscode.TextDocument + ): vscode.CodeLens | null { + const range = element.nameRange; + + switch (action) { + case 'quickChat': + return new vscode.CodeLens(range, { + title: '💬 Quick Chat', + command: 'autodev.codelens.quickChat', + arguments: [document, element] + }); + + case 'explainCode': + return new vscode.CodeLens(range, { + title: '📖 Explain', + command: 'autodev.codelens.explainCode', + arguments: [document, element] + }); + + case 'optimizeCode': + return new vscode.CodeLens(range, { + title: '⚡ Optimize', + command: 'autodev.codelens.optimizeCode', + arguments: [document, element] + }); + + case 'autoComment': + return new vscode.CodeLens(range, { + title: '📝 AutoComment', + command: 'autodev.codelens.autoComment', + arguments: [document, element] + }); + + case 'autoTest': + return new vscode.CodeLens(range, { + title: '🧪 AutoTest', + command: 'autodev.codelens.autoTest', + arguments: [document, element] + }); + + case 'autoMethod': + return new vscode.CodeLens(range, { + title: '✨ AutoMethod', + command: 'autodev.codelens.autoMethod', + arguments: [document, element] + }); + + default: + return null; + } + } + + /** + * 构建折叠模式的 CodeLens(显示下拉菜单) + */ + private buildCollapsedCodeLens(group: vscode.CodeLens[]): vscode.CodeLens { + const [first] = group; + const commands = group.map(lens => lens.command!); + + return new vscode.CodeLens(first.range, { + title: '$(autodev-icon) $(chevron-down)', + command: 'autodev.codelens.showMenu', + arguments: [commands], + tooltip: 'AutoDev Actions' + }); + } + + /** + * 判断是否为测试文件 + */ + private isTestFile(fileName: string): boolean { + const testPatterns = [ + /\.test\./, + /\.spec\./, + /_test\./, + /_spec\./, + /test_.*\.py$/, + /.*Test\.java$/, + /.*Test\.kt$/, + /.*Tests\.cs$/, + /_test\.go$/ + ]; + + return testPatterns.some(pattern => pattern.test(fileName)); + } + + dispose() { + this._onDidChangeCodeLenses.dispose(); + } +} + diff --git a/mpp-vscode/webview/src/App.tsx b/mpp-vscode/webview/src/App.tsx index 2783418eeb..f2a5496a28 100644 --- a/mpp-vscode/webview/src/App.tsx +++ b/mpp-vscode/webview/src/App.tsx @@ -63,9 +63,6 @@ const App: React.FC = () => { // Token usage state const [totalTokens, setTotalTokens] = useState(null); - // Active file state (for auto-add current file feature) - const [activeFile, setActiveFile] = useState(null); - // Completion state - from mpp-core const [completionItems, setCompletionItems] = useState([]); const [completionResult, setCompletionResult] = useState(null); @@ -244,18 +241,6 @@ const App: React.FC = () => { } break; - // Active file changed (for auto-add current file) - case 'activeFileChanged': - if (msg.data) { - setActiveFile({ - path: msg.data.path as string, - name: msg.data.name as string, - relativePath: msg.data.path as string, - isDirectory: msg.data.isDirectory as boolean || false - }); - } - break; - // Completion results from mpp-core case 'completionsResult': if (msg.data?.items) { @@ -467,7 +452,6 @@ const App: React.FC = () => { availableConfigs={configState.availableConfigs} currentConfigName={configState.currentConfigName} totalTokens={totalTokens} - activeFile={activeFile} currentPlan={currentPlan} /> diff --git a/mpp-vscode/webview/src/components/ChatInput.tsx b/mpp-vscode/webview/src/components/ChatInput.tsx index 0661106be3..4932df666c 100644 --- a/mpp-vscode/webview/src/components/ChatInput.tsx +++ b/mpp-vscode/webview/src/components/ChatInput.tsx @@ -25,7 +25,6 @@ interface ChatInputProps { availableConfigs?: ModelConfig[]; currentConfigName?: string | null; totalTokens?: number | null; - activeFile?: SelectedFile | null; currentPlan?: PlanData | null; } @@ -47,7 +46,6 @@ export const ChatInput: React.FC = ({ availableConfigs = [], currentConfigName = null, totalTokens = null, - activeFile = null, currentPlan = null }) => { const [input, setInput] = useState(''); @@ -55,23 +53,12 @@ export const ChatInput: React.FC = ({ const [isEnhancing, setIsEnhancing] = useState(false); const [completionOpen, setCompletionOpen] = useState(false); const [selectedCompletionIndex, setSelectedCompletionIndex] = useState(0); - const [autoAddCurrentFile, setAutoAddCurrentFile] = useState(true); const inputRef = useRef(null); const cursorPositionRef = useRef(0); // Use external completion items if provided const completionItems = externalCompletionItems || []; - // Auto-add active file when it changes - useEffect(() => { - if (autoAddCurrentFile && activeFile) { - setSelectedFiles(prev => { - if (prev.some(f => f.path === activeFile.path)) return prev; - return [...prev, activeFile]; - }); - } - }, [activeFile, autoAddCurrentFile]); - // Handle completion result from mpp-core useEffect(() => { if (completionResult) { @@ -178,8 +165,6 @@ export const ChatInput: React.FC = ({ onAddFile={handleAddFile} onRemoveFile={handleRemoveFile} onClearFiles={handleClearFiles} - autoAddCurrentFile={autoAddCurrentFile} - onToggleAutoAdd={() => setAutoAddCurrentFile(prev => !prev)} /> {/* Input Area with DevIn highlighting */} diff --git a/mpp-vscode/webview/src/components/TopToolbar.tsx b/mpp-vscode/webview/src/components/TopToolbar.tsx index 9609af7952..fdec09102e 100644 --- a/mpp-vscode/webview/src/components/TopToolbar.tsx +++ b/mpp-vscode/webview/src/components/TopToolbar.tsx @@ -15,31 +15,15 @@ interface TopToolbarProps { onAddFile: (file: SelectedFile) => void; onRemoveFile: (file: SelectedFile) => void; onClearFiles: () => void; - autoAddCurrentFile?: boolean; - onToggleAutoAdd?: () => void; } export const TopToolbar: React.FC = ({ - selectedFiles, onAddFile, onRemoveFile, onClearFiles, autoAddCurrentFile = true, onToggleAutoAdd + selectedFiles, onAddFile, onRemoveFile, onClearFiles }) => { const [isSearchOpen, setIsSearchOpen] = useState(false); const [isExpanded, setIsExpanded] = useState(false); const addButtonRef = useRef(null); - // Context indicator component - const ContextIndicator = () => ( - - ); - if (selectedFiles.length === 0 && !isSearchOpen) { return (
@@ -49,7 +33,6 @@ export const TopToolbar: React.FC = ({ Add context - setIsSearchOpen(false)} onSelectFile={onAddFile} selectedFiles={selectedFiles} />
@@ -80,7 +63,6 @@ export const TopToolbar: React.FC = ({ )}
- {selectedFiles.length > 1 && ( -
)} From b047e4ff239ead6ed7aeb3eaa600ebf5894f0c16 Mon Sep 17 00:00:00 2001 From: Phodal Huang Date: Thu, 4 Dec 2025 20:17:26 +0800 Subject: [PATCH 3/4] fix(mpp-vscode): address PR #43 review comments - Add missing autodev.codelens.showMenu command declaration - Remove unsupported languages (csharp, cpp, c) from CodeLens registration - Fix Python test file naming to use consistent test_*.py format - Implement robust WASM file bundling and path resolution - Add copy-wasm.js script to copy WASM files during build - Update build script to include web-tree-sitter external and copy:wasm - Implement multi-strategy path resolution (extension path + node_modules fallback) - Add .vscodeignore to ensure WASM files are packaged --- mpp-vscode/.vscodeignore | 32 ++++++++++++ mpp-vscode/package.json | 9 +++- mpp-vscode/scripts/copy-wasm.js | 49 +++++++++++++++++++ mpp-vscode/src/extension.ts | 4 +- mpp-vscode/src/prompts/prompt-templates.ts | 5 +- .../src/providers/code-element-parser.ts | 35 ++++++++++--- mpp-vscode/src/providers/codelens-provider.ts | 5 +- 7 files changed, 124 insertions(+), 15 deletions(-) create mode 100644 mpp-vscode/.vscodeignore create mode 100644 mpp-vscode/scripts/copy-wasm.js diff --git a/mpp-vscode/.vscodeignore b/mpp-vscode/.vscodeignore new file mode 100644 index 0000000000..9682a7483e --- /dev/null +++ b/mpp-vscode/.vscodeignore @@ -0,0 +1,32 @@ +# VSCode extension ignore file +# Exclude source files and build artifacts not needed in the packaged extension + +# Source files +src/ +webview/src/ +webview/node_modules/ + +# Build artifacts +*.map +tsconfig.json +.eslintrc.json + +# Development files +.vscode/ +node_modules/ +.gitignore +.git/ + +# Test files +**/__tests__/** +**/*.test.* +**/*.spec.* + +# Documentation +README.md +CHANGELOG.md + +# Keep dist and wasm folders +!dist/ +!wasm/ + diff --git a/mpp-vscode/package.json b/mpp-vscode/package.json index 8059a166b9..134f8373ae 100644 --- a/mpp-vscode/package.json +++ b/mpp-vscode/package.json @@ -68,6 +68,10 @@ { "command": "autodev.codelens.autoMethod", "title": "AutoDev: Auto Method" + }, + { + "command": "autodev.codelens.showMenu", + "title": "AutoDev: Show CodeLens Menu" } ], "viewsContainers": { @@ -199,9 +203,10 @@ }, "scripts": { "vscode:prepublish": "npm run build", - "build": "npm run build:extension && npm run build:webview", - "build:extension": "esbuild ./src/extension.ts --bundle --outfile=dist/extension.js --external:vscode --external:ws --format=cjs --platform=node", + "build": "npm run build:extension && npm run build:webview && npm run copy:wasm", + "build:extension": "esbuild ./src/extension.ts --bundle --outfile=dist/extension.js --external:vscode --external:ws --external:web-tree-sitter --format=cjs --platform=node", "build:webview": "cd webview && npm install && npm run build", + "copy:wasm": "node scripts/copy-wasm.js", "watch": "npm run build:extension -- --watch", "package": "vsce package", "lint": "eslint src --ext ts", diff --git a/mpp-vscode/scripts/copy-wasm.js b/mpp-vscode/scripts/copy-wasm.js new file mode 100644 index 0000000000..e28f2b9fb9 --- /dev/null +++ b/mpp-vscode/scripts/copy-wasm.js @@ -0,0 +1,49 @@ +#!/usr/bin/env node +/** + * Copy WASM files from @unit-mesh/treesitter-artifacts to dist/wasm + */ + +const fs = require('fs'); +const path = require('path'); + +const sourceDir = path.join(__dirname, '../node_modules/@unit-mesh/treesitter-artifacts/wasm'); +const targetDir = path.join(__dirname, '../dist/wasm'); + +// Create target directory if it doesn't exist +if (!fs.existsSync(targetDir)) { + fs.mkdirSync(targetDir, { recursive: true }); +} + +// List of required WASM files +const wasmFiles = [ + 'tree-sitter-typescript.wasm', + 'tree-sitter-tsx.wasm', + 'tree-sitter-javascript.wasm', + 'tree-sitter-python.wasm', + 'tree-sitter-java.wasm', + 'tree-sitter-kotlin.wasm', + 'tree-sitter-go.wasm', + 'tree-sitter-rust.wasm' +]; + +// Copy each WASM file +let copiedCount = 0; +for (const file of wasmFiles) { + const sourcePath = path.join(sourceDir, file); + const targetPath = path.join(targetDir, file); + + try { + if (fs.existsSync(sourcePath)) { + fs.copyFileSync(sourcePath, targetPath); + console.log(`✓ Copied ${file}`); + copiedCount++; + } else { + console.warn(`⚠ Warning: ${file} not found in source directory`); + } + } catch (error) { + console.error(`✗ Failed to copy ${file}: ${error.message}`); + } +} + +console.log(`\nCopied ${copiedCount}/${wasmFiles.length} WASM files to dist/wasm/`); + diff --git a/mpp-vscode/src/extension.ts b/mpp-vscode/src/extension.ts index 7905971d5e..6a88655ce0 100644 --- a/mpp-vscode/src/extension.ts +++ b/mpp-vscode/src/extension.ts @@ -127,13 +127,13 @@ export async function activate(context: vscode.ExtensionContext) { log('DevIns language support registered'); // Register CodeLens Provider - const codeLensProvider = new AutoDevCodeLensProvider(log); + const codeLensProvider = new AutoDevCodeLensProvider(log, context.extensionPath); const codeLensCommands = new CodeLensCommands(log, () => chatViewProvider, () => chatViewProvider.getCurrentModelConfig()); // Register for supported languages const supportedLanguages = [ 'typescript', 'javascript', 'typescriptreact', 'javascriptreact', - 'python', 'java', 'kotlin', 'go', 'rust', 'csharp', 'cpp', 'c' + 'python', 'java', 'kotlin', 'go', 'rust' ]; context.subscriptions.push( diff --git a/mpp-vscode/src/prompts/prompt-templates.ts b/mpp-vscode/src/prompts/prompt-templates.ts index 4b0c115030..08f3f1c63d 100644 --- a/mpp-vscode/src/prompts/prompt-templates.ts +++ b/mpp-vscode/src/prompts/prompt-templates.ts @@ -149,7 +149,7 @@ export function getTestFilePath(sourcePath: string, language: string): string { javascriptreact: '.test.jsx', java: 'Test.java', kotlin: 'Test.kt', - python: '_test.py', + python: '.py', go: '_test.go', rust: '', csharp: 'Tests.cs', @@ -161,6 +161,7 @@ export function getTestFilePath(sourcePath: string, language: string): string { const testDir = dirPath.replace('/src/main/', '/src/test/'); return `${testDir}/${baseName}${testSuffix}`; } - if (language === 'python') return `${dirPath}/test_${baseName}.py`; + // Python uses test_ prefix for pytest convention + if (language === 'python') return `${dirPath}/test_${baseName}${testSuffix}`; return `${dirPath}/${baseName}${testSuffix}`; } diff --git a/mpp-vscode/src/providers/code-element-parser.ts b/mpp-vscode/src/providers/code-element-parser.ts index ff4d16f026..35afb5e0ee 100644 --- a/mpp-vscode/src/providers/code-element-parser.ts +++ b/mpp-vscode/src/providers/code-element-parser.ts @@ -213,8 +213,16 @@ export class CodeElementParser { private static ParserClass: any = null; private static languages: Map = new Map(); private static initPromise: Promise | null = null; - - constructor(private log: (message: string) => void) {} + private static extensionPath: string | undefined; + + constructor( + private log: (message: string) => void, + extensionPath?: string + ) { + if (extensionPath && !CodeElementParser.extensionPath) { + CodeElementParser.extensionPath = extensionPath; + } + } /** * Initialize tree-sitter parser @@ -291,12 +299,25 @@ export class CodeElementParser { return null; } - // In VSCode extension context, we need to resolve the path - // This assumes WASM files are bundled in the extension + // Try multiple path resolution strategies + // 1. Try extension's dist/wasm folder (for packaged extension) + if (CodeElementParser.extensionPath) { + const path = require('path'); + const wasmPath = path.join(CodeElementParser.extensionPath, 'dist', 'wasm', `${grammarName}.wasm`); + const fs = require('fs'); + if (fs.existsSync(wasmPath)) { + this.log(`Using WASM from extension: ${wasmPath}`); + return wasmPath; + } + } + + // 2. Try node_modules (for development) try { - return require.resolve(`@unit-mesh/treesitter-artifacts/wasm/${grammarName}.wasm`); - } catch { - this.log(`WASM file not found for ${langId}`); + const wasmPath = require.resolve(`@unit-mesh/treesitter-artifacts/wasm/${grammarName}.wasm`); + this.log(`Using WASM from node_modules: ${wasmPath}`); + return wasmPath; + } catch (error) { + this.log(`WASM file not found for ${langId}: ${error}`); return null; } } diff --git a/mpp-vscode/src/providers/codelens-provider.ts b/mpp-vscode/src/providers/codelens-provider.ts index fe9b3e67a7..7f21cb0cae 100644 --- a/mpp-vscode/src/providers/codelens-provider.ts +++ b/mpp-vscode/src/providers/codelens-provider.ts @@ -27,9 +27,10 @@ export class AutoDevCodeLensProvider implements vscode.CodeLensProvider { public readonly onDidChangeCodeLenses: vscode.Event = this._onDidChangeCodeLenses.event; constructor( - private log: (message: string) => void + private log: (message: string) => void, + extensionPath?: string ) { - this.parser = new CodeElementParser(log); + this.parser = new CodeElementParser(log, extensionPath); } /** From 254e488c3f66bf7c17cd8a380f92498982774138 Mon Sep 17 00:00:00 2001 From: Phodal Huang Date: Thu, 4 Dec 2025 20:29:12 +0800 Subject: [PATCH 4/4] chore(release): bump mppVersion to 0.3.4 Update mppVersion in gradle.properties and add new media assets and backup files for the upcoming release. --- gradle.properties | 2 +- mpp-ui/package.json | 2 +- mpp-ui/package.json.backup | 82 ++++++++++ mpp-vscode/README.md | 260 ++++++++++++++++-------------- mpp-vscode/media/autodev-dark.svg | 13 ++ mpp-vscode/media/autodev.woff | Bin 0 -> 4272 bytes mpp-vscode/media/icon.svg | 14 ++ mpp-vscode/media/pluginIcon.png | Bin 0 -> 6362 bytes mpp-vscode/package.json | 44 +++-- 9 files changed, 289 insertions(+), 128 deletions(-) create mode 100644 mpp-ui/package.json.backup create mode 100644 mpp-vscode/media/autodev-dark.svg create mode 100644 mpp-vscode/media/autodev.woff create mode 100644 mpp-vscode/media/icon.svg create mode 100644 mpp-vscode/media/pluginIcon.png diff --git a/gradle.properties b/gradle.properties index 3426d92392..04f63453d4 100644 --- a/gradle.properties +++ b/gradle.properties @@ -10,7 +10,7 @@ pluginRepositoryUrl = https://github.com/unit-mesh/auto-dev pluginVersion = 2.4.6 # MPP Unified Version (mpp-core, mpp-ui, mpp-server) -mppVersion = 0.3.3 +mppVersion = 0.3.4 # Supported IDEs: idea, pycharm baseIDE=idea diff --git a/mpp-ui/package.json b/mpp-ui/package.json index acd01a6ee0..1d84ac95de 100644 --- a/mpp-ui/package.json +++ b/mpp-ui/package.json @@ -1,6 +1,6 @@ { "name": "@autodev/cli", - "version": "0.3.3", + "version": "0.3.4", "description": "AutoDev CLI - Terminal UI for AI-powered development assistant", "type": "module", "bin": { diff --git a/mpp-ui/package.json.backup b/mpp-ui/package.json.backup new file mode 100644 index 0000000000..1d84ac95de --- /dev/null +++ b/mpp-ui/package.json.backup @@ -0,0 +1,82 @@ +{ + "name": "@autodev/cli", + "version": "0.3.4", + "description": "AutoDev CLI - Terminal UI for AI-powered development assistant", + "type": "module", + "bin": { + "autodev": "./dist/jsMain/typescript/index.js" + }, + "scripts": { + "build:kotlin": "cd .. && ./gradlew :mpp-core:assembleJsPackage", + "build:kotlin-deps": "cd ../mpp-core/build/packages/js && npm install --ignore-scripts", + "build:ts": "cd .. && ./gradlew :mpp-ui:compileKotlinJs && cd mpp-ui && tsc && chmod +x dist/jsMain/typescript/index.js", + "build": "npm run build:kotlin && npm run build:kotlin-deps && npm run build:ts", + "dev": "tsc --watch", + "start": "node dist/jsMain/typescript/index.js", + "code": "node dist/jsMain/typescript/index.js code", + "test": "vitest run", + "test:unit": "vitest run src/jsMain/typescript/**/*.test.ts", + "test:git": "npm run build:kotlin && node ../docs/test-scripts/test-git-nodejs.js", + "test:cli": "node test-cli-completion.js", + "test:all": "npm test && npm run test:completion && npm run test:cli", + "test:ci": "npm run build && npm run test:integration", + "test:framework": "node src/test/framework/validate-structure.cjs", + "test:integration-v2": "npm run build:ts && npm test src/test/integration-v2 -- --reporter=verbose", + "test:json-scenarios": "npm run build:ts && npm test src/test/integration-v2/json-scenarios.test.ts -- --reporter=verbose", + "analyze-test-results": "node scripts/analyze-test-results.cjs", + "generate:scenario:interactive": "node scripts/generate-test-scenario.js --interactive", + "validate:scenarios": "node scripts/validate-scenarios.js", + "clean": "rm -rf dist build", + "icon:convert": "bash scripts/convert-icon.sh && python3 scripts/convert-icon-windows.py", + "prepublish:local": "npm run build", + "publish:local": "node scripts/publish-local.js", + "prepublish:remote": "npm run build", + "publish:remote": "node scripts/publish-remote.js", + "postinstall": "node scripts/check-mpp-core.js || echo 'Warning: mpp-core not found, run npm run build:kotlin first'" + }, + "packageManager": "pnpm@10.20.0", + "keywords": [ + "ai", + "cli", + "terminal", + "coding-assistant", + "autodev", + "tui" + ], + "author": "AutoDev Team", + "license": "MIT", + "engines": { + "node": ">=20.0.0" + }, + "dependencies": { + "@autodev/mpp-core": "file:../mpp-core/build/packages/js", + "@js-joda/core": "^5.6.5", + "@modelcontextprotocol/sdk": "^1.0.4", + "@unit-mesh/treesitter-artifacts": "^1.7.7", + "chalk": "^5.3.0", + "commander": "^12.1.0", + "diff": "^7.0.0", + "dotenv": "^16.4.5", + "highlight.js": "^11.11.1", + "ink": "^5.0.1", + "ink-select-input": "^6.0.0", + "ink-spinner": "^5.0.0", + "ink-text-input": "^6.0.0", + "node-fetch": "^3.3.2", + "react": "^18.3.1", + "web-tree-sitter": "^0.22.2", + "yaml": "^2.6.1" + }, + "devDependencies": { + "@types/diff": "^6.0.0", + "@types/node": "^20.11.24", + "@types/react": "^18.3.12", + "ink-testing-library": "^4.0.0", + "typescript": "^5.3.3", + "vitest": "^2.1.8" + }, + "files": [ + "dist/", + "README.md" + ] +} diff --git a/mpp-vscode/README.md b/mpp-vscode/README.md index e7febba66f..95df1b59d2 100644 --- a/mpp-vscode/README.md +++ b/mpp-vscode/README.md @@ -1,139 +1,165 @@ -# mpp-vscode +

+ logo +

+

AutoDev for VSCode (KMP Edition)

+

+ + Visual Studio Marketplace Version + + + CI + +

+ +> 🧙‍ AI-powered coding wizard with multilingual support 🌐, auto code generation 🏗️, and a helpful bug-slaying assistant 🐞! Built with **Kotlin Multiplatform** for cross-platform capabilities. 🚀 + +This is the **Kotlin Multiplatform (KMP) edition** of AutoDev, rewritten from the ground up to leverage Kotlin's cross-platform capabilities for future iOS, Android, and Desktop support. + +## 🌟 Key Features + +- **💬 Chat Mode**: Interactive AI assistant with context-aware code understanding +- **🔍 CodeLens**: Inline AI actions above functions and classes + - Quick Chat, Explain Code, Optimize Code + - Auto Comment, Auto Test, Auto Method +- **🧪 Auto Test Generation**: Generate unit tests with Tree-sitter AST parsing +- **📝 Auto Documentation**: Generate JSDoc/DocString comments +- **🔧 Code Actions**: Explain, optimize, and fix code with AI +- **🤖 Agent Support**: Extensible agent system via MCP (Model Context Protocol) +- **🌐 Multi-LLM Support**: OpenAI, Anthropic, Google, DeepSeek, Ollama, OpenRouter + +## 🚀 Quick Start + +1. **Install the Extension**: Search for "AutoDev" in VSCode Marketplace +2. **Configure LLM Provider**: Open Settings → AutoDev → Set your API key and model +3. **Start Coding**: Press `Cmd+Shift+A` (Mac) / `Ctrl+Shift+A` (Windows/Linux) to open chat + +## 📖 Configuration + +### LLM Provider Setup + +```json +{ + "autodev.provider": "openai", + "autodev.model": "gpt-4o-mini", + "autodev.apiKey": "your-api-key-here" +} +``` + +### CodeLens Settings + +```json +{ + "autodev.codelens.enable": true, + "autodev.codelens.displayMode": "expand", + "autodev.codelens.items": [ + "quickChat", + "autoTest", + "autoComment" + ] +} +``` + +## 🏗️ Architecture (Kotlin Multiplatform) -基于 Kotlin Multiplatform (KMP) 的 VSCode 扩展,复用 mpp-core 的核心能力。 +This version is built with: -## 架构概述 +- **mpp-core**: Kotlin Multiplatform core library (shared logic) +- **mpp-vscode**: VSCode extension (TypeScript + mpp-core via JS bindings) +- **Tree-sitter**: Accurate code parsing for 8 languages (TS, JS, Python, Java, Kotlin, Go, Rust, etc.) +- **MCP Protocol**: Model Context Protocol for IDE server integration + +### Project Structure ``` mpp-vscode/ -├── package.json # VSCode 扩展配置 -├── src/ -│ ├── extension.ts # 入口点 -│ ├── services/ -│ │ ├── ide-server.ts # MCP 协议服务器 -│ │ ├── diff-manager.ts # Diff 管理 -│ │ └── chat-service.ts # Chat 服务 -│ ├── providers/ -│ │ ├── chat-view.ts # Webview Provider -│ │ └── diff-content.ts # Diff Content Provider -│ ├── commands/ -│ │ └── index.ts # 命令注册 -│ └── bridge/ -│ └── mpp-core.ts # mpp-core 桥接层 -├── webview/ # Webview UI -│ ├── src/ -│ │ ├── App.tsx -│ │ └── components/ -│ └── package.json -└── tsconfig.json +├── src/ # TypeScript extension code +│ ├── extension.ts # Main entry point +│ ├── providers/ # CodeLens, Chat providers +│ ├── services/ # IDE Server, Diff Manager +│ └── commands/ # CodeLens commands +├── webview/ # React-based chat UI +├── dist/ # Build output +│ └── wasm/ # Tree-sitter WASM files +└── scripts/ # Build scripts ``` -## TODO List - -### Phase 1: 项目基础设施 ✅ -- [x] 创建项目目录结构 -- [x] 创建 package.json (VSCode 扩展配置) -- [x] 创建 tsconfig.json -- [x] 配置 esbuild 打包 -- [x] 配置 vitest 测试框架 - -### Phase 2: 核心服务 ✅ -- [x] 实现 mpp-core 桥接层 (`src/bridge/mpp-core.ts`) - - [x] 导入 @autodev/mpp-core - - [x] 封装 LLMService (JsKoogLLMService) - - [x] 封装 CodingAgent (JsCodingAgent) - - [x] 封装 ToolRegistry (JsToolRegistry) - - [x] 封装 CompletionManager (JsCompletionManager) - - [x] 封装 DevInsCompiler (JsDevInsCompiler) -- [x] 实现 extension.ts 入口 - - [x] 扩展激活/停用 - - [x] 服务初始化 -- [x] 添加单元测试 (`test/bridge/mpp-core.test.ts`) - -### Phase 3: IDE 集成 ✅ -- [x] 实现 IDE Server (MCP 协议) - - [x] Express HTTP 服务器 - - [x] 端点: /health, /context, /diff/open, /diff/close, /file/read, /file/write - - [x] 认证和 CORS 保护 - - [x] 端口文件写入 (~/.autodev/ide-server.json) -- [x] 实现 Diff Manager - - [x] showDiff() - 显示差异 - - [x] acceptDiff() - 接受更改 - - [x] cancelDiff() - 取消更改 - - [x] closeDiffByPath() - 按路径关闭 - - [x] DiffContentProvider -- [x] 添加单元测试 (`test/services/`) - -### Phase 4: Chat 界面 ✅ -- [x] 实现 Chat Webview Provider - - [x] Webview 创建和管理 - - [x] 消息桥接 (VSCode ↔ Webview) - - [x] LLM 服务集成 -- [x] 创建 Webview UI (内嵌 HTML) - - [x] 聊天消息组件 - - [x] 输入框组件 - - [x] 流式响应显示 - -### Phase 5: 命令和功能 ✅ -- [x] 注册 VSCode 命令 - - [x] autodev.chat - 打开聊天 - - [x] autodev.acceptDiff - 接受差异 - - [x] autodev.cancelDiff - 取消差异 - - [x] autodev.runAgent - 运行 Agent -- [x] 快捷键绑定 (Cmd+Shift+A) -- [x] 状态栏集成 - -### Phase 6: 高级功能 ✅ -- [x] DevIns 语言支持 - - [x] 语法高亮 (TextMate grammar) - - [x] 自动补全 (/, @, $ 触发) -- [x] React Webview UI - - [x] React + Vite 构建 - - [x] Markdown 渲染 (react-markdown + remark-gfm) - - [x] VSCode 主题集成 - - [x] 流式响应动画 -- [ ] 代码索引集成 -- [ ] 领域词典支持 - -## 参考项目 - -1. **autodev-vscode** - 早期 AutoDev VSCode 版本,全功能实现 -2. **gemini-cli/vscode-ide-companion** - Gemini 的轻量级 MCP 桥接器 -3. **mpp-ui** - 现有的 CLI 工具,展示如何使用 mpp-core - -## 开发指南 - -### 构建 mpp-core +## 🔌 Supported Languages -```bash -cd /Volumes/source/ai/autocrud -./gradlew :mpp-core:assembleJsPackage -``` +CodeLens and code parsing support: -### 安装依赖 +- TypeScript/JavaScript (including React/TSX) +- Python +- Java +- Kotlin +- Go +- Rust + +## 🛠️ Development + +### Prerequisites + +- Node.js 18+ +- VSCode 1.77+ + +### Build from Source ```bash +# Install dependencies cd mpp-vscode npm install -``` -### 开发模式 +# Build +npm run build -```bash +# Watch mode npm run watch -``` -### 打包扩展 - -```bash +# Package extension npm run package ``` -## 技术栈 +## 📚 Documentation + +- **Official Docs**: [https://vscode.unitmesh.cc/](https://vscode.unitmesh.cc/) +- **JetBrains IDE Version**: [https://github.com/unit-mesh/auto-dev](https://github.com/unit-mesh/auto-dev) +- **Contributing**: [https://vscode.unitmesh.cc/development](https://vscode.unitmesh.cc/development) + +## 🤝 Join the Community + +wechat qrcode + +If you are interested in AutoDev, you can join our WeChat group by scanning the QR code above. + +(如果群二维码过期,可以添加我的微信号:`phodal02`,注明 `AutoDev`,我拉你入群) + +## 📋 Feature Comparison + +| Feature | KMP Edition | Original VSCode | +|------------------------|-------------|-----------------| +| Chat mode | ✅ | ✅ | +| CodeLens | ✅ | ✅ | +| AutoDoc | ✅ | ✅ | +| AutoTest | ✅ | ✅ | +| Tree-sitter Parsing | ✅ | ✅ | +| MCP Protocol | ✅ | ❌ | +| Cross-platform Core | ✅ (KMP) | ❌ | +| iOS Support (Future) | 🚧 | ❌ | +| Android Support (Future)| 🚧 | ❌ | + +## 🎯 Roadmap + +- [x] Basic Chat functionality +- [x] CodeLens with Tree-sitter +- [x] Auto Test/Doc/Method +- [x] Multi-LLM support +- [ ] Enhanced agent system +- [ ] iOS/Android support (via KMP) +- [ ] Desktop standalone app + +## 📄 License + +Apache-2.0 -- **TypeScript** - 主要开发语言 -- **mpp-core (Kotlin/JS)** - 核心 LLM 和 Agent 能力 -- **React** - Webview UI -- **Express** - MCP 服务器 -- **esbuild** - 打包工具 +## 🙏 Acknowledgments +Built on the foundation of [AutoDev VSCode](https://github.com/unit-mesh/auto-dev-vscode), reimagined with Kotlin Multiplatform for next-generation cross-platform AI coding assistance. diff --git a/mpp-vscode/media/autodev-dark.svg b/mpp-vscode/media/autodev-dark.svg new file mode 100644 index 0000000000..468b267be6 --- /dev/null +++ b/mpp-vscode/media/autodev-dark.svg @@ -0,0 +1,13 @@ + + + + autodev-dark + Created with Sketch. + + + + + + + + \ No newline at end of file diff --git a/mpp-vscode/media/autodev.woff b/mpp-vscode/media/autodev.woff new file mode 100644 index 0000000000000000000000000000000000000000..58b206cdb6e9e2e52f4aa23eacf49c1a5b4249df GIT binary patch literal 4272 zcmeHKPiS0482@JGy?Jlm{(bu=nYX46D0CfRH@sWjUpRH9W%p-L+vwA*x>1d?n? zQcXlmX~94#SiMLu;=zL__2R9EAO(wfkcuFlqy;sIhaN;wZ2ZleIU1Djj*Dr;muaXgjHMA|=)MO!X{ulaOE3Id|;l zI}_D;t-A(U+v=R0m|i*q-F2eqw9dzWSbTP}Rz<9MFWLum?)u=RUnil2PW$W7+4)?2 zJvpg6g8X(+vtOvh6y(i`B{xeNj}WbSVs(9$Mg%f6W=&D+ zlsBb`0@Q*wjayJ^s}{0oA8HRP#qq}|zviv~wZ0A=`mT||Z2fl53VWCa6pR?G650zQ z6C!h>!KX~zeFK97{awXik)5I=^;To!uuFr*!4d?s*Ik7{ZECho+ZM4U z!9mgHMQ%$+2wvk#N+oKXapBtB7TjXl^BKlD7d9_W_Or(=DL5>Jv_1k9XaHm{Tb8Bl zJJNCF9RSN>j9a3wj|;eBuPm< zmE_!ZE-WissV zr4b^hl7Y3OlKH5ah0DUwn1%_xyc~qSpzGXFg)uj3#p0VMk_0nyhv71G2jsT}?!2K& zPOrs-Yl5{gTozY3c*!S*m+Yly0$y@}ZRRDJhw+kJZX064ezL(77(W4KVk=aFgDc`o zA$WQz4Q`={Nl&%d`=ezCY{a(Sw9u&nqD2>`q|a#LgJwRC>c+cWal;%_kB=zfr`}BHbGG^cyCu*H;be z(r-V!NK8T;^FG33M<TiJ&DAbT=(OYBs$*%1|roV3+y3{11z)N&Bq*9rmVG z4wz^Ox?x^kc;p39?knT;aX2PM4qAfoap0377QlfgJurvPg?bxnoei;s{`nAl)I^u` zo6S + + + ai-copilot + Created with Sketch. + + + + + + + + + \ No newline at end of file diff --git a/mpp-vscode/media/pluginIcon.png b/mpp-vscode/media/pluginIcon.png new file mode 100644 index 0000000000000000000000000000000000000000..acc84e5eded8d2973c526406864bf46fa7be4032 GIT binary patch literal 6362 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 literal 0 HcmV?d00001 diff --git a/mpp-vscode/package.json b/mpp-vscode/package.json index 134f8373ae..736117ca9e 100644 --- a/mpp-vscode/package.json +++ b/mpp-vscode/package.json @@ -1,31 +1,57 @@ { - "name": "autodev-vscode", - "displayName": "AutoDev", - "description": "AI-powered coding assistant based on Kotlin Multiplatform", - "version": "0.1.0", - "publisher": "phodal", + "name": "autodev", + "displayName": "AutoDev - 🧙the AI-powered coding wizard (KMP Edition).", + "description": "🧙‍ AI-powered coding wizard with multilingual support 🌐, auto code generation 🏗️, based on Kotlin Multiplatform. AutoDev provides CodeLens, Chat, and powerful agent features! 🚀", + "version": "0.5.3", + "publisher": "Phodal", "license": "Apache-2.0", "repository": { "type": "git", - "url": "https://github.com/phodal/auto-dev-sketch" + "url": "https://github.com/unit-mesh/auto-dev-vscode" }, + "bugs": { + "url": "https://github.com/unit-mesh/auto-dev-vscode/issues" + }, + "homepage": "https://vscode.unitmesh.cc", + "qna": "https://github.com/unit-mesh/auto-dev-vscode/issues/new/choose", + "pricing": "Free", + "icon": "media/pluginIcon.png", "engines": { "vscode": "^1.85.0" }, + "vsce": { + "dependencies": true, + "yarn": false + }, "categories": [ "Programming Languages", + "Education", "Machine Learning", - "Other" + "Snippets" ], "keywords": [ "ai", "coding assistant", "llm", - "autodev" + "autodev", + "kotlin multiplatform", + "kmp" ], "activationEvents": [ + "onLanguage:java", + "onLanguage:javascript", + "onLanguage:kotlin", + "onLanguage:typescript", + "onLanguage:typescriptreact", + "onLanguage:python", + "onLanguage:rust", + "onLanguage:go", "onStartupFinished" ], + "extensionKind": [ + "ui", + "workspace" + ], "main": "./dist/extension.js", "contributes": { "commands": [ @@ -227,7 +253,7 @@ "vitest": "^1.0.0" }, "dependencies": { - "@autodev/mpp-core": "file:../mpp-core/build/packages/js", + "@autodev/mpp-core": "0.3.4", "@modelcontextprotocol/sdk": "^1.0.0", "@unit-mesh/treesitter-artifacts": "^1.7.7", "cors": "^2.8.5",