diff --git a/package.json b/package.json index 709ef13..49449a1 100644 --- a/package.json +++ b/package.json @@ -248,6 +248,10 @@ "command": "devchat.addConext", "title": "Add to DevChat" }, + { + "command": "devchat.addSummaryContext", + "title": "Add summary to DevChat" + }, { "command": "devchat.askForCode", "title": "Add to DevChat" @@ -277,6 +281,16 @@ "command": "DevChat.AskCodeIndexStop", "title": "Stop AskCode Index", "category": "DevChat" + }, + { + "command": "DevChat.AskCodeSummaryIndexStart", + "title": "Start AskCode Summary Index", + "category": "DevChat" + }, + { + "command": "DevChat.AskCodeIndexSummaryStop", + "title": "Stop AskCode Summary Index", + "category": "DevChat" } ], "menus": { @@ -340,6 +354,10 @@ "command": "devchat.addConext", "when": "false" }, + { + "command": "devchat.addSummaryContext", + "when": "false" + }, { "command": "devchat.askForCode", "when": "false" @@ -371,6 +389,11 @@ "when": "!isChineseLocale && resourceLangId != 'git'", "command": "devchat.addConext", "group": "navigation" + }, + { + "when": "!isChineseLocale && resourceLangId != 'git'", + "command": "devchat.addSummaryContext", + "group": "navigation" } ], "editor/context": [ diff --git a/src/contributes/commands.ts b/src/contributes/commands.ts index 8b92276..d1281d3 100644 --- a/src/contributes/commands.ts +++ b/src/contributes/commands.ts @@ -1,4 +1,5 @@ import * as vscode from 'vscode'; +import * as fs from 'fs'; import { sendFileSelectMessage, sendCodeSelectMessage } from './util'; import ExtensionContextHolder from '../util/extensionContext'; import { TopicManager } from '../topic/topicManager'; @@ -9,14 +10,16 @@ import { UiUtilWrapper } from '../util/uiUtil'; import { isValidApiKey } from '../handler/historyMessagesBase'; import { logger } from '../util/logger'; -import { CommandRun } from '../util/commonUtil'; +import { CommandRun, createTempSubdirectory, runCommandAndWriteOutput, runCommandStringAndWriteOutput, runCommandStringArrayAndWriteOutput } from '../util/commonUtil'; import { updateIndexingStatus, updateLastModifyTime } from '../util/askCodeUtil'; import { installAskCode as installAskCodeFun } from '../util/python_installer/install_askcode'; import { ProgressBar } from '../util/progressBar'; +import path from 'path'; +import { MessageHandler } from '../handler/messageHandler'; let indexProcess: CommandRun | null = null; - +let summaryIndexProcess: CommandRun | null = null; function registerOpenChatPanelCommand(context: vscode.ExtensionContext) { let disposable = vscode.commands.registerCommand('devchat.openChatPanel', async () => { @@ -248,7 +251,7 @@ export function registerAskCodeIndexStartCommand(context: vscode.ExtensionContex if (!pythonVirtualEnv) { progressBar.update("Install devchat-ask package ...", 0); - await installAskCode(supportedFileTypes, progressBar); + await installAskCode(supportedFileTypes, progressBar, indexCode); } else { progressBar.update("Index source files ...", 0); await indexCode(pythonVirtualEnv, supportedFileTypes, progressBar); @@ -266,19 +269,20 @@ function getConfig() { }; } -async function installAskCode(supportedFileTypes, progressBar: any) { - const pythonEnvPath : string = await installAskCodeFun(); - if (!pythonEnvPath) { - logger.channel()?.error(`Installation failed!`); - logger.channel()?.show(); - return; - } - UiUtilWrapper.updateConfiguration("DevChat", "PythonVirtualEnv", pythonEnvPath.trim()); +async function installAskCode(supportedFileTypes, progressBar: any, callback: Function) { + const pythonEnvPath : string = await installAskCodeFun(); + if (!pythonEnvPath) { + logger.channel()?.error(`Installation failed!`); + logger.channel()?.show(); + return; + } + + UiUtilWrapper.updateConfiguration("DevChat", "PythonVirtualEnv", pythonEnvPath.trim()); logger.channel()?.info(`Installation finished.`); - // Execute the indexing command after the installation is finished - await indexCode(pythonEnvPath, supportedFileTypes, progressBar); + // Execute the callback function after the installation is finished + await callback(pythonEnvPath, supportedFileTypes, progressBar); } async function indexCode(pythonVirtualEnv, supportedFileTypes, progressBar: any) { @@ -349,6 +353,136 @@ export function registerAskCodeIndexStopCommand(context: vscode.ExtensionContext context.subscriptions.push(disposable); } +export function registerAskCodeSummaryIndexStartCommand(context: vscode.ExtensionContext) { + let disposable = vscode.commands.registerCommand('DevChat.AskCodeSummaryIndexStart', async () => { + const progressBar = new ProgressBar(); + progressBar.init(); + + progressBar.update("Index source code files for ask codebase summary...", 0); + + const config = getConfig(); + const pythonVirtualEnv = config.pythonVirtualEnv; + const supportedFileTypes = config.supportedFileTypes; + + updateIndexingStatus("started"); + + if (!pythonVirtualEnv) { + progressBar.update("Install devchat-ask package ...", 0); + await installAskCode(supportedFileTypes, progressBar, indexCodeSummary); + } else { + progressBar.update("Index source files for summary...", 0); + await indexCodeSummary(pythonVirtualEnv, supportedFileTypes, progressBar); + } + + updateIndexingStatus("stopped"); + }); + context.subscriptions.push(disposable); +} + +async function indexCodeSummary(pythonVirtualEnv, supportedFileTypes, progressBar: any) { + let envs = {}; + + let openaiApiKey = await ApiKeyManager.getApiKey(); + if (!openaiApiKey) { + logger.channel()?.error('The OpenAI key is invalid!'); + logger.channel()?.show(); + + progressBar.endWithError("The OpenAI key is invalid!"); + return; + } + envs['OPENAI_API_KEY'] = openaiApiKey; + + const openAiApiBase = ApiKeyManager.getEndPoint(openaiApiKey); + if (openAiApiBase) { + envs['OPENAI_API_BASE'] = openAiApiBase; + } + + const workspaceDir = UiUtilWrapper.workspaceFoldersFirstPath(); + + const command = pythonVirtualEnv.trim(); + const args = [UiUtilWrapper.extensionPath() + "/tools/askcode_summary_index.py", "index", supportedFileTypes]; + const options = { env: envs, cwd: workspaceDir }; + + summaryIndexProcess = new CommandRun(); + const result = await summaryIndexProcess.spawnAsync(command, args, options, (data) => { + if (data.includes('Skip file:')) { + return; + } + logger.channel()?.info(`${data}`); + }, (data) => { + if (data.includes('Skip file:')) { + return; + } + logger.channel()?.info(`${data}`); + }, undefined, undefined); + + if (result.exitCode !== 0) { + if (result.exitCode === null) { + logger.channel()?.info(`Indexing stopped!`); + progressBar.endWithError(`Indexing stopped!`); + } else { + logger.channel()?.error(`Indexing failed: ${result.stderr}`); + logger.channel()?.show(); + progressBar.endWithError(`Indexing failed: ${result.stderr}`); + } + + return; + } + + updateLastModifyTime(); + logger.channel()?.info(`index finished.`); + + progressBar.update("Indexing finished."); + progressBar.end(); +} + +export function registerAskCodeSummaryIndexStopCommand(context: vscode.ExtensionContext) { + let disposable = vscode.commands.registerCommand('DevChat.AskCodeIndexSummaryStop', async () => { + // 在这里实现停止索引的功能 + // 你可能需要检查summaryIndexProcess变量是否为null,如果不为null,那么停止索引进程 + if (summaryIndexProcess) { + summaryIndexProcess.stop(); + summaryIndexProcess = null; + } + }); + context.subscriptions.push(disposable); +} + +export function registerAddSummaryContextCommand(context: vscode.ExtensionContext) { + const callback = async (uri: { fsPath: any; }) => { + if (!await ensureChatPanel(context)) { + return; + } + + const workspaceDir = UiUtilWrapper.workspaceFoldersFirstPath(); + if (!workspaceDir) { + return ; + } + // check whether workspaceDir/.chat/.summary.json文件存在 + if (!fs.existsSync(path.join(workspaceDir, '.chat', '.summary.json'))) { + logger.channel()?.info(`You should index this workspace first.`); + logger.channel()?.show(); + return; + } + + const config = getConfig(); + const pythonVirtualEnv: any = config.pythonVirtualEnv; + + const tempDir = await createTempSubdirectory('devchat/context'); + const summaryFile = path.join(tempDir, 'summary.txt'); + + const summaryArgs = [pythonVirtualEnv, UiUtilWrapper.extensionPath() + "/tools/askcode_summary_index.py", "desc", uri.fsPath]; + const result = await runCommandStringArrayAndWriteOutput(summaryArgs, summaryFile); + logger.channel()?.info(` exit code:`, result.exitCode); + + logger.channel()?.debug(` stdout:`, result.stdout); + logger.channel()?.debug(` stderr:`, result.stderr); + MessageHandler.sendMessage(ExtensionContextHolder.provider?.view()!, { command: 'appendContext', context: `[context|${summaryFile}]` }); + }; + context.subscriptions.push(vscode.commands.registerCommand('devchat.addSummaryContext', callback)); +} + + export { registerOpenChatPanelCommand, registerAddContextCommand, diff --git a/src/extension.ts b/src/extension.ts index 8c2a9b4..b358b1e 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -1,22 +1,25 @@ import * as vscode from 'vscode'; import { - registerOpenChatPanelCommand, - registerAddContextCommand, - registerAskForCodeCommand, - registerAskForFileCommand, - registerOpenAiApiKeySettingCommand, - registerDevChatApiKeySettingCommand, - regTopicDeleteCommand, - regAddTopicCommand, - regDeleteSelectTopicCommand, - regSelectTopicCommand, - regReloadTopicCommand, - regApplyDiffResultCommand, - registerStatusBarItemClickCommand, - regPythonPathCommand, - registerAskCodeIndexStartCommand, + registerOpenChatPanelCommand, + registerAddContextCommand, + registerAskForCodeCommand, + registerAskForFileCommand, + registerOpenAiApiKeySettingCommand, + registerDevChatApiKeySettingCommand, + regTopicDeleteCommand, + regAddTopicCommand, + regDeleteSelectTopicCommand, + regSelectTopicCommand, + regReloadTopicCommand, + regApplyDiffResultCommand, + registerStatusBarItemClickCommand, + regPythonPathCommand, + registerAskCodeIndexStartCommand, registerAskCodeIndexStopCommand, + registerAskCodeSummaryIndexStartCommand, + registerAskCodeSummaryIndexStopCommand, + registerAddSummaryContextCommand, } from './contributes/commands'; import { regLanguageContext } from './contributes/context'; import { regDevChatView, regTopicView } from './contributes/views'; @@ -30,36 +33,39 @@ import { UiUtilVscode } from './util/uiUtil_vscode'; function activate(context: vscode.ExtensionContext) { - ExtensionContextHolder.context = context; + ExtensionContextHolder.context = context; - logger.init(LoggerChannelVscode.getInstance()); - UiUtilWrapper.init(new UiUtilVscode()); + logger.init(LoggerChannelVscode.getInstance()); + UiUtilWrapper.init(new UiUtilVscode()); - regLanguageContext(); + regLanguageContext(); - regDevChatView(context); - regTopicView(context); + regDevChatView(context); + regTopicView(context); - registerOpenAiApiKeySettingCommand(context); - registerDevChatApiKeySettingCommand(context); - registerOpenChatPanelCommand(context); - registerAddContextCommand(context); - registerAskForCodeCommand(context); - registerAskForFileCommand(context); - registerStatusBarItemClickCommand(context); + registerOpenAiApiKeySettingCommand(context); + registerDevChatApiKeySettingCommand(context); + registerOpenChatPanelCommand(context); + registerAddContextCommand(context); + registerAskForCodeCommand(context); + registerAskForFileCommand(context); + registerStatusBarItemClickCommand(context); - createStatusBarItem(context); - createAskCodeStatusBarItem(context); + createStatusBarItem(context); + createAskCodeStatusBarItem(context); - regTopicDeleteCommand(context); - regAddTopicCommand(context); - regDeleteSelectTopicCommand(context); - regSelectTopicCommand(context); - regReloadTopicCommand(context); - regApplyDiffResultCommand(context); + regTopicDeleteCommand(context); + regAddTopicCommand(context); + regDeleteSelectTopicCommand(context); + regSelectTopicCommand(context); + regReloadTopicCommand(context); + regApplyDiffResultCommand(context); - regPythonPathCommand(context); - registerAskCodeIndexStartCommand(context); + regPythonPathCommand(context); + registerAskCodeIndexStartCommand(context); registerAskCodeIndexStopCommand(context); + registerAskCodeSummaryIndexStartCommand(context); + registerAskCodeSummaryIndexStopCommand(context); + registerAddSummaryContextCommand(context); } -exports.activate = activate; +exports.activate = activate; \ No newline at end of file diff --git a/tools/askcode_summary_index.py b/tools/askcode_summary_index.py new file mode 100644 index 0000000..0ca6a80 --- /dev/null +++ b/tools/askcode_summary_index.py @@ -0,0 +1,139 @@ +import os +import re +import json +import sys +from chat.ask_codebase.indexing.loader.file import FileMetadata, FileSource, simple_file_filter +from chat.ask_codebase.indexing.module_summary import SummaryWrapper + +# 为已经分析的文件记录最后修改时间 +g_file_last_modified_saved = {} + +def load_file_last_modified(filePath: str): + if not os.path.exists(filePath): + return {} + + with open(filePath, 'r', encoding="utf-8") as f: + fileLastModified = json.load(f) + + return fileLastModified + +def save_file_last_modified(filePath: str, fileLastModified: dict): + with open(filePath, 'w+', encoding="utf-8") as f: + json.dump(fileLastModified, f) + + return fileLastModified + +def is_source_code_new(filePath: str, supportedFileTypes): + for pattern in supportedFileTypes: + if re.match(pattern.strip(), filePath): + return True + return False + +def is_file_modified(filePath: str, supportedFileTypes) -> bool: + if not is_source_code_new(filePath, supportedFileTypes): + return False + + relativePath = os.path.relpath(filePath, os.getcwd()) + + for part in relativePath.split(os.sep): + if part.startswith('.'): + return False + + fileLastModified = g_file_last_modified_saved.get(relativePath, 0) + fileCurrentModified = os.path.getmtime(filePath) + + if fileLastModified != fileCurrentModified: + g_file_last_modified_saved[relativePath] = fileCurrentModified + return True + return False + +def custom_file_filter(file_path: str, supportedFileTypes) -> bool: + print("==> ", file_path) + + if os.path.isdir(file_path): + return True + + return is_file_modified(file_path, supportedFileTypes) + +def index_directory(repo_dir: str, repo_cache_path: str, supportedFileTypes): + """ + index files in repo_dir + """ + global g_file_last_modified_saved + g_file_last_modified_saved = load_file_last_modified('.chat/.index_modified.json') + + sw = SummaryWrapper(repo_cache_path, FileSource( + path=repo_dir, + rel_root=repo_dir, + file_filter=lambda file_path: custom_file_filter(file_path, supportedFileTypes), + )) + + for progress_info in sw.reindex(True, []): + print(progress_info) + + save_file_last_modified('.chat/.index_modified.json', g_file_last_modified_saved) + +def desc(repo_dir: str, repo_cache_path: str, target_path: str): + """ + """ + target_path = target_path.replace(repo_dir, '') + sw = SummaryWrapper(repo_cache_path, FileSource( + path=repo_dir, + rel_root=repo_dir, + file_filter=simple_file_filter, + )) + return sw.get_desc(target_path) + +def context(repo_dir: str, repo_cache_path: str, target_path: str): + """ + """ + target_path = os.path.relpath(target_path, repo_dir) + sw = SummaryWrapper(repo_cache_path, FileSource( + path=repo_dir, + rel_root=repo_dir, + file_filter=simple_file_filter, + )) + return sw.prepare_context_by_top_module(target_path) + +def main(): + if len(sys.argv) < 2: + print("Usage: python askcode_summary_index.py [command] [args]") + print("Available commands: index, desc, context") + sys.exit(1) + + command = sys.argv[1] + + # Set default values for repo_dir and repo_cache_path + repo_dir = os.getcwd() + repo_cache_path = os.path.join(repo_dir, '.chat', '.summary.json') + + if command == "index": + if len(sys.argv) < 3: + print("Usage: python askcode_summary_index.py index [supportedFileTypes]") + sys.exit(1) + + supportedFileTypes = sys.argv[2].split(',') + index_directory(repo_dir, repo_cache_path, supportedFileTypes) + + elif command == "desc": + if len(sys.argv) < 3: + print("Usage: python askcode_summary_index.py desc [target_path]") + sys.exit(1) + + target_path = sys.argv[2] + print(desc(repo_dir, repo_cache_path, target_path)) + + elif command == "context": + if len(sys.argv) < 3: + print("Usage: python askcode_summary_index.py context [target_path]") + sys.exit(1) + + target_path = sys.argv[2] + print(context(repo_dir, repo_cache_path, target_path)) + + else: + print("Invalid command. Available commands: index, desc, context") + sys.exit(1) + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/workflows/auto_command/action/summary/_setting_.json b/workflows/auto_command/action/summary/_setting_.json new file mode 100644 index 0000000..951b301 --- /dev/null +++ b/workflows/auto_command/action/summary/_setting_.json @@ -0,0 +1,15 @@ +{ + "name": "summary", + "description": "Get summary of specified file or direcotry in workspace", + "type": ["project"], + "action": "summary", + "args": [ + { + "name": "path", + "description": "The relative path of the specified file or folder, for example, /hello.py, represents the hello.py file in the root directory. This arg is required.", + "type": "string", + "from": "content.content.path" + } + ], + "handler": ["${PythonVirtualEnv}", "${CurDir}/handler.py", "${path}"] +} \ No newline at end of file diff --git a/workflows/auto_command/action/summary/handler.py b/workflows/auto_command/action/summary/handler.py new file mode 100644 index 0000000..b4d980e --- /dev/null +++ b/workflows/auto_command/action/summary/handler.py @@ -0,0 +1,37 @@ +""" +""" + +import os +import sys +from chat.ask_codebase.indexing.loader.file import FileMetadata, FileSource, simple_file_filter +from chat.ask_codebase.indexing.module_summary import SummaryWrapper + +def desc(repo_dir: str, repo_cache_path: str, target_path: str): + """ + """ + target_path = target_path.replace(repo_dir, '') + sw = SummaryWrapper(repo_cache_path, FileSource( + path=repo_dir, + rel_root=repo_dir, + file_filter=simple_file_filter, + )) + return sw.get_desc(target_path) + + +def summary(): + """ + Get file or directory 's summary + """ + try: + repo_dir = os.getcwd() + repo_cache_path = os.path.join(repo_dir, '.chat', '.summary.json') + + target_path = sys.argv[1] + return desc(repo_dir, repo_cache_path, target_path) + except Exception as e: + sys.stderr.write(f"Error: {str(e)}\n") + sys.exit(1) + +if __name__ == "__main__": + print(summary()) + sys.exit(0) \ No newline at end of file