diff --git a/package.json b/package.json index 7cb9134..ba6a9bd 100644 --- a/package.json +++ b/package.json @@ -667,6 +667,11 @@ "command": "DevChat.InstallCommandPython", "title": "Install Python for Commands", "category": "DevChat" + }, + { + "command": "DevChat.Chat", + "title": "Chat with DevChat", + "category": "DevChat" } ], "menus": { @@ -749,6 +754,10 @@ { "command": "devchat.askForFile_chinese", "when": "false" + }, + { + "command": "DevChat.Chat", + "when": "false" } ], "explorer/context": [ diff --git a/src/contributes/commands.ts b/src/contributes/commands.ts index 8d9d815..f236127 100644 --- a/src/contributes/commands.ts +++ b/src/contributes/commands.ts @@ -18,6 +18,7 @@ import { sendCommandListByDevChatRun, updateChatModels } from '../handler/workfl import DevChat from "../toolwrapper/devchat"; import { createEnvByConda, createEnvByMamba } from '../util/python_installer/app_install'; import { installRequirements } from '../util/python_installer/package_install'; +import { chatWithDevChat } from '../handler/chatHandler'; function registerOpenChatPanelCommand(context: vscode.ExtensionContext) { @@ -340,6 +341,15 @@ export function registerInstallCommandsPython(context: vscode.ExtensionContext) context.subscriptions.push(disposable); } +export function registerDevChatChatCommand(context: vscode.ExtensionContext) { + let disposable = vscode.commands.registerCommand('DevChat.Chat', async (message: string) => { + ensureChatPanel(context); + chatWithDevChat(ExtensionContextHolder.provider?.view()!, message); + }); + + context.subscriptions.push(disposable); +} + export { registerOpenChatPanelCommand, registerAddContextCommand, diff --git a/src/extension.ts b/src/extension.ts index 9ce766e..967cdfd 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -16,7 +16,8 @@ import { regPythonPathCommand, registerInstallCommandsCommand, registerUpdateChatModelsCommand, - registerInstallCommandsPython + registerInstallCommandsPython, + registerDevChatChatCommand } from './contributes/commands'; import { regLanguageContext } from './contributes/context'; import { regDevChatView, regTopicView } from './contributes/views'; @@ -29,6 +30,7 @@ import { UiUtilWrapper } from './util/uiUtil'; import { UiUtilVscode } from './util/uiUtil_vscode'; import { ApiKeyManager } from './util/apiKey'; import { startRpcServer } from './ide_services/services'; +import { registerCodeLensProvider } from './panel/codeLens'; async function isProviderHasSetted() { try { @@ -216,6 +218,7 @@ async function activate(context: vscode.ExtensionContext) { await configUpdateTo1115(); regLanguageContext(); + registerCodeLensProvider(context); regDevChatView(context); regTopicView(context); @@ -241,6 +244,7 @@ async function activate(context: vscode.ExtensionContext) { regApplyDiffResultCommand(context); regPythonPathCommand(context); + registerDevChatChatCommand(context); startRpcServer(); } diff --git a/src/handler/chatHandler.ts b/src/handler/chatHandler.ts new file mode 100644 index 0000000..936699f --- /dev/null +++ b/src/handler/chatHandler.ts @@ -0,0 +1,6 @@ +import { MessageHandler } from './messageHandler'; + + +export async function chatWithDevChat(panel, message: string) { + MessageHandler.sendMessage(panel!, { command: 'chatWithDevChat', 'message': message }); +} diff --git a/src/ide_services/services.ts b/src/ide_services/services.ts index efb61cb..c623ff7 100644 --- a/src/ide_services/services.ts +++ b/src/ide_services/services.ts @@ -1,9 +1,17 @@ import * as http from 'http'; -import * as url from 'url'; +import * as vscode from 'vscode'; +import * as path from 'path'; +import { exec } from 'child_process'; + import * as querystring from 'querystring'; import { logger } from '../util/logger'; import { UiUtilWrapper } from '../util/uiUtil'; +import { createEnvByConda, createEnvByMamba } from '../util/python_installer/app_install'; +import { installRequirements } from '../util/python_installer/package_install'; + + + const functionRegistry: any = { // eslint-disable-next-line @typescript-eslint/naming-convention @@ -12,6 +20,72 @@ const functionRegistry: any = { "handler": async () => { return await UiUtilWrapper.getLSPBrigePort(); } + }, + // eslint-disable-next-line @typescript-eslint/naming-convention + "/update_slash_commands": { + "keys": [], + "handler": async () => { + vscode.commands.executeCommand('DevChat.InstallCommands'); + return true; + } + }, + // eslint-disable-next-line @typescript-eslint/naming-convention + "/open_folder": { + "keys": ["folder"], + "handler": async (folder: string) => { + // open folder by vscode + const folderPathParsed = folder.replace('\\', '/'); + // Updated Uri.parse to Uri.file + const folderUri = vscode.Uri.file(folderPathParsed); + vscode.commands.executeCommand(`vscode.openFolder`, folderUri); + return true; + } + }, + // eslint-disable-next-line @typescript-eslint/naming-convention + "/install_python_env": { + "keys": ["command_name", "requirements_file"], + // eslint-disable-next-line @typescript-eslint/naming-convention + "handler": async (command_name: string, requirements_file: string) => { + // 1. install python >= 3.11 + logger.channel()?.info(`create env for python ...`); + logger.channel()?.info(`try to create env by mamba ...`); + let pythonCommand = await createEnvByMamba(command_name, "", "3.11.4"); + + if (!pythonCommand || pythonCommand === "") { + logger.channel()?.info(`create env by mamba failed, try to create env by conda ...`); + pythonCommand = await createEnvByConda(command_name, "", "3.11.4"); + } + + if (!pythonCommand || pythonCommand === "") { + logger.channel()?.error(`create virtual python env failed, you need create it by yourself with command: "conda create -n devchat-commands python=3.11.4"`); + logger.channel()?.show(); + + return ""; + } + + // 3. install requirements.txt + // run command: pip install -r {requirementsFile} + let isInstalled = false; + // try 3 times + for (let i = 0; i < 4; i++) { + let otherSource: string | undefined = undefined; + if (i>1) { + otherSource = 'https://pypi.tuna.tsinghua.edu.cn/simple/'; + } + isInstalled = await installRequirements(pythonCommand, requirements_file, otherSource); + if (isInstalled) { + break; + } + logger.channel()?.info(`Install packages failed, try again: ${i + 1}`); + } + if (!isInstalled) { + logger.channel()?.error(`Install packages failed, you can install it with command: "${pythonCommand} -m pip install -r ${requirements_file}"`); + logger.channel()?.show(); + return ''; + } + + return pythonCommand.trim(); + } } }; diff --git a/src/panel/codeLens.ts b/src/panel/codeLens.ts new file mode 100644 index 0000000..b171c81 --- /dev/null +++ b/src/panel/codeLens.ts @@ -0,0 +1,182 @@ +import * as vscode from 'vscode'; +import * as fs from 'fs'; +import * as path from 'path'; +import { has } from 'mobx'; + +interface FunctionDefinition { + name: string; + containerName: string | null; + range: vscode.Range; +} + +type CodeLensRegistration = { + elementType: string; + objectName: string; + promptGenerator: string; +}; + +export class CodeLensManager { + private static instance: CodeLensManager; + private registrations: CodeLensRegistration[] = []; + private configFilePath: string; + + private constructor() { + this.configFilePath = path.join(process.env.HOME || process.env.USERPROFILE || '.', '.chat/ideconfig.json'); + this.loadConfig(); + } + + public static getInstance(): CodeLensManager { + if (!CodeLensManager.instance) { + CodeLensManager.instance = new CodeLensManager(); + } + return CodeLensManager.instance; + } + + private loadConfig(): void { + if (!fs.existsSync(this.configFilePath)) { + this.initializeConfig(); + } else { + const data = fs.readFileSync(this.configFilePath, 'utf8'); + this.registrations = JSON.parse(data); + } + } + + private initializeConfig(): void { + this.registrations = [ + // { + // elementType: 'function', + // objectName: 'generate unit tests', + // promptGenerator: '/test generate unit tests for {__filename__} {__functionName__}' + // }, + // { + // elementType: 'inner_function', + // objectName: 'generate comment', + // promptGenerator: 'generate comment for \n ```code\n{__functionCode__}\n```\n' + // }, + // { + // elementType: 'function', + // objectName: 'generate comment', + // promptGenerator: 'generate comment for \n ```code\n{__functionCode__}\n```\n' + // } + ]; + this.saveConfig(); + } + + private saveConfig(): void { + const configDir = path.dirname(this.configFilePath); + if (!fs.existsSync(configDir)) { + fs.mkdirSync(configDir, { recursive: true }); + } + fs.writeFileSync(this.configFilePath, JSON.stringify(this.registrations, null, 2), 'utf8'); + } + + public getRegistrations(): CodeLensRegistration[] { + return this.registrations; + } +} + + +async function getFunctionDefinitions(document: vscode.TextDocument, inner_function: boolean = false): Promise { + const symbols: vscode.DocumentSymbol[] | undefined = await vscode.commands.executeCommand( + 'vscode.executeDocumentSymbolProvider', + document.uri + ); + + if (!symbols) { + return []; + } + + function extractFunctions(symbol: vscode.DocumentSymbol, containerName: string | null, hasInFunction: boolean = false): FunctionDefinition[] { + console.log('==> symbol range:', symbol.range, symbol.name, symbol.kind); + let functions: FunctionDefinition[] = []; + if (symbol.kind === vscode.SymbolKind.Function || symbol.kind === vscode.SymbolKind.Method) { + if (!inner_function || (inner_function && hasInFunction)) { + functions.push({ + name: symbol.name, + containerName: containerName, + range: symbol.range + }); + } + hasInFunction = true; + } + + if (inner_function) { + if (symbol.children && symbol.children.length > 0) { + symbol.children.forEach(child => { + functions = functions.concat(extractFunctions(child, symbol.name, hasInFunction)); + }); + } + } + + return functions; + } + + let functionSymbols: FunctionDefinition[] = []; + symbols.forEach(symbol => { + functionSymbols = functionSymbols.concat(extractFunctions(symbol, null)); + }); + + return functionSymbols; +} + + + +class FunctionTestCodeLensProvider implements vscode.CodeLensProvider { + // The provideCodeLenses method should have the correct signature + async provideCodeLenses(document: vscode.TextDocument, token: vscode.CancellationToken): Promise { + const lenses: vscode.CodeLens[] = []; + const functionDefinitions = await getFunctionDefinitions(document); + const innerFunctionDefinitions = await getFunctionDefinitions(document, true); + + const matchElements = { + 'function': functionDefinitions, + 'inner_function': innerFunctionDefinitions + }; + + for (const [elementType, elements] of Object.entries(matchElements)) { + elements.forEach((funcDef) => { + const range = new vscode.Range( + new vscode.Position(funcDef.range.start.line, 0), + new vscode.Position(funcDef.range.end.line, 10000) + ); + + const codelenRegisters: CodeLensRegistration[] = CodeLensManager.getInstance().getRegistrations(); + // Iterate over codelenRegisters with 'of' instead of 'in' + for (const codelenRegister of codelenRegisters) { + if (codelenRegister.elementType !== elementType) { + continue; + } + + // Read range content in document + const functionCode = document.getText(range); + + // Fix the string replacement syntax and closing parentheses + const prompt = codelenRegister.promptGenerator + .replace('{__filename__}', document.uri.fsPath) + .replace('{__functionName__}', funcDef.name) + .replace('{__functionRange__}', `[${range.start.line}, ${range.end.line}]`) + .replace('{__functionCode__}', functionCode); // Fixed syntax + + const lens = new vscode.CodeLens(range, { + title: codelenRegister.objectName, + command: "DevChat.Chat", + // arguments: [document.uri.fsPath, range, funcDef.name] // Commented out as it's not used + arguments: [prompt] + }); + + lenses.push(lens); + } + }); + } + + return lenses; + } +} + + +export function registerCodeLensProvider(context) { + const provider = new FunctionTestCodeLensProvider(); + const disposable = vscode.languages.registerCodeLensProvider("*", provider); + + context.subscriptions.push(disposable); +} diff --git a/src/toolwrapper/devchat.ts b/src/toolwrapper/devchat.ts index 7848c15..bc12be7 100644 --- a/src/toolwrapper/devchat.ts +++ b/src/toolwrapper/devchat.ts @@ -247,10 +247,6 @@ class DevChat { // eslint-disable-next-line @typescript-eslint/naming-convention "command_python": UiUtilWrapper.getConfiguration('DevChat', 'PythonForCommands') || "python3", // eslint-disable-next-line @typescript-eslint/naming-convention - "DEVCHATPYTHON": UiUtilWrapper.getConfiguration("DevChat", "PythonForChat") || "python3", - // eslint-disable-next-line @typescript-eslint/naming-convention - "PYTHONLIBPATH": UiUtilWrapper.extensionPath() + "/tools/site-packages", - // eslint-disable-next-line @typescript-eslint/naming-convention "PYTHONPATH": UiUtilWrapper.extensionPath() + "/tools/site-packages", // eslint-disable-next-line @typescript-eslint/naming-convention "OPENAI_API_KEY": llmModelData.api_key, diff --git a/src/views/components/InputMessage/index.tsx b/src/views/components/InputMessage/index.tsx index 655af57..efdd2bb 100644 --- a/src/views/components/InputMessage/index.tsx +++ b/src/views/components/InputMessage/index.tsx @@ -137,6 +137,9 @@ const InputMessage = observer((props: any) => { messageUtil.registerHandler('regCommandList', (message: { result: object[]}) => { input.updateCommands(message.result); }); + messageUtil.registerHandler('chatWithDevChat', (message: {command: string, message: string}) => { + chat.commonMessage(message.message, []); + }); messageUtil.registerHandler('appendContext', (message: { command: string; context: string }) => { // context is a temp file path const match = /\|([^]+?)\]/.exec(message.context); diff --git a/tools b/tools index d157dfc..fb4d456 160000 --- a/tools +++ b/tools @@ -1 +1 @@ -Subproject commit d157dfc834ebd5e40be62a32c0406999374bbff6 +Subproject commit fb4d4565e8346020d4a42064db79068e2912f38c