diff --git a/.gitignore b/.gitignore index 7b08db9..3488941 100644 --- a/.gitignore +++ b/.gitignore @@ -9,3 +9,4 @@ custom/* !custom/config.yml.example user_settings.yml +.aider* diff --git a/lib/ide_service/service.py b/lib/ide_service/service.py index ee054a2..d13d6d2 100644 --- a/lib/ide_service/service.py +++ b/lib/ide_service/service.py @@ -117,7 +117,7 @@ def ide_name(self) -> str: return self._result @rpc_method - def diff_apply(self, filepath, content) -> bool: + def diff_apply(self, filepath, content, autoedit: bool = False) -> bool: """ Applies a given diff to a file. @@ -182,3 +182,25 @@ def get_extension_tools_path(self) -> str: The extension tools path. """ return self._result + + @rpc_method + def select_range( + self, fileName: str, startLine: int, startColumn: int, endLine: int, endColumn: int + ) -> bool: + """ + Selects a range of text in the specified file. + + Args: + fileName: The name of the file. + startLine: The starting line of the selection (0-based). + startColumn: The starting column of the selection (0-based). + endLine: The ending line of the selection (0-based). + endColumn: The ending column of the selection (0-based). + + Returns: + A boolean indicating whether the selection was successful. + + Note: + If startLine is -1, it cancels the current selection. + """ + return self._result diff --git a/lib/ide_service/vscode_service.py b/lib/ide_service/vscode_service.py index c8f93a8..7de4536 100644 --- a/lib/ide_service/vscode_service.py +++ b/lib/ide_service/vscode_service.py @@ -10,12 +10,7 @@ def run_code(code: str): @rpc_call -def diff_apply(filepath, content): - pass - - -@rpc_call -def get_symbol_defines_in_selected_code(): +def diff_apply(filepath, content, autoedit=False): pass diff --git a/merico/aider/README.md b/merico/aider/README.md new file mode 100644 index 0000000..08a4c56 --- /dev/null +++ b/merico/aider/README.md @@ -0,0 +1,19 @@ +### 操作指南 + +aider工作流命令使用步骤如下: + +1. 确保已经使用 `/aider.files.add` 命令添加了需要处理的文件。 +2. 输入 `/aider ` 命令,其中 `` 是你想要aider执行的任务描述。 +3. 等待aider生成建议的更改。 +4. 系统会自动显示每个文件的Diff View,你可以选择是否接受修改。 +5. 对于多个文件的更改,系统会在每个文件之后询问是否继续查看下一个文件的更改。 + +注意事项: +- 如果没有添加任何文件到aider,命令将会提示你先使用 'aider.files.add' 命令添加文件。 +- 你可以使用 `aider.files.remove` 命令从aider中移除文件。 +- 所有的更改都会在IDE中以Diff View的形式展示,你可以在查看后决定是否应用这些更改。 + +使用示例: +/aider 重构这段代码以提高性能 + +这个命令会让aider分析当前添加的文件,并提供重构建议以提高代码性能。 \ No newline at end of file diff --git a/merico/aider/command.py b/merico/aider/command.py new file mode 100644 index 0000000..602aabe --- /dev/null +++ b/merico/aider/command.py @@ -0,0 +1,214 @@ +import json +import os +import subprocess +import sys + +from devchat.ide import IDEService + +from lib.chatmark import Button + +GLOBAL_CONFIG_PATH = os.path.join(os.path.expanduser("~"), ".chat", ".workflow_config.json") + + +def save_config(config_path, item, value): + if os.path.exists(config_path): + with open(config_path, "r", encoding="utf-8") as f: + config = json.load(f) + else: + config = {} + + config[item] = value + with open(config_path, "w", encoding="utf-8") as f: + json.dump(config, f, indent=4) + + +def write_python_path_to_config(): + """ + Write the current system Python path to the configuration. + """ + python_path = sys.executable + save_config(GLOBAL_CONFIG_PATH, "aider_python", python_path) + print(f"Python path '{python_path}' has been written to the configuration.") + + +def get_aider_files(): + """ + 从.chat/.aider_files文件中读取aider文件列表 + """ + aider_files_path = os.path.join(".chat", ".aider_files") + if not os.path.exists(aider_files_path): + return [] + + with open(aider_files_path, "r") as f: + return [line.strip() for line in f if line.strip()] + + +def run_aider(message, files): + """ + 运行aider命令 + """ + python = sys.executable + model = os.environ.get("LLM_MODEL", "gpt-3.5-turbo-1106") + + cmd = [ + python, + "-m", + "aider", + "--model", + f"openai/{model}", + "--yes", + "--no-auto-commits", + "--dry-run", + "--no-pretty", + "--message", + message, + ] + files + + process = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True) + + has_started = False + aider_output = "" + for line in process.stdout: + if "run with --help" in line or 'run "aider --help"' in line: + has_started = True + continue + if has_started: + aider_output += line + print(line, end="", flush=True) + + return_code = process.wait() + + if return_code != 0: + for line in process.stderr: + print(f"Error: {line.strip()}", file=sys.stderr) + sys.exit(return_code) + + return aider_output + + +def apply_changes(changes, files): + """ + 应用aider生成的更改 + """ + changes_file = ".chat/changes.txt" + os.makedirs(os.path.dirname(changes_file), exist_ok=True) + with open(changes_file, "w") as f: + f.write(changes) + + python = sys.executable + model = os.environ.get("LLM_MODEL", "gpt-3.5-turbo-1106") + + cmd = [ + python, + "-m", + "aider", + "--model", + f"openai/{model}", + "--yes", + "--no-auto-commits", + "--apply", + changes_file, + ] + files + + process = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True) + + has_started = False + for line in process.stdout: + if "Model:" in line: + has_started = True + continue + if has_started: + print(line, end="", flush=True) + + return_code = process.wait() + + if return_code != 0: + for line in process.stderr: + print(f"Error: {line.strip()}", file=sys.stderr) + sys.exit(return_code) + + os.remove(changes_file) + + +def main(): + """ + Main function to run the aider command. + + This function performs the following tasks: + 1. Checks for correct command-line usage + 2. Writes the current Python path to the configuration + 3. Retrieves the list of files to be processed + 4. Runs the aider command with the given message + 5. Applies the suggested changes + 6. Displays the differences in the IDE + + Usage: python command.py + """ + if len(sys.argv) < 2: + print("Usage: python command.py ", file=sys.stderr) + sys.exit(1) + + write_python_path_to_config() + + message = sys.argv[1] + files = get_aider_files() + + if not files: + print( + "No files added to aider. Please add files using 'aider.files.add' command.", + file=sys.stderr, + ) + sys.exit(1) + + print("Running aider...\n", flush=True) + changes = run_aider(message, files) + + if not changes: + print("No changes suggested by aider.") + sys.exit(0) + + print("\nApplying changes...\n", flush=True) + + # 保存原始文件内容 + original_contents = {} + for file in files: + with open(file, "r") as f: + original_contents[file] = f.read() + + # 应用更改 + apply_changes(changes, files) + + # 读取更新后的文件内容 + updated_contents = {} + for file in files: + with open(file, "r") as f: + updated_contents[file] = f.read() + + # 还原原始文件内容 + for file in files: + with open(file, "w") as f: + f.write(original_contents[file]) + + # 使用 IDEService 展示差异 + ide_service = IDEService() + for index, file in enumerate(files): + ide_service.diff_apply(file, updated_contents[file]) + if index < len(files) - 1: + # 等待用户确认 + button = Button( + ["Show Next Changes", "Cancel"], + ) + button.render() + + idx = button.clicked + print("click button:", idx) + if idx == 0: + continue + else: + break + + print("Changes have been displayed in the IDE.") + + +if __name__ == "__main__": + main() diff --git a/merico/aider/command.yml b/merico/aider/command.yml new file mode 100644 index 0000000..b125589 --- /dev/null +++ b/merico/aider/command.yml @@ -0,0 +1,9 @@ +description: "aider command" +workflow_python: + env_name: devchat-aider-env + version: 3.11.0 + dependencies: requirements.txt +input: required +help: README.md +steps: + - run: $workflow_python $command_path/command.py "$input" \ No newline at end of file diff --git a/merico/aider/files/add/README.md b/merico/aider/files/add/README.md new file mode 100644 index 0000000..b23c19e --- /dev/null +++ b/merico/aider/files/add/README.md @@ -0,0 +1,20 @@ +### aider.files.add + +添加文件到aider处理列表中。 + +用法: +/aider.files.add + +参数: +- : 要添加的文件路径(必需) + +描述: +这个命令将指定的文件添加到aider的处理列表中。添加后,该文件将被包含在后续的aider操作中。 + +注意: +- 文件路径必须是有效的格式。 +- 如果文件已经在列表中,它不会被重复添加。 +- 添加成功后,会显示当前aider文件列表。 + +示例: +/aider.files.add src/main.py \ No newline at end of file diff --git a/merico/aider/files/add/command.py b/merico/aider/files/add/command.py new file mode 100644 index 0000000..1f56830 --- /dev/null +++ b/merico/aider/files/add/command.py @@ -0,0 +1,66 @@ +import os +import sys + + +def is_valid_path(path): + """ + 检查路径是否为有效的文件路径形式 + """ + try: + # 尝试规范化路径 + normalized_path = os.path.normpath(path) + # 检查路径是否是绝对路径或相对路径 + return ( + os.path.isabs(normalized_path) + or not os.path.dirname(normalized_path) == normalized_path + ) + except Exception: + return False + + +def add_file(file_path): + # 1. 检查是否为有效的文件路径形式 + if not is_valid_path(file_path): + print(f"Error: '{file_path}' is not a valid file path format.", file=sys.stderr) + sys.exit(1) + + # 获取绝对路径 + abs_file_path = file_path.strip() + + # 2. 将新增文件路径存储到.chat/.aider_files文件中 + aider_files_path = os.path.join(".chat", ".aider_files") + + # 确保.chat目录存在 + os.makedirs(os.path.dirname(aider_files_path), exist_ok=True) + + # 读取现有文件列表 + existing_files = set() + if os.path.exists(aider_files_path): + with open(aider_files_path, "r") as f: + existing_files = set(line.strip() for line in f) + + # 添加新文件 + existing_files.add(abs_file_path) + + # 写入更新后的文件列表 + with open(aider_files_path, "w") as f: + for file in sorted(existing_files): + f.write(f"{file}\n") + + print(f"Added '{abs_file_path}' to aider files.") + print("\nCurrent aider files:") + for file in sorted(existing_files): + print(f"- {file}") + + +def main(): + if len(sys.argv) != 2 or sys.argv[1].strip() == "": + print("Usage: /aider.files.add ", file=sys.stderr) + sys.exit(1) + + file_path = sys.argv[1] + add_file(file_path) + + +if __name__ == "__main__": + main() diff --git a/merico/aider/files/add/command.yml b/merico/aider/files/add/command.yml new file mode 100644 index 0000000..f56041e --- /dev/null +++ b/merico/aider/files/add/command.yml @@ -0,0 +1,5 @@ +description: "add files to aider" +input: required +help: README.md +steps: + - run: $devchat_python $command_path/command.py "$input" \ No newline at end of file diff --git a/merico/aider/files/list/README.md b/merico/aider/files/list/README.md new file mode 100644 index 0000000..9504d45 --- /dev/null +++ b/merico/aider/files/list/README.md @@ -0,0 +1,16 @@ +### aider.files.list + +列出当前在aider处理列表中的所有文件。 + +用法: +/aider.files.list + +描述: +这个命令会显示所有已添加到aider处理列表中的文件。它提供了一个当前aider正在处理的文件的概览。 + +注意: +- 如果没有文件被添加到aider,会显示相应的消息。 +- 文件按字母顺序排序显示。 + +示例: +/aider.files.list \ No newline at end of file diff --git a/merico/aider/files/list/command.py b/merico/aider/files/list/command.py new file mode 100644 index 0000000..f05e790 --- /dev/null +++ b/merico/aider/files/list/command.py @@ -0,0 +1,31 @@ +import os +import sys + + +def list_files(): + aider_files_path = os.path.join(".chat", ".aider_files") + + # 确保.chat/.aider_files文件存在 + if not os.path.exists(aider_files_path): + print("No files have been added to aider yet.") + sys.exit(0) + + # 读取文件列表 + with open(aider_files_path, "r") as f: + files = [line.strip() for line in f] + + # 打印文件列表 + if files: + print("Aider files:") + for file in sorted(files): + print(f"- {file}") + else: + print("No files found in aider.") + + +def main(): + list_files() + + +if __name__ == "__main__": + main() diff --git a/merico/aider/files/list/command.yml b/merico/aider/files/list/command.yml new file mode 100644 index 0000000..0c76383 --- /dev/null +++ b/merico/aider/files/list/command.yml @@ -0,0 +1,4 @@ +description: "list files in aider" +help: README.md +steps: + - run: $devchat_python $command_path/command.py \ No newline at end of file diff --git a/merico/aider/files/remove/README.md b/merico/aider/files/remove/README.md new file mode 100644 index 0000000..2607e03 --- /dev/null +++ b/merico/aider/files/remove/README.md @@ -0,0 +1,20 @@ +### aider.files.remove + +从aider处理列表中移除指定的文件。 + +用法: +/aider.files.remove + +参数: +- : 要移除的文件路径(必需) + +描述: +这个命令从aider的处理列表中移除指定的文件。移除后,该文件将不再被包含在后续的aider操作中。 + +注意: +- 文件路径必须是有效的格式。 +- 如果指定的文件不在列表中,会显示相应的消息。 +- 移除成功后,会显示更新后的aider文件列表。 + +示例: +/aider.files.remove src/main.py \ No newline at end of file diff --git a/merico/aider/files/remove/command.py b/merico/aider/files/remove/command.py new file mode 100644 index 0000000..5405a7b --- /dev/null +++ b/merico/aider/files/remove/command.py @@ -0,0 +1,72 @@ +import os +import sys + + +def is_valid_path(path): + """ + 检查路径是否为有效的文件路径形式 + """ + try: + # 尝试规范化路径 + normalized_path = os.path.normpath(path) + # 检查路径是否是绝对路径或相对路径 + return ( + os.path.isabs(normalized_path) + or not os.path.dirname(normalized_path) == normalized_path + ) + except Exception: + return False + + +def remove_file(file_path): + # 1. 检查是否为有效的文件路径形式 + if not is_valid_path(file_path): + print(f"Error: '{file_path}' is not a valid file path format.", file=sys.stderr) + sys.exit(1) + + # 获取绝对路径 + abs_file_path = file_path.strip() + + # 2. 从.chat/.aider_files文件中移除指定文件路径 + aider_files_path = os.path.join(".chat", ".aider_files") + + # 确保.chat目录存在 + if not os.path.exists(aider_files_path): + print(f"Error: '{aider_files_path}' does not exist.", file=sys.stderr) + sys.exit(1) + + # 读取现有文件列表 + existing_files = set() + with open(aider_files_path, "r") as f: + existing_files = set(line.strip() for line in f) + + # 检查文件是否在列表中 + if abs_file_path not in existing_files: + print(f"'{abs_file_path}' is not in aider files.") + sys.exit(0) + + # 移除文件 + existing_files.remove(abs_file_path) + + # 写入更新后的文件列表 + with open(aider_files_path, "w") as f: + for file in sorted(existing_files): + f.write(f"{file}\n") + + print(f"Removed '{abs_file_path}' from aider files.") + print("\nCurrent aider files:") + for file in sorted(existing_files): + print(f"- {file}") + + +def main(): + if len(sys.argv) != 2 or sys.argv[1].strip() == "": + print("Usage: /aider.files.remove ", file=sys.stderr) + sys.exit(1) + + file_path = sys.argv[1] + remove_file(file_path) + + +if __name__ == "__main__": + main() diff --git a/merico/aider/files/remove/command.yml b/merico/aider/files/remove/command.yml new file mode 100644 index 0000000..aa1f66d --- /dev/null +++ b/merico/aider/files/remove/command.yml @@ -0,0 +1,5 @@ +description: "remove files from aider" +input: required +help: README.md +steps: + - run: $devchat_python $command_path/command.py "$input" \ No newline at end of file diff --git a/merico/aider/requirements.txt b/merico/aider/requirements.txt new file mode 100644 index 0000000..02f9e9f --- /dev/null +++ b/merico/aider/requirements.txt @@ -0,0 +1,2 @@ +git+https://gitee.com/imlaji/aider.git@main +git+https://gitee.com/devchat-ai/devchat.git@aider diff --git a/merico/ask_issue/README.md b/merico/ask_issue/README.md new file mode 100644 index 0000000..79e0560 --- /dev/null +++ b/merico/ask_issue/README.md @@ -0,0 +1,21 @@ +### ask_issue + +自动修复代码中的lint错误。 + +用法: +/ask_issue + +描述: +这个命令帮助开发者自动修复代码中的lint错误。它使用AI分析选中的代码行,识别lint问题,并提供修复建议。 + +步骤: +1. 在IDE中选择包含lint错误的代码行。 +2. 运行 /ask_issue 命令。 +3. 命令会自动获取选中的代码、相关的lint诊断信息,并调用AI生成修复方案。 +4. AI会提供问题解释和修复后的代码片段。 + +注意事项: +- 确保在运行命令前已选择包含lint错误的代码行。 +- 命令会优先处理SonarLint诊断的问题。 +- AI生成的修复方案会包含问题解释和修改后的代码。 +- 修改后的代码会以Markdown格式展示,包含足够的上下文信息。 diff --git a/merico/ask_issue/command.yml b/merico/ask_issue/command.yml new file mode 100644 index 0000000..e96e880 --- /dev/null +++ b/merico/ask_issue/command.yml @@ -0,0 +1,4 @@ +description: Automatically fix lint errors. +help: README.md +steps: + - run: $devchat_python $command_path/main.py diff --git a/merico/ask_issue/main.py b/merico/ask_issue/main.py new file mode 100644 index 0000000..5211d6f --- /dev/null +++ b/merico/ask_issue/main.py @@ -0,0 +1,286 @@ +import os +import re +import sys + +from devchat.llm import chat +from devchat.memory import FixSizeChatMemory + +from lib.ide_service import IDEService + + +def extract_edits_block(text): + """ + Extracts the first Markdown code block from the given text without the language specifier. + + :param text: A string containing Markdown text + :return: The content of the first Markdown code block, or None if not found + """ + index = text.find("```edits") + if index == -1: + return None + else: + start = index + len("```edits") + end = text.find("```", start) + if end == -1: + return None + else: + return text[start:end] + + +def extract_markdown_block(text): + """ + Extracts the first Markdown code block from the given text without the language specifier. + + :param text: A string containing Markdown text + :return: The content of the first Markdown code block, or None if not found + """ + edit_code = extract_edits_block(text) + if edit_code: + return edit_code + + pattern = r"```(?:\w+)?\s*\n(.*?)\n```" + match = re.search(pattern, text, re.DOTALL) + + if match: + block_content = match.group(1) + return block_content + else: + # whether exist ```language? + if text.find("```"): + return None + return text + + +# step 1 : get selected code +def get_selected_code(): + selected_data = IDEService().get_selected_range().dict() + + if selected_data["range"]["start"] == -1: + return None, None, None + + if selected_data["range"]["start"]["line"] != selected_data["range"]["end"]["line"]: + print("Please select the line code of issue reported.\n\n", file=sys.stderr) + sys.exit(1) + + return selected_data["abspath"], selected_data["text"], selected_data["range"]["start"]["line"] + + +# step 2 : input issue descriptions +def input_issue_descriptions(file_path, issue_line_num): + diagnostics = IDEService().get_diagnostics_in_range(file_path, issue_line_num, issue_line_num) + if not diagnostics: + return None + + # select first sonarlint diagnostic + for diagnostic in diagnostics: + if diagnostic.find(" 0: + return diagnostic + return diagnostics[0] + + +# step 3 : call llm to generate fix solutions +SYSTEM_ROLE_DIFF = """ +You are a code refactoring assistant. +Your task is to refactor the user's code to fix lint diagnostics. +You will be provided with a code snippet and a list of diagnostics. \ +Your response should include two parts: + +1. An explanation of the reason for the diagnostics and how to fix them. +2. The edited code snippet with the diagnostics fixed, using markdown format for clarity. + +The markdown block for edits should look like this: + +```edits +def hello(): + print("Call hello():") ++ print("hello") + +... + +- hello(20) ++ hello() +``` +Or like this, if a variable is not defined: + +```edits +... ++ cur_file = __file__ + print(cur_file) +``` +Please note the following important points: + +1. The new code should maintain the correct indentation. \ +The "+ " sign is followed by two spaces for indentation, \ +which should be included in the edited code. +2. In addition to outputting key editing information, \ +sufficient context (i.e., key information before and after editing) \ +should also be provided to help locate the specific position of the edited line. +3. Don't output all file lines, if some lines are unchanged, \ +please use "..." to indicate the ignored lines. +4. Use "+ " and "- " at start of the line to indicate the addition and deletion of lines. + +Here are some examples of incorrect responses: + +Incorrect example 1, where the indentation is not correct: + +```edits +def hello(): + print("Call hello():") ++ print("hello") +``` +In this case, if the "+ " sign and the extra space are removed, \ +the print("hello") statement will lack the necessary two spaces for correct indentation. + +Incorrect example 2, where no other code lines are provided: + +```edits ++ print("hello") +``` +This is an incorrect example because without additional context, \ +it's unclear where the new print("hello") statement should be inserted. +""" + +SYSTEM_ROLE_CODEBLOCK = """ +你是一个重构工程师,你需要根据错误描述,对代码进行问题修正,只需要关注描述的问题,不需要关注代码中的其他问题。 + +输出的修正代码中,如果修改了多个代码段,中间没有修改的代码段,请使用...表示。 +每一个被修改的代码段,应该包含前后至少3行未修改的代码,作为修改代码段的边界表示。 + +输出一个代码块中,例如: +```edits +def hello(): + msg = "hello" + print(msg) + +... + +if __name__ == "__main__": + hello() +``` +""" + + +LLM_MODEL = os.environ.get("LLM_MODEL", "gpt-3.5-turbo-1106") +if LLM_MODEL in [ + "qwen2-72b-instruct", + "qwen-long", + "qwen-turbo", + "Yi-34B-Chat", + "deepseek-coder", + "xinghuo-3.5", +]: + SYSTEM_ROLE = SYSTEM_ROLE_CODEBLOCK +else: + SYSTEM_ROLE = SYSTEM_ROLE_DIFF +MESSAGES_A = [ + { + "role": "system", + "content": SYSTEM_ROLE, + }, +] + +# step 3 : call llm to generate fix solutions +PROMPT = """ + +Here is the code file: + +{file_content} + +There is an issue in the following code: + +{issue_line_code} + +{issue_description} + +Here is the rule description: + +{rule_description} + +Please focus only on the error described in the prompt. \ +Other errors in the code should be disregarded. + +""" + +memory = FixSizeChatMemory(max_size=20, messages=MESSAGES_A) + + +@chat(prompt=PROMPT, stream_out=True, memory=memory) +def call_llm_to_generate_fix_solutions( + file_content, issue_line_code, issue_description, rule_description +): + pass + + +# current file content +def get_current_file_content(file_path, issue_line_num): + try: + return IDEService().get_collapsed_code(file_path, issue_line_num, issue_line_num) + except Exception: + print("Error reading file:", file=sys.stderr) + return None + + +# get issue description +def get_rule_description(issue_description): + def parse_source_code(text): + pattern = r"<(\w+):(.+?)>" + match = re.search(pattern, text) + + if match: + source = match.group(1) + code = match.group(2) + return source, code + else: + return None, None + + issue_source, issue_code = parse_source_code(issue_description) + if issue_source.find("sonar") == -1: + return issue_description + + issue_id = issue_code.split(":")[-1] + issue_language = issue_code.split(":")[0] + + tools_path = IDEService().get_extension_tools_path() + rules_path = "sonar-rspec" + + rule_path = os.path.join(tools_path, rules_path, "rules", issue_id, issue_language, "rule.adoc") + if os.path.exists(rule_path): + with open(rule_path, "r", encoding="utf-8") as file: + return file.read() + return issue_description + + +def main(): + print("start fix issue ...\n\n") + file_path, issue_line, issue_line_num = get_selected_code() + if not file_path or not issue_line: + print("No code selected. Please select the code line you want to fix.", file=sys.stderr) + sys.exit(1) + issue_description = input_issue_descriptions(file_path, issue_line_num) + if not issue_description: + print( + "There are no issues to resolve on the current line. " + "Please select the line where an issue needs to be resolved." + ) + sys.exit(0) + + print("make llm prompt ...\n\n") + current_file_content = get_current_file_content(file_path, issue_line_num) + rule_description = get_rule_description(issue_description) + # print("Rule description:\n\n", rule_description, end="\n\n") + + print("call llm to fix issue ...\n\n") + fix_solutions = call_llm_to_generate_fix_solutions( + file_content=current_file_content, + issue_line_code=issue_line, + issue_description=issue_description, + rule_description=rule_description, + ) + if not fix_solutions: + sys.exit(1) + + print("\n\n", flush=True) + + +if __name__ == "__main__": + main() diff --git a/merico/fix_issue/README.md b/merico/fix_issue/README.md index 60da6be..8c531e7 100644 --- a/merico/fix_issue/README.md +++ b/merico/fix_issue/README.md @@ -1,3 +1,23 @@ +### fix_issue -### 操作指南 +自动修复代码中的lint错误。 +用法: +/fix_issue + +描述: +这个命令帮助开发者自动修复代码中的lint错误。它使用AI分析选中的代码行,识别lint问题,并提供修复建议。然后,它会自动应用这些修复建议,并在IDE中显示更改。 + +步骤: +1. 在IDE中选择包含lint错误的代码行。 +2. 运行 /fix_issue 命令。 +3. 命令会自动获取选中的代码、相关的lint诊断信息,并调用AI生成修复方案。 +4. AI会提供问题解释和修复后的代码。 +5. 命令会自动应用这些修复,并在IDE中显示更改。 + +注意事项: +- 确保在运行命令前已选择包含lint错误的代码行。 +- 命令会优先处理SonarLint诊断的问题。 +- 如果安装了aider Python,命令会使用aider来执行AI访问和应用更改。 +- 如果没有安装aider Python,命令会使用默认实现来生成和应用修复。 +- 所有的更改都会在IDE中以Diff View的形式展示,你可以在查看后决定是否接受这些更改。 diff --git a/merico/fix_issue/main.py b/merico/fix_issue/main.py index 7afe2b9..dcb0523 100644 --- a/merico/fix_issue/main.py +++ b/merico/fix_issue/main.py @@ -1,12 +1,34 @@ +import json import os import re +import subprocess import sys from devchat.llm import chat +from devchat.memory import FixSizeChatMemory from lib.ide_service import IDEService +def extract_edits_block(text): + """ + Extracts the first Markdown code block from the given text without the language specifier. + + :param text: A string containing Markdown text + :return: The content of the first Markdown code block, or None if not found + """ + index = text.find("```edits") + if index == -1: + return None + else: + start = index + len("```edits") + end = text.find("```", start) + if end == -1: + return None + else: + return text[start:end] + + def extract_markdown_block(text): """ Extracts the first Markdown code block from the given text without the language specifier. @@ -14,6 +36,10 @@ def extract_markdown_block(text): :param text: A string containing Markdown text :return: The content of the first Markdown code block, or None if not found """ + edit_code = extract_edits_block(text) + if edit_code: + return edit_code + pattern = r"```(?:\w+)?\s*\n(.*?)\n```" match = re.search(pattern, text, re.DOTALL) @@ -54,30 +80,165 @@ def input_issue_descriptions(file_path, issue_line_num): return diagnostics[0] +# step 3 : call llm to generate fix solutions +SYSTEM_ROLE_DIFF = """ +You are a code refactoring assistant. \ +Your task is to refactor the user's code to fix lint diagnostics. \ +You will be provided with a code snippet and a list of diagnostics. \ +Your response should include two parts: + +1. An explanation of the reason for the diagnostics and how to fix them. +2. The edited code snippet with the diagnostics fixed, using markdown format for clarity. + +The markdown block for edits should look like this: + +```edits +def hello(): + print("Call hello():") ++ print("hello") + +... + +- hello(20) ++ hello() +``` +Or like this, if a variable is not defined: + +```edits +... ++ cur_file = __file__ + print(cur_file) +``` +Please note the following important points: + +1. The new code should maintain the correct indentation. \ +The "+ " sign is followed by two spaces for indentation, \ +which should be included in the edited code. +2. In addition to outputting key editing information, \ +sufficient context (i.e., key information before and after editing) \ +should also be provided to help locate the specific position of the edited line. +3. Don't output all file lines, if some lines are unchanged, \ +please use "..." to indicate the ignored lines. +4. Use "+ " and "- " at start of the line to indicate the addition and deletion of lines. + +Here are some examples of incorrect responses: + +Incorrect example 1, where the indentation is not correct: + +```edits +def hello(): + print("Call hello():") ++ print("hello") +``` +In this case, if the "+ " sign and the extra space are removed, \ +the print("hello") statement will lack the necessary two spaces for correct indentation. + +Incorrect example 2, where no other code lines are provided: + +```edits ++ print("hello") +``` +This is an incorrect example because without additional context, \ +it's unclear where the new print("hello") statement should be inserted. +""" + +SYSTEM_ROLE_CODEBLOCK = """ +你是一个重构工程师,你需要根据错误描述,对代码进行问题修正,只需要关注描述的问题,不需要关注代码中的其他问题。 + +输出的修正代码中,如果修改了多个代码段,中间没有修改的代码段,请使用...表示。 +每一个被修改的代码段,应该包含前后至少3行未修改的代码,作为修改代码段的边界表示。 + +输出一个代码块中,例如: +```edits +def hello(): + msg = "hello" + print(msg) + +... + +if __name__ == "__main__": + hello() +``` +""" + + +LLM_MODEL = os.environ.get("LLM_MODEL", "gpt-3.5-turbo-1106") +if LLM_MODEL in [ + "qwen2-72b-instruct", + "qwen-long", + "qwen-turbo", + "Yi-34B-Chat", + "deepseek-coder", + "xinghuo-3.5", +]: + SYSTEM_ROLE = SYSTEM_ROLE_CODEBLOCK +else: + SYSTEM_ROLE = SYSTEM_ROLE_DIFF +MESSAGES_A = [ + { + "role": "system", + "content": SYSTEM_ROLE, + }, +] + # step 3 : call llm to generate fix solutions PROMPT = """ -You are a code refactoring assistant. -This is my code file: + +Here is the code file: + {file_content} -There is a issue in the following code: +There is an issue in the following code: + {issue_line_code} + {issue_description} Here is the rule description: + {rule_description} -Please provide me refactor code to fix this issue. +Please focus only on the error described in the prompt. \ +Other errors in the code should be disregarded. + """ +memory = FixSizeChatMemory(max_size=20, messages=MESSAGES_A) -@chat(prompt=PROMPT, stream_out=True) + +@chat(prompt=PROMPT, stream_out=True, memory=memory) def call_llm_to_generate_fix_solutions( file_content, issue_line_code, issue_description, rule_description ): pass +APPLY_SYSTEM_PROMPT = """ +Your task is apply the fix solution to the code, \ +output the whole new code in markdown code block format. + +Here is the code file: +{file_content} + +Here is the fix solution: +{fix_solution} + +Some rules for output code: +1. Focus on the fix solution, don't focus on other errors in the code. +2. Don't change the indentation of the code. +3. Don't change lines which are not metioned in fix solution, for example, \ +don't remove empty lines in code. + +Please output only the whole new code which is the result of \ +applying the fix solution, and output the whole code. +""" + + +@chat(prompt=APPLY_SYSTEM_PROMPT, stream_out=True, model="deepseek-coder") +def apply_fix_solution(file_content, fix_solution): + pass + + # current file content def get_current_file_content(file_path, issue_line_num): try: @@ -117,18 +278,150 @@ def parse_source_code(text): return issue_description -# step 4 : apply fix solutions to code -def apply_fix_solutions_to_code(): - pass +def get_file_content(file_path): + try: + with open(file_path, "r", encoding="utf-8") as file: + return file.read() + except Exception: + print("Error reading file:", file=sys.stderr) + return None -# step 0: try parse user input -def try_parse_user_input(): - pass +GLOBAL_CONFIG_PATH = os.path.join(os.path.expanduser("~"), ".chat", ".workflow_config.json") + + +def get_aider_python_path(): + """ + Retrieves the path to the Aider Python executable from the global configuration file. + + Returns: + str or None: The path to the Aider Python executable if found in the configuration, + or None if the configuration file doesn't exist or the path is not set. + """ + if os.path.exists(GLOBAL_CONFIG_PATH): + with open(GLOBAL_CONFIG_PATH, "r", encoding="utf-8") as f: + config = json.load(f) + return config.get("aider_python2") + return None + + +def run_aider(message, file_path): + """ + Run the Aider tool to apply changes to a file based on a given message. + + This function executes the Aider tool with specific parameters to apply changes + to the specified file. It captures and returns the output from Aider. + + Args: + message (str): The message describing the changes to be made. + file_path (str): The path to the file that needs to be modified. + + Returns: + str: The output from the Aider tool, containing information about the changes made. + + Raises: + SystemExit: If the Aider process returns a non-zero exit code, indicating an error. + """ + python = get_aider_python_path() + model = os.environ.get("LLM_MODEL", "gpt-3.5-turbo-1106") + + cmd = [ + python, + "-m", + "aider", + "--model", + f"openai/{model}", + "--yes", + "--no-auto-commits", + "--dry-run", + "--no-pretty", + "--message", + message, + file_path, + ] + + process = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True) + + has_started = False + aider_output = "" + for line in process.stdout: + if "run with --help" in line or 'run "aider --help"' in line: + has_started = True + continue + if has_started: + aider_output += line + print(line, end="", flush=True) + + return_code = process.wait() + + if return_code != 0: + for line in process.stderr: + print(f"Error: {line.strip()}", file=sys.stderr) + sys.exit(return_code) + + return aider_output + + +def apply_changes(changes, file_path): + """ + Apply the changes to the specified file using aider. + + Args: + changes (str): The changes to be applied to the file. + file_path (str): The path to the file where changes will be applied. + + This function creates a temporary file with the changes, then uses aider to apply + these changes to the specified file. It handles the execution of aider and manages + the output and potential errors. + """ + changes_file = ".chat/changes.txt" + os.makedirs(os.path.dirname(changes_file), exist_ok=True) + with open(changes_file, "w", encoding="utf-8") as f: + f.write(changes) + + python = get_aider_python_path() + model = os.environ.get("LLM_MODEL", "gpt-3.5-turbo-1106") + + cmd = [ + python, + "-m", + "aider", + "--model", + f"openai/{model}", + "--yes", + "--no-auto-commits", + "--apply", + changes_file, + file_path, + ] + + process = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True) + + has_started = False + for line in process.stdout: + if "Model:" in line: + has_started = True + continue + if has_started: + print(line, end="", flush=True) + + return_code = process.wait() + + if return_code != 0: + for line in process.stderr: + print(f"Error: {line.strip()}", file=sys.stderr) + sys.exit(return_code) + + os.remove(changes_file) def main(): - print("start fix issue ...\n\n") + """ + Main function to fix issues in the selected code. + It retrieves the selected code, gets issue descriptions, + generates fix solutions using LLM, and applies the changes. + """ + print("start fix issue ...\n\n", flush=True) file_path, issue_line, issue_line_num = get_selected_code() if not file_path or not issue_line: print("No code selected. Please select the code line you want to fix.", file=sys.stderr) @@ -141,20 +434,71 @@ def main(): ) sys.exit(0) - print("make llm prompt ...\n\n") + print("make llm prompt ...\n\n", flush=True) current_file_content = get_current_file_content(file_path, issue_line_num) rule_description = get_rule_description(issue_description) - print("--->>:", rule_description) - - print("call llm to fix issue ...\n\n") - fix_solutions = call_llm_to_generate_fix_solutions( - file_content=current_file_content, - issue_line_code=issue_line, - issue_description=issue_description, - rule_description=rule_description, - ) - if not fix_solutions: - sys.exit(1) + # print("Rule description:\n\n", rule_description, end="\n\n") + + print("call llm to fix issue ...\n\n", flush=True) + + # ===> 如果aider python已经安装,则直接调用aider来执行AI访问 + aider_python = get_aider_python_path() + + if aider_python and os.path.exists(aider_python): + python_path = os.environ.get("PYTHONPATH", "") + if python_path: + # remove PYTHONPATH + os.environ.pop("PYTHONPATH") + # Use aider-based implementation + message = f""" +Fix issue: {issue_description} +Which is reported at line: {issue_line} + +Rule description: {rule_description} +""" + changes = run_aider(message, file_path) + if not changes: + print("No changes suggested by aider.") + sys.exit(0) + + print("\nApplying changes...\n", flush=True) + + with open(file_path, "r", encoding="utf-8") as f: + original_content = f.read() + + apply_changes(changes, file_path) + + with open(file_path, "r", encoding="utf-8") as f: + updated_content = f.read() + + with open(file_path, "w", encoding="utf-8") as f: + f.write(original_content) + + os.environ["PYTHONPATH"] = python_path + + # Display changes in IDE + IDEService().select_range(file_path, -1, -1, -1, -1) + IDEService().diff_apply("", updated_content, False) + else: + print("No aider python found, using default implementation.", end="\n\n") + fix_solutions = call_llm_to_generate_fix_solutions( + file_content=current_file_content, + issue_line_code=issue_line, + issue_description=issue_description, + rule_description=rule_description, + ) + if not fix_solutions: + sys.exit(1) + + print("\n\n", flush=True) + + print("apply fix solution ...\n\n") + updated_content = extract_markdown_block(fix_solutions) + if updated_content: + # Display changes in IDE + IDEService().diff_apply("", updated_content, True) + + print("Changes have been displayed in the IDE.") if __name__ == "__main__":