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/.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/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 0000000000..58b206cdb6 Binary files /dev/null and b/mpp-vscode/media/autodev.woff differ diff --git a/mpp-vscode/media/icon.svg b/mpp-vscode/media/icon.svg new file mode 100644 index 0000000000..c8927a0a72 --- /dev/null +++ b/mpp-vscode/media/icon.svg @@ -0,0 +1,14 @@ + + + + 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 0000000000..acc84e5ede Binary files /dev/null and b/mpp-vscode/media/pluginIcon.png differ diff --git a/mpp-vscode/package.json b/mpp-vscode/package.json index 86f202abfb..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": [ @@ -44,6 +70,34 @@ { "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" + }, + { + "command": "autodev.codelens.showMenu", + "title": "AutoDev: Show CodeLens Menu" } ], "viewsContainers": { @@ -99,6 +153,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" } } }, @@ -137,9 +229,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", @@ -160,10 +253,12 @@ "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", "express": "^4.18.2", + "web-tree-sitter": "^0.25.10", "yaml": "^2.8.2", "zod": "^3.22.4" } 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/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..1520ae44a6 --- /dev/null +++ b/mpp-vscode/src/commands/codelens-commands.ts @@ -0,0 +1,254 @@ +/** + * 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..6a88655ce0 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, 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' + ]; + + 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..08f3f1c63d --- /dev/null +++ b/mpp-vscode/src/prompts/prompt-templates.ts @@ -0,0 +1,167 @@ +/** + * 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: '.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}`; + } + // 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/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..35afb5e0ee --- /dev/null +++ b/mpp-vscode/src/providers/code-element-parser.ts @@ -0,0 +1,667 @@ +/** + * 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; + private static extensionPath: string | undefined; + + constructor( + private log: (message: string) => void, + extensionPath?: string + ) { + if (extensionPath && !CodeElementParser.extensionPath) { + CodeElementParser.extensionPath = extensionPath; + } + } + + /** + * 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; + } + + // 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 { + 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; + } + } + + /** + * 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..7f21cb0cae --- /dev/null +++ b/mpp-vscode/src/providers/codelens-provider.ts @@ -0,0 +1,229 @@ +/** + * 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, + extensionPath?: string + ) { + this.parser = new CodeElementParser(log, extensionPath); + } + + /** + * 刷新 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.css b/mpp-vscode/webview/src/App.css index 2041c33924..d4e290b0db 100644 --- a/mpp-vscode/webview/src/App.css +++ b/mpp-vscode/webview/src/App.css @@ -115,15 +115,14 @@ display: flex; align-items: center; justify-content: center; - gap: 10px; - padding: 20px; + padding: 10px; color: var(--vscode-descriptionForeground); font-size: 13px; } .loading-spinner { - width: 16px; - height: 16px; + width: 12px; + height: 12px; border: 2px solid var(--vscode-progressBar-background, #0e639c); border-top-color: transparent; border-radius: 50%; diff --git a/mpp-vscode/webview/src/App.tsx b/mpp-vscode/webview/src/App.tsx index 2783418eeb..2feed34acb 100644 --- a/mpp-vscode/webview/src/App.tsx +++ b/mpp-vscode/webview/src/App.tsx @@ -50,7 +50,7 @@ const App: React.FC = () => { currentStreamingContent: '', isProcessing: false, currentIteration: 0, - maxIterations: 10, + maxIterations: 100, tasks: [], }); @@ -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/Timeline.css b/mpp-vscode/webview/src/components/Timeline.css index 3d9cf35bf5..96a8a706c8 100644 --- a/mpp-vscode/webview/src/components/Timeline.css +++ b/mpp-vscode/webview/src/components/Timeline.css @@ -1,13 +1,10 @@ .timeline { display: flex; flex-direction: column; - gap: 16px; - padding: 16px; } .timeline-item { display: flex; - flex-direction: column; gap: 8px; } @@ -26,7 +23,8 @@ .timeline-item.message.assistant { background-color: transparent; border: none; - padding: 4px 12px; + gap: 0; + margin: 8px 4px; } .timeline-item.message.system { @@ -95,12 +93,12 @@ /* Tool call items */ .timeline-item.tool-call { - margin-left: 16px; + margin-left: 0; } /* Terminal items */ .timeline-item.terminal { - margin-left: 16px; + margin-left: 0; } /* Task complete items */ diff --git a/mpp-vscode/webview/src/components/Timeline.tsx b/mpp-vscode/webview/src/components/Timeline.tsx index 7c1c0b031c..77eada9bdd 100644 --- a/mpp-vscode/webview/src/components/Timeline.tsx +++ b/mpp-vscode/webview/src/components/Timeline.tsx @@ -34,14 +34,6 @@ export const Timeline: React.FC = ({ {/* Streaming content indicator */} {currentStreamingContent && (
-
- AutoDev - - - - - -
= ({ item, onAct const MessageItemRenderer: React.FC<{ item: MessageTimelineItem; onAction?: (action: string, data: any) => void }> = ({ item, onAction }) => { const { message, isStreaming } = item; const isUser = message.role === 'user'; - const isSystem = message.role === 'system'; return (
-
- - {isUser ? 'You' : isSystem ? 'System' : 'AutoDev'} - - {isStreaming && ( - - - - - - )} -
{isUser ? (

{message.content}

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 && ( -
)}
diff --git a/mpp-vscode/webview/src/utils/codeFence.ts b/mpp-vscode/webview/src/utils/codeFence.ts index 44042e0e22..7f27ad28cf 100644 --- a/mpp-vscode/webview/src/utils/codeFence.ts +++ b/mpp-vscode/webview/src/utils/codeFence.ts @@ -1,6 +1,12 @@ /** * CodeFence parser - mirrors mpp-core's CodeFence.parseAll() * Parses markdown content into code blocks and text blocks + * + * Handles: + * - Standard markdown code fences (```) + * - tags (converted from ```devin blocks) + * - tags + * - comments */ export interface CodeBlock { @@ -10,80 +16,223 @@ export interface CodeBlock { extension?: string; } +// Regex patterns matching mpp-core's CodeFence +const devinStartRegex = //; +const devinEndRegex = /<\/devin>/; +const thinkingStartRegex = //; +const thinkingEndRegex = /<\/thinking>/; +const walkthroughStartRegex = //; +const walkthroughEndRegex = //; +const normalCodeBlockRegex = /\s*```([\w#+ ]*)\n/; +const languageRegex = /\s*```([\w#+ ]*)/; + +/** + * Pre-process ```devin blocks to tags + * Matches mpp-core's preProcessDevinBlock + */ +function preProcessDevinBlock(content: string): string { + let currentContent = content; + + // Find all ```devin blocks + const devinMatches = [...content.matchAll(/(?:^|\n)```devin\n([\s\S]*?)\n```(?:\n|$)/g)]; + + for (const match of devinMatches) { + let devinContent = match[1] || ''; + + // Check if there's an unclosed code block inside + if (normalCodeBlockRegex.test(devinContent)) { + if (!devinContent.trim().endsWith('```')) { + devinContent += '\n```'; + } + } + + const replacement = `\n\n${devinContent}\n`; + currentContent = currentContent.replace(match[0], replacement); + } + + return currentContent; +} + /** * Parse content into code blocks and text blocks * Handles markdown code fences (```) and special block types */ export function parseCodeBlocks(content: string): CodeBlock[] { const blocks: CodeBlock[] = []; + let currentIndex = 0; + + // Pre-process ```devin blocks to tags + let processedContent = content; + if (content.includes('```devin\n')) { + processedContent = preProcessDevinBlock(content); + } + + // Find all special tag matches + interface TagMatch { + type: string; + match: RegExpExecArray; + } + const tagMatches: TagMatch[] = []; + + // Find tags + let match: RegExpExecArray | null; + const devinRegex = new RegExp(devinStartRegex.source, 'g'); + while ((match = devinRegex.exec(processedContent)) !== null) { + tagMatches.push({ type: 'devin', match }); + } + + // Find tags + const thinkingRegex = new RegExp(thinkingStartRegex.source, 'g'); + while ((match = thinkingRegex.exec(processedContent)) !== null) { + tagMatches.push({ type: 'thinking', match }); + } + + // Find tags + const walkthroughRegex = new RegExp(walkthroughStartRegex.source, 'g'); + while ((match = walkthroughRegex.exec(processedContent)) !== null) { + tagMatches.push({ type: 'walkthrough', match }); + } + + // Sort by position + tagMatches.sort((a, b) => a.match.index - b.match.index); + + // Process each tag match + for (const { type, match: startMatch } of tagMatches) { + if (startMatch.index >= currentIndex) { + // Parse content before this tag + if (startMatch.index > currentIndex) { + const beforeText = processedContent.substring(currentIndex, startMatch.index); + if (beforeText.trim()) { + parseMarkdownContent(beforeText, blocks); + } + } + + // Find the corresponding end tag + const endRegex = type === 'devin' ? devinEndRegex : + type === 'thinking' ? thinkingEndRegex : + walkthroughEndRegex; + + const endMatch = endRegex.exec(processedContent.substring(startMatch.index + startMatch[0].length)); + const isComplete = endMatch !== null; + + const tagContent = isComplete + ? processedContent.substring( + startMatch.index + startMatch[0].length, + startMatch.index + startMatch[0].length + endMatch!.index + ).trim() + : processedContent.substring(startMatch.index + startMatch[0].length).trim(); + + blocks.push({ + languageId: type, + text: tagContent, + isComplete, + extension: type + }); + + currentIndex = isComplete + ? startMatch.index + startMatch[0].length + endMatch!.index + endMatch![0].length + : processedContent.length; + } + } + + // Parse remaining content + if (currentIndex < processedContent.length) { + const remainingContent = processedContent.substring(currentIndex); + if (remainingContent.trim()) { + parseMarkdownContent(remainingContent, blocks); + } + } + + // Filter out empty blocks (except special types) + return blocks.filter(block => { + if (block.languageId === 'devin' || block.languageId === 'thinking' || block.languageId === 'walkthrough') { + return true; + } + return block.text.trim().length > 0; + }); +} + +/** + * Parse markdown content (text and code blocks) + */ +function parseMarkdownContent(content: string, blocks: CodeBlock[]): void { const lines = content.split('\n'); - - let currentBlock: CodeBlock | null = null; - let textBuffer: string[] = []; - - for (let i = 0; i < lines.length; i++) { - const line = lines[i]; - const trimmedLine = line.trim(); - - // Check for code fence start - if (trimmedLine.startsWith('```')) { - // Flush text buffer - if (textBuffer.length > 0) { - const text = textBuffer.join('\n').trim(); - if (text) { - blocks.push({ - languageId: '', - text, - isComplete: true - }); + + let codeStarted = false; + let languageId: string | null = null; + const codeBuilder: string[] = []; + const textBuilder: string[] = []; + + for (const line of lines) { + if (!codeStarted) { + const trimmedLine = line.trimStart(); + const matchResult = languageRegex.exec(trimmedLine); + + if (matchResult) { + // Save accumulated text + if (textBuilder.length > 0) { + const text = textBuilder.join('\n').trim(); + if (text) { + blocks.push({ + languageId: 'markdown', + text, + isComplete: true, + extension: 'md' + }); + } + textBuilder.length = 0; } - textBuffer = []; + + languageId = matchResult[1]?.trim() || null; + codeStarted = true; + } else { + textBuilder.push(line); } - - if (currentBlock) { + } else { + const trimmedLine = line.trimStart(); + + if (trimmedLine === '```') { // End of code block - currentBlock.isComplete = true; - blocks.push(currentBlock); - currentBlock = null; + const codeContent = codeBuilder.join('\n').trim(); + blocks.push({ + languageId: languageId || 'markdown', + text: codeContent, + isComplete: true, + extension: getExtensionForLanguage(languageId || 'md') + }); + + codeBuilder.length = 0; + codeStarted = false; + languageId = null; } else { - // Start of code block - const langMatch = trimmedLine.match(/^```(\w+)?/); - const languageId = langMatch?.[1] || ''; - currentBlock = { - languageId, - text: '', - isComplete: false, - extension: getExtensionForLanguage(languageId) - }; + codeBuilder.push(line); } - continue; - } - - if (currentBlock) { - // Inside code block - currentBlock.text += (currentBlock.text ? '\n' : '') + line; - } else { - // Regular text - textBuffer.push(line); } } - - // Handle remaining content - if (currentBlock) { - // Unclosed code block - blocks.push(currentBlock); - } else if (textBuffer.length > 0) { - const text = textBuffer.join('\n').trim(); + + // Add remaining text + if (textBuilder.length > 0) { + const text = textBuilder.join('\n').trim(); if (text) { blocks.push({ - languageId: '', + languageId: 'markdown', text, - isComplete: true + isComplete: true, + extension: 'md' }); } } - - return blocks; + + // Add unclosed code block + if (codeStarted && codeBuilder.length > 0) { + const code = codeBuilder.join('\n').trim(); + blocks.push({ + languageId: languageId || 'markdown', + text: code, + isComplete: false, + extension: getExtensionForLanguage(languageId || 'md') + }); + } } /**