diff --git a/package.json b/package.json index b4c7196..a2f044b 100644 --- a/package.json +++ b/package.json @@ -93,6 +93,10 @@ "command": "aide.smartPaste", "title": "%command.smartPaste%" }, + { + "command": "aide.batchProcessor", + "title": "%command.batchProcessor%" + }, { "command": "aide.copyFileText", "title": "%command.copyFileText%", @@ -125,6 +129,11 @@ "when": "explorerResourceIsFolder || selectedFilesCount > 0 || resourceLangId", "command": "aide.askAI", "group": "0_aide@1" + }, + { + "when": "explorerResourceIsFolder || selectedFilesCount > 0 || resourceLangId", + "command": "aide.batchProcessor", + "group": "0_aide@2" } ], "editor/context": [ @@ -174,6 +183,11 @@ "default": "https://api.openai.com/v1", "markdownDescription": "%config.openaiBaseUrl.description%" }, + "aide.apiConcurrency": { + "type": "number", + "default": 1, + "markdownDescription": "%config.apiConcurrency.description%" + }, "aide.useSystemProxy": { "type": "boolean", "default": true, @@ -311,6 +325,7 @@ "langchain": "^0.2.10", "lint-staged": "^15.2.7", "minimatch": "^9.0.5", + "p-limit": "^6.1.0", "pnpm": "^9.6.0", "prettier": "^3.3.3", "rimraf": "^6.0.1", diff --git a/package.nls.en.json b/package.nls.en.json index ee6d0c5..0272506 100644 --- a/package.nls.en.json +++ b/package.nls.en.json @@ -5,6 +5,7 @@ "command.codeViewerHelper": "✨ Aide: Code Viewer Helper", "command.renameVariable": "✨ Aide: Rename Variable", "command.smartPaste": "✨ Aide: Smart Paste", + "command.batchProcessor": "✨ Aide: AI Batch Processor", "command.copyFileText": "Copy text", "command.quickCloseFileWithoutSave": "Quick close", "command.replaceFile": "Replace original", @@ -13,6 +14,7 @@ "config.openaiKey.description": "OpenAI Key, [click to view online documentation](https://aide.nicepkg.cn/guide/configuration/openai-key)", "config.openaiModel.description": "OpenAI Model, [click to view online documentation](https://aide.nicepkg.cn/guide/configuration/openai-model)", "config.openaiBaseUrl.description": "OpenAI Base URL, [click to view online documentation](https://aide.nicepkg.cn/guide/configuration/openai-base-url)", + "config.apiConcurrency.description": "API request concurrency, [click to view online documentation](https://aide.nicepkg.cn/guide/configuration/api-concurrency)", "config.useSystemProxy.description": "Use global proxy (`HTTP_PROXY`, `HTTPS_PROXY`, `ALL_PROXY`), you need to restart `VSCode` to take effect after changing this setting, [click to view online documentation](https://aide.nicepkg.cn/guide/configuration/use-system-proxy)", "config.codeViewerHelperPrompt.description": "Code viewer helper prompt template, [click to view online documentation](https://aide.nicepkg.cn/guide/configuration/code-viewer-helper-prompt)", "config.convertLanguagePairs.description": "Default convert language pairs, [click to view online documentation](https://aide.nicepkg.cn/guide/configuration/convert-language-pairs)", @@ -36,14 +38,14 @@ "error.configKeyRequired": "{0} configuration key is required", "error.vscodeLLMModelNotFound": "VSCode LLM model not found, please check configuration", "error.noSelection": "No file or folder selected", - "error.noActiveEditor": "No file is currently open", + "error.noActiveEditor": "Please open any file first to determine workspace", "error.noTargetLanguage": "No target language selected", "error.noContext": "Context not initialized", "error.emptyClipboard": "Clipboard is empty", "error.xclipNotFound": "xclip is not installed. Please install it using your package manager (e.g., sudo apt-get install xclip)", "error.fileNotFound": "File not found", + "error.invalidInput": "Invalid input", "info.copied": "File contents have been copied to clipboard", - "info.customLanguage": "Custom language", "info.noAiSuggestionsVariableName": "AI thinks your variable name is already good", "info.processing": "Aide is processing...", "info.continueMessage": "Continue? I'm not sure if it's done yet, if there's still content not generated, you can click continue.", @@ -52,11 +54,14 @@ "info.cancel": "Cancel", "info.commandCopiedToClipboard": "AI command has been copied to clipboard", "info.fileReplaceSuccess": "File content has been replaced successfully", + "info.batchProcessorSuccess": "AI batch processor success!\n\nTotal {0} files generated, you can review and replace manually.\n\nTasks completed:\n{1}", "input.array.promptEnding": "Enter comma separated values", "input.json.promptEnding": "Enter JSON formatted value", "input.aiCommand.prompt": "Enter question for AI command", "input.aiCommand.placeholder": "Enter question for AI command", "input.codeConvertTargetLanguage.prompt": "Select convert target language", "input.selectAiSuggestionsVariableName.prompt": "Select AI suggestions variable name", + "input.batchProcessor.prompt": "Let AI batch process your selected {0} files, what do you want AI to do?", + "input.batchProcessor.placeholder": "eg: help me migrate from python2 to python3", "file.content": "File: {0}\n```{1}\n{2}\n```\n\n" } diff --git a/package.nls.json b/package.nls.json index ee6d0c5..0272506 100644 --- a/package.nls.json +++ b/package.nls.json @@ -5,6 +5,7 @@ "command.codeViewerHelper": "✨ Aide: Code Viewer Helper", "command.renameVariable": "✨ Aide: Rename Variable", "command.smartPaste": "✨ Aide: Smart Paste", + "command.batchProcessor": "✨ Aide: AI Batch Processor", "command.copyFileText": "Copy text", "command.quickCloseFileWithoutSave": "Quick close", "command.replaceFile": "Replace original", @@ -13,6 +14,7 @@ "config.openaiKey.description": "OpenAI Key, [click to view online documentation](https://aide.nicepkg.cn/guide/configuration/openai-key)", "config.openaiModel.description": "OpenAI Model, [click to view online documentation](https://aide.nicepkg.cn/guide/configuration/openai-model)", "config.openaiBaseUrl.description": "OpenAI Base URL, [click to view online documentation](https://aide.nicepkg.cn/guide/configuration/openai-base-url)", + "config.apiConcurrency.description": "API request concurrency, [click to view online documentation](https://aide.nicepkg.cn/guide/configuration/api-concurrency)", "config.useSystemProxy.description": "Use global proxy (`HTTP_PROXY`, `HTTPS_PROXY`, `ALL_PROXY`), you need to restart `VSCode` to take effect after changing this setting, [click to view online documentation](https://aide.nicepkg.cn/guide/configuration/use-system-proxy)", "config.codeViewerHelperPrompt.description": "Code viewer helper prompt template, [click to view online documentation](https://aide.nicepkg.cn/guide/configuration/code-viewer-helper-prompt)", "config.convertLanguagePairs.description": "Default convert language pairs, [click to view online documentation](https://aide.nicepkg.cn/guide/configuration/convert-language-pairs)", @@ -36,14 +38,14 @@ "error.configKeyRequired": "{0} configuration key is required", "error.vscodeLLMModelNotFound": "VSCode LLM model not found, please check configuration", "error.noSelection": "No file or folder selected", - "error.noActiveEditor": "No file is currently open", + "error.noActiveEditor": "Please open any file first to determine workspace", "error.noTargetLanguage": "No target language selected", "error.noContext": "Context not initialized", "error.emptyClipboard": "Clipboard is empty", "error.xclipNotFound": "xclip is not installed. Please install it using your package manager (e.g., sudo apt-get install xclip)", "error.fileNotFound": "File not found", + "error.invalidInput": "Invalid input", "info.copied": "File contents have been copied to clipboard", - "info.customLanguage": "Custom language", "info.noAiSuggestionsVariableName": "AI thinks your variable name is already good", "info.processing": "Aide is processing...", "info.continueMessage": "Continue? I'm not sure if it's done yet, if there's still content not generated, you can click continue.", @@ -52,11 +54,14 @@ "info.cancel": "Cancel", "info.commandCopiedToClipboard": "AI command has been copied to clipboard", "info.fileReplaceSuccess": "File content has been replaced successfully", + "info.batchProcessorSuccess": "AI batch processor success!\n\nTotal {0} files generated, you can review and replace manually.\n\nTasks completed:\n{1}", "input.array.promptEnding": "Enter comma separated values", "input.json.promptEnding": "Enter JSON formatted value", "input.aiCommand.prompt": "Enter question for AI command", "input.aiCommand.placeholder": "Enter question for AI command", "input.codeConvertTargetLanguage.prompt": "Select convert target language", "input.selectAiSuggestionsVariableName.prompt": "Select AI suggestions variable name", + "input.batchProcessor.prompt": "Let AI batch process your selected {0} files, what do you want AI to do?", + "input.batchProcessor.placeholder": "eg: help me migrate from python2 to python3", "file.content": "File: {0}\n```{1}\n{2}\n```\n\n" } diff --git a/package.nls.zh-cn.json b/package.nls.zh-cn.json index 44e02cc..1a69168 100644 --- a/package.nls.zh-cn.json +++ b/package.nls.zh-cn.json @@ -5,6 +5,7 @@ "command.codeViewerHelper": "✨ Aide: 代码查看器助手", "command.renameVariable": "✨ Aide: 重命名变量", "command.smartPaste": "✨ Aide: 智能粘贴", + "command.batchProcessor": "✨ Aide: AI 批量处理", "command.copyFileText": "复制全文", "command.quickCloseFileWithoutSave": "快速关闭", "command.replaceFile": "替换原文", @@ -13,6 +14,7 @@ "config.openaiKey.description": "OpenAI Key, [点击查看在线文档](https://aide.nicepkg.cn/zh/guide/configuration/openai-key)", "config.openaiModel.description": "OpenAI Model, [点击查看在线文档](https://aide.nicepkg.cn/zh/guide/configuration/openai-model)", "config.openaiBaseUrl.description": "OpenAI Base URL, [点击查看在线文档](https://aide.nicepkg.cn/zh/guide/configuration/openai-base-url)", + "config.apiConcurrency.description": "API 请求并发数, [点击查看在线文档](https://aide.nicepkg.cn/zh/guide/configuration/api-concurrency)", "config.useSystemProxy.description": "是否使用全局代理 (`HTTP_PROXY`、`HTTPS_PROXY`、`ALL_PROXY`) , 更改此设置后需要重启 `VSCode` 才生效, [点击查看在线文档](https://aide.nicepkg.cn/zh/guide/configuration/use-system-proxy)", "config.codeViewerHelperPrompt.description": "代码查看器助手 prompt 模板, [点击查看在线文档](https://aide.nicepkg.cn/zh/guide/configuration/code-viewer-helper-prompt)", "config.convertLanguagePairs.description": "默认转换语言对照表, [点击查看在线文档](https://aide.nicepkg.cn/zh/guide/configuration/convert-language-pairs)", @@ -36,14 +38,14 @@ "error.configKeyRequired": "{0} 配置键是必需的", "error.vscodeLLMModelNotFound": "未找到 VSCode LLM 模型,请检查配置", "error.noSelection": "未选择任何文件或文件夹", - "error.noActiveEditor": "未打开任何文件", + "error.noActiveEditor": "请先打开任意一个文件以确定 workspace", "error.noTargetLanguage": "未选择目标语言", "error.noContext": "上下文未初始化", "error.emptyClipboard": "剪贴板为空", "error.xclipNotFound": "xclip 未安装。请使用你的包管理器安装它 (例如,sudo apt-get install xclip)", "error.fileNotFound": "文件未找到", + "error.invalidInput": "无效的输入", "info.copied": "文件内容已复制到剪贴板", - "info.customLanguage": "自定义语言", "info.noAiSuggestionsVariableName": " AI 觉得你这个变量名字已经很好了", "info.processing": "Aide 正在处理中...", "info.continueMessage": "继续吗?我不确定是否已经完成了,如果还有内容没生成,你可以点击继续。", @@ -52,11 +54,14 @@ "info.cancel": "取消", "info.commandCopiedToClipboard": "AI 命令已复制到剪贴板", "info.fileReplaceSuccess": "文件内容已成功替换", + "info.batchProcessorSuccess": "AI 批量处理成功!\n\n共生成了 {0} 个文件, 你可以自己 review 手动替换。\n\n已完成任务:\n{1}", "input.array.promptEnding": "输入逗号分隔的值", "input.json.promptEnding": "输入 JSON 格式的值", "input.aiCommand.prompt": "输入 AI 命令的问题", "input.aiCommand.placeholder": "输入 AI 命令的问题", "input.codeConvertTargetLanguage.prompt": "选择转换目标语言", "input.selectAiSuggestionsVariableName.prompt": "选择 AI 建议的变量名", + "input.batchProcessor.prompt": "让 AI 批量处理你选中的 {0} 个文件,你想 AI 做什么?", + "input.batchProcessor.placeholder": "比如:帮我从 python2 迁移到 python3", "file.content": "File: {0}\n```{1}\n{2}\n```\n\n" } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 391057c..f17d00b 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -134,6 +134,9 @@ importers: minimatch: specifier: ^9.0.5 version: 9.0.5 + p-limit: + specifier: ^6.1.0 + version: 6.1.0 pnpm: specifier: ^9.6.0 version: 9.6.0 @@ -3859,6 +3862,10 @@ packages: resolution: {integrity: sha512-5b0R4txpzjPWVw/cXXUResoD4hb6U/x9BH08L7nw+GN1sezDzPdxeRvpc9c433fZhBan/wusjbCsqwqm4EIBIQ==} engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + p-limit@6.1.0: + resolution: {integrity: sha512-H0jc0q1vOzlEk0TqAKXKZxdl7kX3OFUzCnNVUnq5Pc3DGo0kpeaMuPqxQn235HibwBEb0/pm9dgKTjXy66fBkg==} + engines: {node: '>=18'} + p-locate@5.0.0: resolution: {integrity: sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==} engines: {node: '>=10'} @@ -4886,6 +4893,10 @@ packages: resolution: {integrity: sha512-9bnSc/HEW2uRy67wc+T8UwauLuPJVn28jb+GtJY16iiKWyvmYJRXVT4UamsAEGQfPohgr2q4Tq0sQbQlxTfi1g==} engines: {node: '>=12.20'} + yocto-queue@1.1.1: + resolution: {integrity: sha512-b4JR1PFR10y1mKjhHY9LaGo6tmrgjit7hxVIeAmyMw3jegXR4dhYqLaQF5zMXZxY7tLpMyJeLjr1C4rLmkVe8g==} + engines: {node: '>=12.20'} + yoctocolors-cjs@2.1.2: resolution: {integrity: sha512-cYVsTjKl8b+FrnidjibDWskAv7UKOfcwaVZdp/it9n1s9fU3IkgDbhdIRKCW4JDsAlECJY0ytoVPT3sK6kideA==} engines: {node: '>=18'} @@ -8833,6 +8844,10 @@ snapshots: dependencies: yocto-queue: 1.0.0 + p-limit@6.1.0: + dependencies: + yocto-queue: 1.1.1 + p-locate@5.0.0: dependencies: p-limit: 3.1.0 @@ -9915,6 +9930,8 @@ snapshots: yocto-queue@1.0.0: {} + yocto-queue@1.1.1: {} + yoctocolors-cjs@2.1.2: {} yoctocolors@2.1.0: {} diff --git a/src/ai/get-reference-file-paths.ts b/src/ai/get-reference-file-paths.ts index 2c654b8..6f88883 100644 --- a/src/ai/get-reference-file-paths.ts +++ b/src/ai/get-reference-file-paths.ts @@ -34,10 +34,10 @@ export const getReferenceFilePaths = async ({ useHistory: false, zodSchema: z.object({ referenceFileRelativePaths: z.array(z.string()).min(0).max(3).describe(` - The relative paths of the up to three most useful files related to the currently edited file. This can include 0 to 3 files. + Required! The relative paths of the up to three most useful files related to the currently edited file. This can include 0 to 3 files. `), dependenceFileRelativePath: z.string().describe(` - The relative path of the dependency file for the current file. If the dependency file is not found, return an empty string. + Required! The relative path of the dependency file for the current file. If the dependency file is not found, return an empty string. `) }) }) diff --git a/src/ai/model-providers/claude.ts b/src/ai/model-providers/claude.ts index dfe938b..0c218e2 100644 --- a/src/ai/model-providers/claude.ts +++ b/src/ai/model-providers/claude.ts @@ -23,7 +23,7 @@ export class AnthropicModelProvider extends BaseModelProvider { }, model: openaiModel, temperature: 0.95, // never use 1.0, some models do not support it - maxRetries: 3, + maxRetries: 6, verbose: isDev }) diff --git a/src/auto-open-corresponding-files.ts b/src/auto-open-corresponding-files.ts new file mode 100644 index 0000000..1462dc3 --- /dev/null +++ b/src/auto-open-corresponding-files.ts @@ -0,0 +1,57 @@ +import * as vscode from 'vscode' + +import { getOriginalFileUri, isTmpFileUri } from './file-utils/create-tmp-file' +import { VsCodeFS } from './file-utils/vscode-fs' +import { logger } from './logger' + +let isHandlingEditorChange = false + +const openCorrespondingFiles = async (tmpUri: vscode.Uri): Promise => { + if (isHandlingEditorChange || !isTmpFileUri(tmpUri)) return + isHandlingEditorChange = true + const originalUri = getOriginalFileUri(tmpUri) + + try { + // check if the original file exists + await VsCodeFS.stat(originalUri.fsPath) + + // open original file + const originalDocument = + await vscode.workspace.openTextDocument(originalUri) + await vscode.window.showTextDocument( + originalDocument, + vscode.ViewColumn.One + ) + + // 重新聚焦到 .aide.vue 文件 + // refocus on the .aide file + const tmpDocument = await vscode.workspace.openTextDocument(tmpUri) + await vscode.window.showTextDocument(tmpDocument, vscode.ViewColumn.Two) + } catch (e) { + logger.warn('openCorrespondingFiles error', e) + } finally { + isHandlingEditorChange = false + } +} + +export const autoOpenCorrespondingFiles = ( + context: vscode.ExtensionContext +) => { + context.subscriptions.push( + vscode.workspace.onDidOpenTextDocument(async document => { + const maybeTmpUri = document.uri + if (isTmpFileUri(maybeTmpUri)) { + await openCorrespondingFiles(maybeTmpUri) + } + }) + ) + + context.subscriptions.push( + vscode.window.onDidChangeActiveTextEditor(editor => { + const maybeTmpUri = editor?.document.uri + if (maybeTmpUri && isTmpFileUri(maybeTmpUri) && !isHandlingEditorChange) { + openCorrespondingFiles(maybeTmpUri) + } + }) + ) +} diff --git a/src/commands/ask-ai.ts b/src/commands/ask-ai/index.ts similarity index 100% rename from src/commands/ask-ai.ts rename to src/commands/ask-ai/index.ts diff --git a/src/commands/batch-processor/get-pre-process-info.ts b/src/commands/batch-processor/get-pre-process-info.ts new file mode 100644 index 0000000..9ebc54c --- /dev/null +++ b/src/commands/batch-processor/get-pre-process-info.ts @@ -0,0 +1,157 @@ +import path from 'path' +import { createModelProvider } from '@/ai/helpers' +import { traverseFileOrFolders } from '@/file-utils/traverse-fs' +import { getCurrentWorkspaceFolderEditor } from '@/utils' +import { z } from 'zod' + +export interface PreProcessInfo { + processFilePathInfo: { + sourceFileRelativePath: string + processedFileRelativePath: string + referenceFileRelativePaths: string[] + }[] + dependenceFileRelativePath?: string + ignoreFileRelativePaths?: string[] +} + +export const getPreProcessInfo = async ({ + prompt, + fileRelativePathsForProcess +}: { + prompt: string + fileRelativePathsForProcess: string[] +}): Promise< + PreProcessInfo & { + allFileRelativePaths: string[] + } +> => { + const { workspaceFolder } = await getCurrentWorkspaceFolderEditor() + const allFileRelativePaths: string[] = [] + + await traverseFileOrFolders( + [workspaceFolder.uri.fsPath], + workspaceFolder.uri.fsPath, + fileInfo => { + allFileRelativePaths.push(fileInfo.relativePath) + } + ) + + const modelProvider = await createModelProvider() + const aiRunnable = await modelProvider.createStructuredOutputRunnable({ + useHistory: false, + zodSchema: z.object({ + processFilePathInfo: z + .array( + z.object({ + sourceFileRelativePath: z + .string() + .describe( + `Required! The relative path of the source file to be processed.` + ), + processedFileRelativePath: z + .string() + .describe( + `Required! The relative path of the processed file. It should be identical to the source path, except for a possible change in file extension.` + ), + referenceFileRelativePaths: z + .array(z.string()) + .min(0) + .max(3) + .optional() + .describe( + `Required! Up to three relative paths of files that are useful for processing the source file.` + ) + }) + ) + .min(0) + .max(fileRelativePathsForProcess.length) + .describe(`Required!`), + dependenceFileRelativePath: z.string().optional().describe(` + The relative path of the dependency file for the current project. + **Ensure that very large files, such as yarn.lock, are not included in the results.** + `), + ignoreFileRelativePaths: z + .array(z.string()) + .optional() + .describe( + `The relative paths of the files that should be ignored during the processing.` + ) + }) + }) + + const aiRes: PreProcessInfo = await aiRunnable.invoke({ + input: ` +I need your help to analyze a list of files for a code conversion project. I'll provide you with all file paths in the project and a list of selected files for processing. Please perform the following tasks: + +1. Identify the main dependency file for the project (e.g., package.json, requirements.txt, pom.xml). Ensure that very large files, such as yarn.lock, are not included in the results. + +2. For each selected file, determine: + a) The source file path + b) The processed file path. This should be identical to the source path, except for a possible change in file extension. + For example, in a Vue to React conversion, 'src/CheckBox.vue' would become 'src/CheckBox.tsx', but not 'src/check-box.tsx'. + Another example, add comment for Vue files, 'src/CheckBox.vue' would become 'src/CheckBox.vue'. + c) Up to 3 related files that might be useful during the conversion process + +3. Identify any files from the selected list that should be ignored during the conversion. These might include: + - Static asset files (e.g., .css, .scss, .less, .svg, .png, .jpg) + - Configuration files that don't need conversion (e.g., .eslintrc, .prettierrc) + - Files that are not typically converted in the given scenario (e.g., test files if not part of the conversion scope) + +Here's some important context: +- We're working on a code conversion project, likely involving framework or language migration. +- The goal is to prepare a list of files for processing, identify related files, and exclude files that don't need conversion. +- Use your judgment to determine which files should be processed, referenced, or ignored based on common development practices. + +All file paths in the project: +${allFileRelativePaths.join('\n')} + +Selected files for processing: +${fileRelativePathsForProcess.join('\n')} + +Requirement: +${prompt} + +Please analyze these files and provide the requested information to help streamline the conversion process. + ` + }) + + // data cleaning + // Process and filter the file path information + const finalProcessFilePathInfo: PreProcessInfo['processFilePathInfo'] = + aiRes.processFilePathInfo + .map(info => { + // Extract the base name and extension from the source file path + const sourceBaseName = path.basename( + info.sourceFileRelativePath, + path.extname(info.sourceFileRelativePath) + ) + // Get the extension from the processed file path + const processedExtName = path.extname(info.processedFileRelativePath) + // Construct the full processed file path + const fullProcessedPath = path.join( + path.dirname(info.sourceFileRelativePath), + sourceBaseName + processedExtName + ) + + // Check if the processed file path should be ignored + const shouldIgnore = + fullProcessedPath === info.sourceFileRelativePath && + aiRes.ignoreFileRelativePaths?.includes(info.sourceFileRelativePath) + + // Return the new info object or null if it should be ignored + return shouldIgnore + ? null + : { ...info, processedFileRelativePath: fullProcessedPath } + }) + // Filter out any null entries + .filter( + (info): info is PreProcessInfo['processFilePathInfo'][0] => + info !== null + ) + + return { + ...aiRes, + processFilePathInfo: finalProcessFilePathInfo, + allFileRelativePaths + } +} diff --git a/src/commands/batch-processor/index.ts b/src/commands/batch-processor/index.ts new file mode 100644 index 0000000..c32c05a --- /dev/null +++ b/src/commands/batch-processor/index.ts @@ -0,0 +1,92 @@ +import { getConfigKey } from '@/config' +import { isTmpFileUri } from '@/file-utils/create-tmp-file' +import { traverseFileOrFolders } from '@/file-utils/traverse-fs' +import { t } from '@/i18n' +import { createLoading } from '@/loading' +import { logger } from '@/logger' +import { stateStorage } from '@/storage' +import pLimit from 'p-limit' +import * as vscode from 'vscode' + +import { getPreProcessInfo } from './get-pre-process-info' +import { writeAndSaveTmpFile } from './write-and-save-tmp-file' + +export const handleBatchProcessor = async ( + uri: vscode.Uri, + selectedUris: vscode.Uri[] = [] +) => { + const workspaceFolder = vscode.workspace.getWorkspaceFolder(uri) + if (!workspaceFolder) throw new Error(t('error.noWorkspace')) + + const selectedItems = selectedUris?.length > 0 ? selectedUris : [uri] + if (selectedItems.length === 0) throw new Error(t('error.noSelection')) + + const selectedFileOrFolders = selectedItems.map(item => item.fsPath) + const filesInfo = await traverseFileOrFolders( + selectedFileOrFolders, + workspaceFolder.uri.fsPath, + fileInfo => fileInfo + ) + const fileRelativePathsForProcess = filesInfo + .filter(fileInfo => !isTmpFileUri(vscode.Uri.file(fileInfo.fullPath))) + .map(fileInfo => fileInfo.relativePath) + + // show input box + const prompt = await vscode.window.showInputBox({ + prompt: t( + 'input.batchProcessor.prompt', + fileRelativePathsForProcess.length + ), + placeHolder: t('input.batchProcessor.placeholder'), + value: stateStorage.getItem('batchProcessorLastPrompt') || '' + }) + + if (!prompt) return + stateStorage.setItem('batchProcessorLastPrompt', prompt) + + const abortController = new AbortController() + const { showProcessLoading, hideProcessLoading } = createLoading() + + try { + showProcessLoading({ + onCancel() { + abortController.abort() + } + }) + + const preProcessInfo = await getPreProcessInfo({ + prompt, + fileRelativePathsForProcess + }) + + logger.log('handleBatchProcessor', preProcessInfo) + + const apiConcurrency = (await getConfigKey('apiConcurrency')) || 1 + const limit = pLimit(apiConcurrency) + const promises = preProcessInfo.processFilePathInfo.map(info => + limit(() => + writeAndSaveTmpFile({ + prompt, + workspacePath: workspaceFolder.uri.fsPath, + allFileRelativePaths: preProcessInfo.allFileRelativePaths, + sourceFileRelativePath: info.sourceFileRelativePath, + processedFileRelativePath: info.processedFileRelativePath, + dependenceFileRelativePath: preProcessInfo.dependenceFileRelativePath, + abortController + }) + ) + ) + + await Promise.allSettled(promises) + hideProcessLoading() + await vscode.window.showInformationMessage( + t( + 'info.batchProcessorSuccess', + preProcessInfo.processFilePathInfo.length, + prompt + ) + ) + } finally { + hideProcessLoading() + } +} diff --git a/src/commands/batch-processor/write-and-save-tmp-file.ts b/src/commands/batch-processor/write-and-save-tmp-file.ts new file mode 100644 index 0000000..4a503e3 --- /dev/null +++ b/src/commands/batch-processor/write-and-save-tmp-file.ts @@ -0,0 +1,135 @@ +import path from 'path' +import { createModelProvider } from '@/ai/helpers' +import { getTmpFileUri } from '@/file-utils/create-tmp-file' +import { tmpFileWriter } from '@/file-utils/tmp-file-writer' +import { VsCodeFS } from '@/file-utils/vscode-fs' +import { logger } from '@/logger' +import { getLanguageId } from '@/utils' +import { HumanMessage } from '@langchain/core/messages' +import * as vscode from 'vscode' + +export const writeAndSaveTmpFile = async ({ + prompt, + workspacePath, + allFileRelativePaths, + sourceFileRelativePath, + processedFileRelativePath, + dependenceFileRelativePath, + abortController +}: { + prompt: string + workspacePath: string + allFileRelativePaths: string[] + sourceFileRelativePath: string + processedFileRelativePath: string + dependenceFileRelativePath: string | undefined + abortController?: AbortController +}) => { + logger.log(`writeAndSaveTmpFile...: ${sourceFileRelativePath}`) + // ai + const modelProvider = await createModelProvider() + const aiModel = (await modelProvider.getModel()).bind({ + signal: abortController?.signal + }) + + const getContentFromRelativePath = async (relativePath: string) => { + if (!relativePath) return '' + const fileFullPath = path.join(workspacePath, relativePath) + const content = await VsCodeFS.readFile(fileFullPath, 'utf8') + return content + } + + const getProcessedFileUri = async () => { + const processedFileExt = path.extname(processedFileRelativePath).slice(1) + const processedLanguageId = getLanguageId(processedFileExt) + const sourceFileFullPath = path.join(workspacePath, sourceFileRelativePath) + const aideFileUri = getTmpFileUri({ + originalFileFullPath: sourceFileFullPath, + languageId: processedLanguageId, + untitled: false + }) + + await VsCodeFS.writeFile(aideFileUri.fsPath, '', 'utf8') + + return aideFileUri + } + + const locale = vscode.env.language + const processedFileUri = await getProcessedFileUri() + const sourceFileContent = await getContentFromRelativePath( + sourceFileRelativePath + ) + const dependenceFileContent = await getContentFromRelativePath( + dependenceFileRelativePath || '' + ) + + await tmpFileWriter({ + stopWriteWhenClosed: true, + enableProcessLoading: false, + autoSaveWhenDone: true, + autoCloseWhenDone: true, + tmpFileUri: processedFileUri, + onCancel() { + abortController?.abort() + }, + buildAiStream: async () => { + const aiStream = aiModel.stream([ + new HumanMessage({ + content: ` +You are an expert programmer and code analyzer. Your task is to process the given source file according to the user's specific requirements. This may include, but is not limited to, converting code to a new format or framework, adding detailed comments, optimizing code, refactoring, or any other code-related task. Please follow these instructions: + +1. Context: + - All file paths in the project: ${allFileRelativePaths.join(', ')} + - Source file path: ${sourceFileRelativePath} + - Processed file path: ${processedFileRelativePath} + - Dependency file path: ${dependenceFileRelativePath || 'Not provided'} + +2. Source content: +\`\`\` +${sourceFileContent} +\`\`\` + +3. Dependency file content (if available): +\`\`\` +${dependenceFileContent} +\`\`\` + +4. IMPORTANT! User's requirements: +${prompt} + +5. Processing task: + - Carefully analyze the source file content and the user's requirements. + - Perform the requested task, which could be any of the following or a combination: + * Converting the code to a different format or framework + * Adding detailed code comments + * Optimizing the code for better performance or readability + * Refactoring the code + * Implementing new features or modifying existing ones + * Any other code-related task specified by the user + - User's native language is ${locale}, please consider this when adding comments or documentation. + - Ensure that your changes align with the user's requirements and the overall project structure. + - Unless explicitly requested otherwise, maintain the original code's structure and naming conventions as much as possible. + +6. Output: + - Provide the processed code that should be written to the processed file. + - The output should contain only the processed code, including any new comments if that was part of the task. + - Do not include any explanations or meta-comments outside of the code itself. + +7. Important notes: + - Pay close attention to the user's specific requirements and tailor your response accordingly. + - If the task involves significant changes, ensure that the core functionality of the code is preserved unless explicitly instructed otherwise. + - Be mindful of the project's overall structure and any dependencies when making changes. + - If certain parts of the task are unclear or seem contradictory, interpret them in a way that seems most beneficial to the project and consistent with good coding practices. + - If the task involves adding comments or documentation, ensure they are clear, concise, and genuinely helpful for understanding the code. + +Please proceed with the requested task and output the resulting code. +Please do not reply with any text other than the code, and do not use markdown syntax. + ` + }) + ]) + return aiStream + } + }) + + logger.log(`writeAndSaveTmpFile done: ${sourceFileRelativePath}`) +} diff --git a/src/commands/code-convert.ts b/src/commands/code-convert.ts deleted file mode 100644 index f654347..0000000 --- a/src/commands/code-convert.ts +++ /dev/null @@ -1,204 +0,0 @@ -import { - createModelProvider, - getCurrentSessionIdHistoriesMap -} from '@/ai/helpers' -import { getConfigKey, setConfigKey } from '@/config' -import { languageIdExts, languageIds } from '@/constants' -import { createTmpFileInfo } from '@/file-utils/create-tmp-file' -import { showContinueMessage } from '@/file-utils/show-continue-message' -import { tmpFileWriter } from '@/file-utils/tmp-file-writer' -import { t } from '@/i18n' -import { getLanguageId } from '@/utils' -import type { BaseLanguageModelInput } from '@langchain/core/language_models/base' -import type { RunnableConfig } from '@langchain/core/runnables' -import * as vscode from 'vscode' - -const buildGeneratePrompt = async ({ - sourceLanguageId, - targetLanguageId, - targetLanguageDescription, - sourceCode -}: { - sourceLanguageId: string - targetLanguageId: string - targetLanguageDescription: string - sourceCode: string -}): Promise => { - const locale = vscode.env.language - - const targetLanguageDescriptionPrompt = targetLanguageDescription - ? ` - For the converted language, my additional notes are as follows: **${targetLanguageDescription}.** - ` - : '' - - const prompt = ` - You are a programming language converter. - You need to help me convert ${sourceLanguageId} code into ${targetLanguageId} code. - ${targetLanguageDescriptionPrompt} - All third-party API and third-party dependency names do not need to be changed, - as my purpose is only to understand and read, not to run. Please use ${locale} language to add some additional comments as appropriate. - Please do not reply with any text other than the code, and do not use markdown syntax. - Here is the code you need to convert: - - ${sourceCode} -` - return prompt -} - -/** - * Get target language info - * if user input custom language like: vue please convert to vue3 - * return { targetLanguageId: 'vue', targetLanguageDescription: 'please convert to vue3' } - */ -const getTargetLanguageInfo = async (originalFileLanguageId: string) => { - const convertLanguagePairs = await getConfigKey('convertLanguagePairs', { - targetForSet: vscode.ConfigurationTarget.WorkspaceFolder, - allowCustomOptionValue: true - }) - let targetLanguageInfo = convertLanguagePairs?.[originalFileLanguageId] || '' - const customLanguageOption = t('info.customLanguage') - - if (!targetLanguageInfo) { - targetLanguageInfo = - (await vscode.window.showQuickPick( - [customLanguageOption, ...languageIds, ...languageIdExts], - { - placeHolder: t('input.codeConvertTargetLanguage.prompt'), - canPickMany: false - } - )) || '' - - if (!targetLanguageInfo) throw new Error(t('error.noTargetLanguage')) - - if (targetLanguageInfo === customLanguageOption) { - targetLanguageInfo = - (await vscode.window.showInputBox({ - prompt: t('info.customLanguage') - })) || '' - } - - if (!targetLanguageInfo) throw new Error(t('error.noTargetLanguage')) - - const autoRememberConvertLanguagePairs = await getConfigKey( - 'autoRememberConvertLanguagePairs' - ) - - if (autoRememberConvertLanguagePairs) { - await setConfigKey( - 'convertLanguagePairs', - { - ...convertLanguagePairs, - [originalFileLanguageId]: targetLanguageInfo - }, - { - targetForSet: vscode.ConfigurationTarget.WorkspaceFolder, - allowCustomOptionValue: true - } - ) - } - } - - const [targetLanguageIdOrExt, ...targetLanguageRest] = - targetLanguageInfo.split(/\s+/) - const targetLanguageDescription = targetLanguageRest.join(' ') - const targetLanguageId = getLanguageId(targetLanguageIdOrExt || 'plaintext') - - return { - targetLanguageId: targetLanguageId || targetLanguageInfo, - targetLanguageDescription: targetLanguageDescription?.trim() || '' - } -} - -export const cleanupCodeConvertRunnables = async () => { - const openDocumentPaths = new Set( - vscode.workspace.textDocuments.map(doc => doc.uri.fsPath) - ) - - const sessionIdHistoriesMap = await getCurrentSessionIdHistoriesMap() - - Object.keys(sessionIdHistoriesMap).forEach(sessionId => { - const path = sessionId.match(/^codeConvert:(.*)$/)?.[1] - - if (path && !openDocumentPaths.has(path)) { - delete sessionIdHistoriesMap[sessionId] - } - }) -} - -export const handleCodeConvert = async () => { - const { - originalFileContent, - originalFileLanguageId, - tmpFileUri, - isTmpFileHasContent - } = await createTmpFileInfo() - - const { targetLanguageId, targetLanguageDescription } = - await getTargetLanguageInfo(originalFileLanguageId) - - // ai - const modelProvider = await createModelProvider() - const aiRunnableAbortController = new AbortController() - const aiRunnable = await modelProvider.createRunnable({ - signal: aiRunnableAbortController.signal - }) - const sessionId = `codeConvert:${tmpFileUri.fsPath}}` - const aiRunnableConfig: RunnableConfig = { - configurable: { - sessionId - } - } - - const sessionIdHistoriesMap = await getCurrentSessionIdHistoriesMap() - const isSessionHistoryExists = !!sessionIdHistoriesMap[sessionId] - const isContinue = isTmpFileHasContent && isSessionHistoryExists - - const prompt = await buildGeneratePrompt({ - sourceLanguageId: originalFileLanguageId, - targetLanguageId, - targetLanguageDescription, - sourceCode: originalFileContent - }) - - const tmpFileWriterReturns = await tmpFileWriter({ - languageId: targetLanguageId, - onCancel() { - aiRunnableAbortController.abort() - }, - buildAiStream: async () => { - if (!isContinue) { - // cleanup previous session - delete sessionIdHistoriesMap[sessionId] - - const aiStream = aiRunnable.stream( - { - input: prompt - }, - aiRunnableConfig - ) - return aiStream - } - - // continue - return aiRunnable.stream( - { - input: ` - continue, please do not reply with any text other than the code, and do not use markdown syntax. - go continue. - ` - }, - aiRunnableConfig - ) - } - }) - - await showContinueMessage({ - tmpFileUri: tmpFileWriterReturns.tmpFileUri, - originalFileContentLineCount: originalFileContent.split('\n').length, - continueMessage: t('info.continueMessage') + t('info.iconContinueMessage'), - onContinue: async () => { - await handleCodeConvert() - } - }) -} diff --git a/src/commands/code-convert/build-convert-prompt.ts b/src/commands/code-convert/build-convert-prompt.ts new file mode 100644 index 0000000..6f6afba --- /dev/null +++ b/src/commands/code-convert/build-convert-prompt.ts @@ -0,0 +1,35 @@ +import type { BaseLanguageModelInput } from '@langchain/core/language_models/base' +import * as vscode from 'vscode' + +export const buildConvertPrompt = async ({ + sourceLanguageId, + targetLanguageId, + targetLanguageDescription, + sourceCode +}: { + sourceLanguageId: string + targetLanguageId: string + targetLanguageDescription: string + sourceCode: string +}): Promise => { + const locale = vscode.env.language + + const targetLanguageDescriptionPrompt = targetLanguageDescription + ? ` + For the converted language, my additional notes are as follows: **${targetLanguageDescription}.** + ` + : '' + + const prompt = ` + You are a programming language converter. + You need to help me convert ${sourceLanguageId} code into ${targetLanguageId} code. + ${targetLanguageDescriptionPrompt} + All third-party API and third-party dependency names do not need to be changed, + as my purpose is only to understand and read, not to run. Please use ${locale} language to add some additional comments as appropriate. + Please do not reply with any text other than the code, and do not use markdown syntax. + Here is the code you need to convert: + + ${sourceCode} +` + return prompt +} diff --git a/src/commands/code-convert/get-target-language-info.ts b/src/commands/code-convert/get-target-language-info.ts new file mode 100644 index 0000000..fbd519a --- /dev/null +++ b/src/commands/code-convert/get-target-language-info.ts @@ -0,0 +1,55 @@ +import { getConfigKey, setConfigKey } from '@/config' +import { languageIdExts, languageIds } from '@/constants' +import { t } from '@/i18n' +import { getLanguageId, showQuickPickWithCustomInput } from '@/utils' +import * as vscode from 'vscode' + +/** + * Get target language info + * if user input custom language like: vue please convert to vue3 + * return { targetLanguageId: 'vue', targetLanguageDescription: 'please convert to vue3' } + */ +export const getTargetLanguageInfo = async (originalFileLanguageId: string) => { + const convertLanguagePairs = await getConfigKey('convertLanguagePairs', { + targetForSet: vscode.ConfigurationTarget.WorkspaceFolder, + allowCustomOptionValue: true + }) + let targetLanguageInfo = convertLanguagePairs?.[originalFileLanguageId] || '' + + if (!targetLanguageInfo) { + targetLanguageInfo = await showQuickPickWithCustomInput({ + items: [...languageIds, ...languageIdExts], + placeholder: t('input.codeConvertTargetLanguage.prompt') + }) + + if (!targetLanguageInfo) throw new Error(t('error.noTargetLanguage')) + + const autoRememberConvertLanguagePairs = await getConfigKey( + 'autoRememberConvertLanguagePairs' + ) + + if (autoRememberConvertLanguagePairs) { + await setConfigKey( + 'convertLanguagePairs', + { + ...convertLanguagePairs, + [originalFileLanguageId]: targetLanguageInfo + }, + { + targetForSet: vscode.ConfigurationTarget.WorkspaceFolder, + allowCustomOptionValue: true + } + ) + } + } + + const [targetLanguageIdOrExt, ...targetLanguageRest] = + targetLanguageInfo.split(/\s+/) + const targetLanguageDescription = targetLanguageRest.join(' ') + const targetLanguageId = getLanguageId(targetLanguageIdOrExt || 'plaintext') + + return { + targetLanguageId: targetLanguageId || targetLanguageInfo, + targetLanguageDescription: targetLanguageDescription?.trim() || '' + } +} diff --git a/src/commands/code-convert/index.ts b/src/commands/code-convert/index.ts new file mode 100644 index 0000000..587c0b4 --- /dev/null +++ b/src/commands/code-convert/index.ts @@ -0,0 +1,106 @@ +import { + createModelProvider, + getCurrentSessionIdHistoriesMap +} from '@/ai/helpers' +import { createTmpFileInfo } from '@/file-utils/create-tmp-file' +import { showContinueMessage } from '@/file-utils/show-continue-message' +import { tmpFileWriter } from '@/file-utils/tmp-file-writer' +import { t } from '@/i18n' +import type { RunnableConfig } from '@langchain/core/runnables' +import * as vscode from 'vscode' + +import { buildConvertPrompt } from './build-convert-prompt' +import { getTargetLanguageInfo } from './get-target-language-info' + +export const cleanupCodeConvertRunnables = async () => { + const openDocumentPaths = new Set( + vscode.workspace.textDocuments.map(doc => doc.uri.fsPath) + ) + + const sessionIdHistoriesMap = await getCurrentSessionIdHistoriesMap() + + Object.keys(sessionIdHistoriesMap).forEach(sessionId => { + const path = sessionId.match(/^codeConvert:(.*)$/)?.[1] + + if (path && !openDocumentPaths.has(path)) { + delete sessionIdHistoriesMap[sessionId] + } + }) +} + +export const handleCodeConvert = async () => { + const { + originalFileContent, + originalFileLanguageId, + tmpFileUri, + isTmpFileHasContent + } = await createTmpFileInfo() + + const { targetLanguageId, targetLanguageDescription } = + await getTargetLanguageInfo(originalFileLanguageId) + + // ai + const modelProvider = await createModelProvider() + const aiRunnableAbortController = new AbortController() + const aiRunnable = await modelProvider.createRunnable({ + signal: aiRunnableAbortController.signal + }) + const sessionId = `codeConvert:${tmpFileUri.fsPath}}` + const aiRunnableConfig: RunnableConfig = { + configurable: { + sessionId + } + } + + const sessionIdHistoriesMap = await getCurrentSessionIdHistoriesMap() + const isSessionHistoryExists = !!sessionIdHistoriesMap[sessionId] + const isContinue = isTmpFileHasContent && isSessionHistoryExists + + const prompt = await buildConvertPrompt({ + sourceLanguageId: originalFileLanguageId, + targetLanguageId, + targetLanguageDescription, + sourceCode: originalFileContent + }) + + const tmpFileWriterReturns = await tmpFileWriter({ + languageId: targetLanguageId, + onCancel() { + aiRunnableAbortController.abort() + }, + buildAiStream: async () => { + if (!isContinue) { + // cleanup previous session + delete sessionIdHistoriesMap[sessionId] + + const aiStream = aiRunnable.stream( + { + input: prompt + }, + aiRunnableConfig + ) + return aiStream + } + + // continue + return aiRunnable.stream( + { + input: ` + continue, please do not reply with any text other than the code, and do not use markdown syntax. + go continue. + ` + }, + aiRunnableConfig + ) + } + }) + + await showContinueMessage({ + tmpFileUri: tmpFileWriterReturns.tmpFileUri, + originalFileContentLineCount: originalFileContent.split('\n').length, + continueMessage: t('info.continueMessage') + t('info.iconContinueMessage'), + onContinue: async () => { + await handleCodeConvert() + } + }) +} diff --git a/src/commands/code-viewer-helper/build-generate-prompt.ts b/src/commands/code-viewer-helper/build-generate-prompt.ts new file mode 100644 index 0000000..0761e87 --- /dev/null +++ b/src/commands/code-viewer-helper/build-generate-prompt.ts @@ -0,0 +1,19 @@ +import { getConfigKey } from '@/config' +import type { BaseLanguageModelInput } from '@langchain/core/language_models/base' +import * as vscode from 'vscode' + +export const buildGeneratePrompt = async ({ + sourceLanguage, + code +}: { + sourceLanguage: string + code: string +}): Promise => { + const locale = vscode.env.language + const codeViewerHelperPrompt = await getConfigKey('codeViewerHelperPrompt') + const prompt = codeViewerHelperPrompt + .replace('#{sourceLanguage}', sourceLanguage) + .replace('#{locale}', locale) + .replace('#{content}', code) + return prompt +} diff --git a/src/commands/code-viewer-helper.ts b/src/commands/code-viewer-helper/index.ts similarity index 83% rename from src/commands/code-viewer-helper.ts rename to src/commands/code-viewer-helper/index.ts index 4d10674..be0111a 100644 --- a/src/commands/code-viewer-helper.ts +++ b/src/commands/code-viewer-helper/index.ts @@ -2,30 +2,14 @@ import { createModelProvider, getCurrentSessionIdHistoriesMap } from '@/ai/helpers' -import { getConfigKey } from '@/config' import { createTmpFileInfo } from '@/file-utils/create-tmp-file' import { showContinueMessage } from '@/file-utils/show-continue-message' import { tmpFileWriter } from '@/file-utils/tmp-file-writer' import { t } from '@/i18n' -import type { BaseLanguageModelInput } from '@langchain/core/language_models/base' import type { RunnableConfig } from '@langchain/core/runnables' import * as vscode from 'vscode' -const buildGeneratePrompt = async ({ - sourceLanguage, - code -}: { - sourceLanguage: string - code: string -}): Promise => { - const locale = vscode.env.language - const codeViewerHelperPrompt = await getConfigKey('codeViewerHelperPrompt') - const prompt = codeViewerHelperPrompt - .replace('#{sourceLanguage}', sourceLanguage) - .replace('#{locale}', locale) - .replace('#{content}', code) - return prompt -} +import { buildGeneratePrompt } from './build-generate-prompt' export const cleanupCodeViewerHelperRunnables = async () => { const openDocumentPaths = new Set( diff --git a/src/commands/copy-as-prompt.ts b/src/commands/copy-as-prompt/index.ts similarity index 100% rename from src/commands/copy-as-prompt.ts rename to src/commands/copy-as-prompt/index.ts diff --git a/src/commands/index.ts b/src/commands/index.ts index e8c59dd..a6be285 100644 --- a/src/commands/index.ts +++ b/src/commands/index.ts @@ -2,14 +2,15 @@ import { commandErrorCatcher } from '@/utils' import * as vscode from 'vscode' import { handleAskAI } from './ask-ai' +import { handleBatchProcessor } from './batch-processor' import { handleCodeConvert } from './code-convert' import { handleCodeViewerHelper } from './code-viewer-helper' import { handleCopyAsPrompt } from './copy-as-prompt' -import { handleCopyFileText } from './copy-file-text' -import { handleQuickCloseFileWithoutSave } from './quick-close-file-without-save' +import { handleCopyFileText } from './private/copy-file-text' +import { handleQuickCloseFileWithoutSave } from './private/quick-close-file-without-save' +import { handleReplaceFile } from './private/replace-file' +import { handleShowDiff } from './private/show-diff' import { handleRenameVariable } from './rename-variable' -import { handleReplaceFile } from './replace-file' -import { handleShowDiff } from './show-diff' import { handleSmartPaste } from './smart-paste' export const registerCommands = async (context: vscode.ExtensionContext) => { @@ -42,6 +43,11 @@ export const registerCommands = async (context: vscode.ExtensionContext) => { commandErrorCatcher(handleSmartPaste) ) + const batchProcessorDisposable = vscode.commands.registerCommand( + 'aide.batchProcessor', + commandErrorCatcher(handleBatchProcessor) + ) + // private command const copyFileTextDisposable = vscode.commands.registerCommand( 'aide.copyFileText', @@ -73,6 +79,7 @@ export const registerCommands = async (context: vscode.ExtensionContext) => { codeViewerHelperDisposable, renameVariableDisposable, smartPasteDisposable, + batchProcessorDisposable, copyFileTextDisposable, quickCloseFileWithoutSaveDisposable, replaceFileDisposable, diff --git a/src/commands/copy-file-text.ts b/src/commands/private/copy-file-text.ts similarity index 100% rename from src/commands/copy-file-text.ts rename to src/commands/private/copy-file-text.ts diff --git a/src/commands/quick-close-file-without-save.ts b/src/commands/private/quick-close-file-without-save.ts similarity index 100% rename from src/commands/quick-close-file-without-save.ts rename to src/commands/private/quick-close-file-without-save.ts diff --git a/src/commands/private/replace-file.ts b/src/commands/private/replace-file.ts new file mode 100644 index 0000000..e6258fe --- /dev/null +++ b/src/commands/private/replace-file.ts @@ -0,0 +1,73 @@ +import path from 'path' +import { VsCodeFS } from '@/file-utils/vscode-fs' +import { t } from '@/i18n' +import * as vscode from 'vscode' + +const replaceFileContent = async (uri: vscode.Uri, content: string) => { + const editor = vscode.window.visibleTextEditors.find( + e => e.document.uri.toString() === uri.toString() + ) + if (editor && !editor.selection.isEmpty) { + await editor.edit(editBuilder => + editBuilder.replace(editor.selection, content) + ) + } else { + await VsCodeFS.writeFile(uri.fsPath, content, 'utf8') + } +} + +const changeFileExtension = async ( + uri: vscode.Uri, + newExt: string +): Promise => { + const oldPath = uri.fsPath + const dir = path.dirname(oldPath) + const nameWithoutExt = path.basename(oldPath, path.extname(oldPath)) + const newPath = path.join(dir, `${nameWithoutExt}${newExt}`) + const newUri = vscode.Uri.file(newPath) + await vscode.workspace.fs.rename(uri, newUri) + return newUri +} + +/** + * Handles the replacement of a file with another file. + * + * @param fromFileUri - The file will not removed, but its content will be replaced. + * @param toFileUri - The file will be removed. + * @throws An error if either `fromFileUri` or `toFileUri` is not provided. + */ +export const handleReplaceFile = async ( + fromFileUri: vscode.Uri, + toFileUri: vscode.Uri +) => { + if (!fromFileUri || !toFileUri) throw new Error(t('error.fileNotFound')) + + const toFileContent = ( + await vscode.workspace.openTextDocument(toFileUri) + ).getText() + await replaceFileContent(fromFileUri, toFileContent) + + vscode.window.showInformationMessage(t('info.fileReplaceSuccess')) + + // close the toFileUri + await vscode.commands.executeCommand( + 'aide.quickCloseFileWithoutSave', + toFileUri + ) + + // get toFileExt and set it to fromFileUri + const toFileExt = path.extname(toFileUri.fsPath) + if (toFileExt) { + fromFileUri = await changeFileExtension(fromFileUri, toFileExt) + } + + try { + // delete the toFileUri + await VsCodeFS.unlink(toFileUri.fsPath) + + // if fromFileUri is not opened, open it + const fromFileDocument = + await vscode.workspace.openTextDocument(fromFileUri) + await vscode.window.showTextDocument(fromFileDocument) + } catch {} +} diff --git a/src/commands/show-diff.ts b/src/commands/private/show-diff.ts similarity index 92% rename from src/commands/show-diff.ts rename to src/commands/private/show-diff.ts index 8c2a134..cd55fab 100644 --- a/src/commands/show-diff.ts +++ b/src/commands/private/show-diff.ts @@ -27,7 +27,6 @@ export const handleShowDiff = async ( fromFileTitle = `${path.basename(fromFileUri.fsPath)} (Selection)` finalFromFileUri = vscode.Uri.parse(`untitled:${fromFileTitle}`) - // FIXME: 这里会在编辑器打开一个新的临时文件给用户看,这不是我期望的,希望这是偷偷打开但是不展示给用户 // Create an in-memory document with the selected content const selectedContent = fromFileEditor.document.getText( fromFileEditor.selection diff --git a/src/commands/rename-variable/build-rename-suggestion-prompt.ts b/src/commands/rename-variable/build-rename-suggestion-prompt.ts new file mode 100644 index 0000000..03feb4d --- /dev/null +++ b/src/commands/rename-variable/build-rename-suggestion-prompt.ts @@ -0,0 +1,47 @@ +import type { BaseLanguageModelInput } from '@langchain/core/language_models/base' +import * as vscode from 'vscode' + +export const buildRenameSuggestionPrompt = async ({ + contextCode, + variableName, + selection, + fileRelativePath +}: { + contextCode: string + variableName: string + selection: vscode.Selection + fileRelativePath: string +}): Promise => { + const lines = contextCode.split('\n') + + // get the line index where the selected variable is located + const activeLine = selection.start.line + + // calculate the range of 250 lines before and after + const start = Math.max(0, activeLine - 250) + const end = Math.min(lines.length, activeLine + 250) + + // get the content of 250 lines before and after + const contextLines = lines.slice(start, end) + + // add a comment line above the line where the variable is located + contextLines.splice( + activeLine - start, + 0, + `### Here is the variable you want to change: ${variableName} ###` + ) + + const codeContextForPrompt = contextLines.join('\n') + + const prompt = ` + Please refer to the following code snippet to change the variable name \`${variableName}\` to a more reasonable name. + Give a few suggestions for a more reasonable name. + **You should always follow the naming conventions of the current code snippet to generate new variable names.** + current file relative path: ${fileRelativePath} + Here is the code snippet: + + ${codeContextForPrompt} + ` + + return prompt +} diff --git a/src/commands/rename-variable.ts b/src/commands/rename-variable/index.ts similarity index 51% rename from src/commands/rename-variable.ts rename to src/commands/rename-variable/index.ts index 3cee4a6..2118efc 100644 --- a/src/commands/rename-variable.ts +++ b/src/commands/rename-variable/index.ts @@ -6,116 +6,30 @@ import { import { t } from '@/i18n' import { createLoading } from '@/loading' import { getCurrentWorkspaceFolderEditor } from '@/utils' -import type { BaseLanguageModelInput } from '@langchain/core/language_models/base' import type { RunnableConfig } from '@langchain/core/runnables' import * as vscode from 'vscode' import { z } from 'zod' -const buildRenameSuggestionPrompt = async ({ - contextCode, - variableName, - selection, - fileRelativePath -}: { - contextCode: string - variableName: string - selection: vscode.Selection - fileRelativePath: string -}): Promise => { - const lines = contextCode.split('\n') - - // get the line index where the selected variable is located - const activeLine = selection.start.line - - // calculate the range of 250 lines before and after - const start = Math.max(0, activeLine - 250) - const end = Math.min(lines.length, activeLine + 250) - - // get the content of 250 lines before and after - const contextLines = lines.slice(start, end) - - // add a comment line above the line where the variable is located - contextLines.splice( - activeLine - start, - 0, - `### Here is the variable you want to change: ${variableName} ###` - ) - - const codeContextForPrompt = contextLines.join('\n') - - const prompt = ` - Please refer to the following code snippet to change the variable name \`${variableName}\` to a more reasonable name. - Give a few suggestions for a more reasonable name. - **You should always follow the naming conventions of the current code snippet to generate new variable names.** - current file relative path: ${fileRelativePath} - Here is the code snippet: - - ${codeContextForPrompt} - ` - - return prompt -} +import { buildRenameSuggestionPrompt } from './build-rename-suggestion-prompt' +import { submitRenameVariable } from './submit-rename-variable' const renameSuggestionZodSchema = z.object({ suggestionVariableNameOptions: z .array( z.object({ - variableName: z.string().describe('variable name'), + variableName: z.string().describe('Required! variable name'), description: z .string() .describe( - `About this variable, describe its meaning in my mother tongue ${vscode.env.language}, within 15 words` + `Required! About this variable, describe its meaning in my mother tongue ${vscode.env.language}, within 15 words` ) }) ) - .describe('suggested variable names') + .describe('Required! suggested variable names list') }) type RenameSuggestionZodSchema = z.infer -const renameVariable = async ({ - newName, - selection -}: { - newName: string - selection: vscode.Selection -}) => { - const editor = vscode.window.activeTextEditor - - if (!editor) throw new Error(t('error.noActiveEditor')) - - const { document } = editor - const position = selection.start - - // find all references - const references = await vscode.commands.executeCommand( - 'vscode.executeReferenceProvider', - document.uri, - position - ) - - // create a workspace edit - const edit = new vscode.WorkspaceEdit() - - if (references && references.length > 0) { - // if references found, change all references - references.forEach(reference => { - edit.replace(reference.uri, reference.range, newName) - }) - } else { - // if no references found, only change the selected position - edit.replace( - document.uri, - new vscode.Range(selection.start, selection.end), - newName - ) - } - - // apply the workspace edit - await vscode.workspace.applyEdit(edit) - await document.save() -} - export const handleRenameVariable = async () => { const { workspaceFolder, activeEditor } = getCurrentWorkspaceFolderEditor() const { selection } = activeEditor @@ -178,7 +92,7 @@ export const handleRenameVariable = async () => { ) if (selectedVariableNameOption) { - await renameVariable({ + await submitRenameVariable({ newName: selectedVariableNameOption.label, selection }) diff --git a/src/commands/rename-variable/submit-rename-variable.ts b/src/commands/rename-variable/submit-rename-variable.ts new file mode 100644 index 0000000..e979ef4 --- /dev/null +++ b/src/commands/rename-variable/submit-rename-variable.ts @@ -0,0 +1,45 @@ +import { t } from '@/i18n' +import * as vscode from 'vscode' + +export const submitRenameVariable = async ({ + newName, + selection +}: { + newName: string + selection: vscode.Selection +}) => { + const editor = vscode.window.activeTextEditor + + if (!editor) throw new Error(t('error.noActiveEditor')) + + const { document } = editor + const position = selection.start + + // find all references + const references = await vscode.commands.executeCommand( + 'vscode.executeReferenceProvider', + document.uri, + position + ) + + // create a workspace edit + const edit = new vscode.WorkspaceEdit() + + if (references && references.length > 0) { + // if references found, change all references + references.forEach(reference => { + edit.replace(reference.uri, reference.range, newName) + }) + } else { + // if no references found, only change the selected position + edit.replace( + document.uri, + new vscode.Range(selection.start, selection.end), + newName + ) + } + + // apply the workspace edit + await vscode.workspace.applyEdit(edit) + await document.save() +} diff --git a/src/commands/replace-file.ts b/src/commands/replace-file.ts deleted file mode 100644 index 05a9706..0000000 --- a/src/commands/replace-file.ts +++ /dev/null @@ -1,51 +0,0 @@ -import { VsCodeFS } from '@/file-utils/vscode-fs' -import { t } from '@/i18n' -import * as vscode from 'vscode' - -/** - * Handles the replacement of a file with another file. - * - * @param fromFileUri - The file will not removed, but its content will be replaced. - * @param toFileUri - The file will be removed. - * @throws An error if either `fromFileUri` or `toFileUri` is not provided. - */ -export const handleReplaceFile = async ( - fromFileUri: vscode.Uri, - toFileUri: vscode.Uri -) => { - if (!fromFileUri || !toFileUri) throw new Error(t('error.fileNotFound')) - - const toFileDocument = await vscode.workspace.openTextDocument(toFileUri) - const toFileContent = toFileDocument.getText() - const fromFileEditor = vscode.window.visibleTextEditors.find( - editor => editor.document.uri.toString() === fromFileUri.toString() - ) - const isFromFileHasSelection = - fromFileEditor && - fromFileEditor.document.uri.toString() === fromFileUri.toString() && - !fromFileEditor.selection.isEmpty - - if (isFromFileHasSelection) { - // replace the content with the toFileContent - await fromFileEditor.edit(editBuilder => { - editBuilder.replace(fromFileEditor.selection, toFileContent) - }) - } else { - await VsCodeFS.writeFile(fromFileUri.fsPath, toFileContent, 'utf8') - } - - vscode.window.showInformationMessage(t('info.fileReplaceSuccess')) - - // close the toFileUri - await vscode.commands.executeCommand( - 'aide.quickCloseFileWithoutSave', - toFileUri - ) - - // delete the toFileUri - await VsCodeFS.unlink(toFileUri.fsPath) - - // if fromFileUri is not opened, open it - const fromFileDocument = await vscode.workspace.openTextDocument(fromFileUri) - await vscode.window.showTextDocument(fromFileDocument) -} diff --git a/src/commands/smart-paste.ts b/src/commands/smart-paste/build-convert-chat-messages.ts similarity index 75% rename from src/commands/smart-paste.ts rename to src/commands/smart-paste/build-convert-chat-messages.ts index 052355e..aa70f15 100644 --- a/src/commands/smart-paste.ts +++ b/src/commands/smart-paste/build-convert-chat-messages.ts @@ -1,16 +1,10 @@ import { getReferenceFilePaths } from '@/ai/get-reference-file-paths' -import { - createModelProvider, - getCurrentSessionIdHistoriesMap -} from '@/ai/helpers' import { safeReadClipboard } from '@/clipboard' import { getConfigKey } from '@/config' import { getFileOrFoldersPromptInfo } from '@/file-utils/get-fs-prompt-info' import { insertTextAtSelection } from '@/file-utils/insert-text-at-selection' -import { streamingCompletionWriter } from '@/file-utils/stream-completion-writer' import { t } from '@/i18n' import { cacheFn } from '@/storage' -import { getCurrentWorkspaceFolderEditor } from '@/utils' import { HumanMessage, type BaseMessage } from '@langchain/core/messages' import * as vscode from 'vscode' @@ -30,7 +24,7 @@ const getClipboardContent = async () => { return { clipboardImg, clipboardContent } } -const buildConvertChatMessages = async ({ +export const buildConvertChatMessages = async ({ workspaceFolder, currentFilePath, selection @@ -168,62 +162,3 @@ Convert the clipboard content to match the programming language and context of t return messages } - -export const cleanupSmartPasteRunnables = async () => { - const openDocumentPaths = new Set( - vscode.workspace.textDocuments.map(doc => doc.uri.fsPath) - ) - const sessionIdHistoriesMap = await getCurrentSessionIdHistoriesMap() - - Object.keys(sessionIdHistoriesMap).forEach(sessionId => { - const path = sessionId.match(/^smartPaste:(.*)$/)?.[1] - - if (path && !openDocumentPaths.has(path)) { - delete sessionIdHistoriesMap[sessionId] - } - }) -} - -export const handleSmartPaste = async () => { - const { workspaceFolder, activeEditor } = - await getCurrentWorkspaceFolderEditor() - const currentFilePath = activeEditor.document.uri.fsPath - - // ai - const modelProvider = await createModelProvider() - const aiModelAbortController = new AbortController() - const aiModel = (await modelProvider.getModel()).bind({ - signal: aiModelAbortController.signal - }) - - const sessionId = `smartPaste:${currentFilePath}}` - const sessionIdHistoriesMap = await getCurrentSessionIdHistoriesMap() - - // TODO: remove and support continue generate in the future - delete sessionIdHistoriesMap[sessionId] - - await streamingCompletionWriter({ - editor: activeEditor, - onCancel() { - aiModelAbortController.abort() - }, - buildAiStream: async () => { - const convertMessages = await buildConvertChatMessages({ - workspaceFolder, - currentFilePath, - selection: activeEditor.selection - }) - - const history = await modelProvider.getHistory(sessionId) - const historyMessages = await history.getMessages() - const currentMessages: BaseMessage[] = convertMessages - const aiStream = aiModel.stream([...historyMessages, ...currentMessages]) - history.addMessages(currentMessages) - - return aiStream - } - }) - - // TODO: remove and support continue generate in the future - delete sessionIdHistoriesMap[sessionId] -} diff --git a/src/commands/smart-paste/index.ts b/src/commands/smart-paste/index.ts new file mode 100644 index 0000000..226fe7a --- /dev/null +++ b/src/commands/smart-paste/index.ts @@ -0,0 +1,69 @@ +import { + createModelProvider, + getCurrentSessionIdHistoriesMap +} from '@/ai/helpers' +import { streamingCompletionWriter } from '@/file-utils/stream-completion-writer' +import { getCurrentWorkspaceFolderEditor } from '@/utils' +import { type BaseMessage } from '@langchain/core/messages' +import * as vscode from 'vscode' + +import { buildConvertChatMessages } from './build-convert-chat-messages' + +export const cleanupSmartPasteRunnables = async () => { + const openDocumentPaths = new Set( + vscode.workspace.textDocuments.map(doc => doc.uri.fsPath) + ) + const sessionIdHistoriesMap = await getCurrentSessionIdHistoriesMap() + + Object.keys(sessionIdHistoriesMap).forEach(sessionId => { + const path = sessionId.match(/^smartPaste:(.*)$/)?.[1] + + if (path && !openDocumentPaths.has(path)) { + delete sessionIdHistoriesMap[sessionId] + } + }) +} + +export const handleSmartPaste = async () => { + const { workspaceFolder, activeEditor } = + await getCurrentWorkspaceFolderEditor() + const currentFilePath = activeEditor.document.uri.fsPath + + // ai + const modelProvider = await createModelProvider() + const aiModelAbortController = new AbortController() + const aiModel = (await modelProvider.getModel()).bind({ + signal: aiModelAbortController.signal + }) + + const sessionId = `smartPaste:${currentFilePath}}` + const sessionIdHistoriesMap = await getCurrentSessionIdHistoriesMap() + + // TODO: remove and support continue generate in the future + delete sessionIdHistoriesMap[sessionId] + + await streamingCompletionWriter({ + editor: activeEditor, + onCancel() { + aiModelAbortController.abort() + }, + buildAiStream: async () => { + const convertMessages = await buildConvertChatMessages({ + workspaceFolder, + currentFilePath, + selection: activeEditor.selection + }) + + const history = await modelProvider.getHistory(sessionId) + const historyMessages = await history.getMessages() + const currentMessages: BaseMessage[] = convertMessages + const aiStream = aiModel.stream([...historyMessages, ...currentMessages]) + history.addMessages(currentMessages) + + return aiStream + } + }) + + // TODO: remove and support continue generate in the future + delete sessionIdHistoriesMap[sessionId] +} diff --git a/src/config.ts b/src/config.ts index e175f91..12dec07 100644 --- a/src/config.ts +++ b/src/config.ts @@ -84,6 +84,10 @@ const configKey = { ...pkgConfig['aide.useSystemProxy'], type: 'boolean' }, + apiConcurrency: { + ...pkgConfig['aide.apiConcurrency'], + type: 'number' + }, openaiKey: { ...pkgConfig['aide.openaiKey'], type: 'string' diff --git a/src/file-utils/create-tmp-file.ts b/src/file-utils/create-tmp-file.ts index f9bda83..82d8dd9 100644 --- a/src/file-utils/create-tmp-file.ts +++ b/src/file-utils/create-tmp-file.ts @@ -1,27 +1,38 @@ import path from 'path' -import { languageIds } from '@/constants' import { t } from '@/i18n' -import { getLanguageIdExt } from '@/utils' +import { getLanguageId, getLanguageIdExt } from '@/utils' import * as vscode from 'vscode' /** * Returns a temporary file URI based on the original file URI and language ID. * @param originalFileUri The URI of the original file. + * @param originalFileFullPath The full path of the original file. * @param languageId The language ID of the file. + * @param untitled Indicates whether the temporary file is untitled. * @returns The temporary file URI. */ -export const getTmpFileUri = ( - originalFileUri: vscode.Uri, +export const getTmpFileUri = ({ + originalFileUri, + originalFileFullPath, + languageId, + untitled = true +}: { + originalFileUri?: vscode.Uri + originalFileFullPath?: string languageId: string -) => { - const originalFileDir = path.dirname(originalFileUri.fsPath) - const originalFileName = path.parse(originalFileUri.fsPath).name - const originalFileExt = path.parse(originalFileUri.fsPath).ext - + untitled?: boolean +}) => { + if (!originalFileUri && !originalFileFullPath) + throw new Error(t('error.fileNotFound')) + const filePath = originalFileFullPath || originalFileUri!.fsPath + + const originalFileDir = path.dirname(filePath) + const originalFileName = path.parse(filePath).name + const originalFileExt = path.parse(filePath).ext const languageExt = getLanguageIdExt(languageId) || languageId return vscode.Uri.parse( - `untitled:${path.join(originalFileDir, `${originalFileName}${originalFileExt}.aide${languageExt ? `.${languageExt}` : ''}`)}` + `${untitled ? 'untitled:' : ''}${path.join(originalFileDir, `${originalFileName}${originalFileExt}.aide${languageExt ? `.${languageExt}` : ''}`)}` ) } @@ -157,7 +168,10 @@ export const createTmpFileInfo = async (): Promise => { } const originalFileLanguageId = originalFileDocument.languageId - const tmpFileUri = getTmpFileUri(originalFileUri, originalFileLanguageId) + const tmpFileUri = getTmpFileUri({ + originalFileUri, + languageId: originalFileLanguageId + }) const tmpFileDocument = vscode.workspace.textDocuments.find( document => document.uri.fsPath === tmpFileUri.fsPath ) @@ -178,7 +192,8 @@ export const createTmpFileInfo = async (): Promise => { } export interface CreateTmpFileOptions { - languageId: string + languageId?: string + tmpFileUri?: vscode.Uri } /** @@ -218,6 +233,18 @@ export interface WriteTmpFileResult { */ getText: () => string + /** + * Saves the temporary file. + * @returns A promise that resolves when the temporary file is saved. + */ + save: () => Promise + + /** + * Closes the temporary file. + * @returns A promise that resolves when the temporary file is closed. + */ + close: () => Promise + /** * Checks if the temporary file was closed without saving. * @returns A boolean indicating if the temporary file was closed without saving. @@ -233,9 +260,21 @@ export interface WriteTmpFileResult { export const createTmpFileAndWriter = async ( options: CreateTmpFileOptions ): Promise => { - const { languageId } = options + if (!options.languageId && !options.tmpFileUri) + throw new Error( + "createTmpFileAndWriter: Either 'languageId' or 'tmpFileUri' must be provided." + ) + const originalFileUri = getOriginalFileUri() - const tmpFileUri = getTmpFileUri(originalFileUri, languageId) + + const languageId = + options.languageId || + getLanguageId(path.extname(options.tmpFileUri!.fsPath).slice(1)) + + const tmpFileUri = + options.tmpFileUri || + getTmpFileUri({ originalFileUri, languageId: languageId! }) + const tmpDocument = await vscode.workspace.openTextDocument(tmpFileUri) const isDocumentAlreadyShown = vscode.window.visibleTextEditors.some( editor => editor.document.uri.toString() === tmpDocument.uri.toString() @@ -248,11 +287,13 @@ export const createTmpFileAndWriter = async ( }) } - const docLanguageId = languageIds.includes(languageId) - ? languageId - : 'plaintext' + if (languageId) { + // const docLanguageId = languageIds.includes(languageId) + // ? languageId + // : 'plaintext' - vscode.languages.setTextDocumentLanguage(tmpDocument, docLanguageId) + vscode.languages.setTextDocumentLanguage(tmpDocument, languageId) + } const writeText = async (text: string) => { const edit = new vscode.WorkspaceEdit() @@ -278,6 +319,17 @@ export const createTmpFileAndWriter = async ( const getText = () => tmpDocument.getText() + const save = async () => { + await tmpDocument.save() + } + + const close = async () => { + await vscode.commands.executeCommand( + 'aide.quickCloseFileWithoutSave', + tmpFileUri + ) + } + const isClosedWithoutSaving = () => { if (tmpDocument.isClosed) { return !tmpDocument.getText() @@ -292,6 +344,8 @@ export const createTmpFileAndWriter = async ( writeText, writeTextPart, getText, + save, + close, isClosedWithoutSaving } } diff --git a/src/file-utils/tmp-file-writer.ts b/src/file-utils/tmp-file-writer.ts index 2ebd30e..5f5606a 100644 --- a/src/file-utils/tmp-file-writer.ts +++ b/src/file-utils/tmp-file-writer.ts @@ -15,6 +15,10 @@ import { } from './create-tmp-file' export interface TmpFileWriterOptions extends CreateTmpFileOptions { + stopWriteWhenClosed?: boolean + enableProcessLoading?: boolean + autoSaveWhenDone?: boolean + autoCloseWhenDone?: boolean buildAiStream: () => Promise> onCancel?: () => void } @@ -28,25 +32,41 @@ export interface TmpFileWriterOptions extends CreateTmpFileOptions { export const tmpFileWriter = async ( options: TmpFileWriterOptions ): Promise => { - const { buildAiStream, onCancel, ...createTmpFileOptions } = options + const { + buildAiStream, + onCancel, + stopWriteWhenClosed = true, + enableProcessLoading = true, + autoSaveWhenDone = false, + autoCloseWhenDone = false, + ...createTmpFileOptions + } = options const createTmpFileAndWriterReturns = await createTmpFileAndWriter(createTmpFileOptions) - const { writeTextPart, getText, writeText, isClosedWithoutSaving } = - createTmpFileAndWriterReturns + const { + writeTextPart, + getText, + writeText, + save, + close, + isClosedWithoutSaving + } = createTmpFileAndWriterReturns const ModelProvider = await getCurrentModelProvider() const { showProcessLoading, hideProcessLoading } = createLoading() try { - showProcessLoading({ - onCancel - }) + enableProcessLoading && + showProcessLoading({ + onCancel + }) + const aiStream = await buildAiStream() for await (const chunk of aiStream) { - if (isClosedWithoutSaving()) { - hideProcessLoading() + if (stopWriteWhenClosed && isClosedWithoutSaving()) { + enableProcessLoading && hideProcessLoading() return createTmpFileAndWriterReturns } @@ -76,8 +96,12 @@ export const tmpFileWriter = async ( // write the final code await writeText(finalText) } + + if (autoSaveWhenDone) save() + + if (autoCloseWhenDone) close() } finally { - hideProcessLoading() + enableProcessLoading && hideProcessLoading() } return createTmpFileAndWriterReturns diff --git a/src/index.ts b/src/index.ts index 3b2387b..e85e605 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,6 +1,7 @@ import * as vscode from 'vscode' import { BaseModelProvider } from './ai/model-providers/base' +import { autoOpenCorrespondingFiles } from './auto-open-corresponding-files' import { cleanup } from './cleanup' import { registerCommands } from './commands' import { setContext } from './context' @@ -25,6 +26,7 @@ export const activate = async (context: vscode.ExtensionContext) => { await registerCommands(context) await registerProviders(context) + await autoOpenCorrespondingFiles(context) await cleanup(context) } catch (err) { logger.warn('Failed to activate extension', err) diff --git a/src/utils.ts b/src/utils.ts index 74ea119..9b75a10 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -141,3 +141,46 @@ export const tryParseJSON = (str: string, returnOriginal = false) => { return returnOriginal ? str : null } } + +type QuickPickItemType = string | vscode.QuickPickItem + +export interface QuickPickOptions { + items: QuickPickItemType[] + placeholder: string + customOption?: string +} + +export const showQuickPickWithCustomInput = async ( + options: QuickPickOptions +): Promise => { + const quickPick = vscode.window.createQuickPick() + + quickPick.items = options.items.map(item => + typeof item === 'string' ? { label: item } : item + ) + + quickPick.placeholder = options.placeholder + + if (options.customOption) { + quickPick.items = [{ label: options.customOption }, ...quickPick.items] + } + + return new Promise(resolve => { + quickPick.onDidAccept(() => { + const selection = quickPick.selectedItems[0] + if (selection) { + resolve(selection.label) + } else { + resolve(quickPick.value) + } + quickPick.hide() + }) + + quickPick.onDidHide(() => { + resolve('') + quickPick.dispose() + }) + + quickPick.show() + }) +} diff --git a/website/en/guide/features/code-convert.md b/website/en/guide/features/code-convert.md index cb50f58..a60f9fe 100644 --- a/website/en/guide/features/code-convert.md +++ b/website/en/guide/features/code-convert.md @@ -4,6 +4,8 @@ Command Name: `aide.codeConvert` Use AI to convert an entire file or selected code from one programming language to another. Supports any language. Most languages support highlighting. +You can enter any language or file extension. If it is not in the [`language list`](https://code.visualstudio.com/docs/languages/identifiers#_known-language-identifiers), it might not support syntax highlighting, but it can still be converted. + **Usage:** - Select the code in the editor. @@ -15,19 +17,19 @@ If the output is interrupted, you can click the original paper icon or right-cli