From 470f3043d7dd248dbd8a88f11aa30b141c5463b9 Mon Sep 17 00:00:00 2001 From: "bobo.yang" Date: Fri, 9 Feb 2024 13:48:47 +0800 Subject: [PATCH 1/2] feat: Support cross-IDE text selection and visible range - Implemented new interface for IDE services to handle text selection and visible range retrieval - Enhanced compatibility with both vscode and IntelliJ through modular service design - Added LocationWithText type to unify response format across IDEs --- devchat/ide/idea_services.py | 14 ++++ devchat/ide/rpc.py | 39 +++++++++ devchat/ide/service.py | 140 ++++++++++++++++++++++----------- devchat/ide/types.py | 6 ++ devchat/ide/vscode_services.py | 35 +++++++++ 5 files changed, 189 insertions(+), 45 deletions(-) create mode 100644 devchat/ide/idea_services.py diff --git a/devchat/ide/idea_services.py b/devchat/ide/idea_services.py new file mode 100644 index 00000000..e0211316 --- /dev/null +++ b/devchat/ide/idea_services.py @@ -0,0 +1,14 @@ +from .types import LocationWithText +from .rpc import rpc_method + +class IdeaIDEService: + def __init__(self): + self._result = None + + @rpc_method + def get_visible_range(self) -> LocationWithText: + return LocationWithText.parse_obj(self._result) + + @rpc_method + def get_selected_range(self) -> LocationWithText: + return LocationWithText.parse_obj(self._result) diff --git a/devchat/ide/rpc.py b/devchat/ide/rpc.py index 23eb2caf..480e5df7 100644 --- a/devchat/ide/rpc.py +++ b/devchat/ide/rpc.py @@ -39,3 +39,42 @@ def wrapper(*args, **kwargs): raise err return wrapper + + +def rpc_method(f): + """ + Decorator for Service methods + """ + + @wraps(f) + def wrapper(self, *args, **kwargs): + if os.environ.get("DEVCHAT_IDE_SERVICE_URL", "") == "": + # maybe in a test, user don't want to mock services functions + return + + try: + function_name = f.__name__ + url = f"{BASE_SERVER_URL}/{function_name}" + + data = dict(zip(f.__code__.co_varnames[1:], args)) # Exclude "self" + data.update(kwargs) + headers = {"Content-Type": "application/json"} + + response = requests.post(url, json=data, headers=headers) + + if response.status_code != 200: + raise Exception(f"Server error: {response.status_code}") + + response_data = response.json() + if "error" in response_data: + raise Exception(f"Server returned an error: {response_data['error']}") + + # Store the result in the _result attribute of the instance + self._result = response_data.get("result", None) + return f(self, *args, **kwargs) + + except ConnectionError as err: + # TODO + raise err + + return wrapper diff --git a/devchat/ide/service.py b/devchat/ide/service.py index 8eaaa359..e622f198 100644 --- a/devchat/ide/service.py +++ b/devchat/ide/service.py @@ -7,54 +7,12 @@ # pylint: disable=W3101 # pylint: disable=C0103 -import os -from functools import wraps from typing import List -import requests - +from .rpc import rpc_method from .types import Location, SymbolNode - -BASE_SERVER_URL = os.environ.get("DEVCHAT_IDE_SERVICE_URL", "http://localhost:3000") - - -def rpc_method(f): - """ - Decorator for Service methods - """ - - @wraps(f) - def wrapper(self, *args, **kwargs): - if os.environ.get("DEVCHAT_IDE_SERVICE_URL", "") == "": - # maybe in a test, user don't want to mock services functions - return - - try: - function_name = f.__name__ - url = f"{BASE_SERVER_URL}/{function_name}" - - data = dict(zip(f.__code__.co_varnames[1:], args)) # Exclude "self" - data.update(kwargs) - headers = {"Content-Type": "application/json"} - - response = requests.post(url, json=data, headers=headers) - - if response.status_code != 200: - raise Exception(f"Server error: {response.status_code}") - - response_data = response.json() - if "error" in response_data: - raise Exception(f"Server returned an error: {response_data['error']}") - - # Store the result in the _result attribute of the instance - self._result = response_data.get("result", None) - return f(self, *args, **kwargs) - - except ConnectionError as err: - # TODO - raise err - - return wrapper +from .vscode_services import selected_range, visible_range +from .idea_services import IdeaIDEService class IDEService: @@ -72,18 +30,34 @@ def __init__(self): @rpc_method def get_lsp_brige_port(self) -> str: + """ + Get the LSP bridge port. + + :return: str + """ return self._result @rpc_method def install_python_env(self, command_name: str, requirements_file: str) -> str: + """ + A method to install a Python environment with the provided command name and requirements file, returning a string result. + """ return self._result @rpc_method def update_slash_commands(self) -> bool: + """ + Update the slash commands and return a boolean indicating the success of the operation. + """ return self._result @rpc_method def ide_language(self) -> str: + """ + Returns the current IDE language setting for the user. + - zh: Chinese + - en: English + """ return self._result @rpc_method @@ -95,8 +69,84 @@ def ide_logging(self, level: str, message: str) -> bool: @rpc_method def get_document_symbols(self, abspath: str) -> List[SymbolNode]: + """ + Retrieves the document symbols for a given file. + + Args: + abspath: The absolute path to the file whose symbols are to be retrieved. + + Returns: + A list of SymbolNode objects representing the symbols found in the document. + """ return [SymbolNode.parse_obj(node) for node in self._result] @rpc_method def find_type_def_locations(self, abspath: str, line: int, character: int) -> List[Location]: + """ + Finds the location of type definitions within a file. + + Args: + abspath: The absolute path to the file to be searched. + line: The line number within the file to begin the search. + character: The character position within the line to begin the search. + + Returns: + A list of Location objects representing the locations of type definitions found. + """ return [Location.parse_obj(loc) for loc in self._result] + + @rpc_method + def ide_name(self) -> str: + """Returns the name of the IDE. + + This method is a remote procedure call (RPC) that fetches the name of the IDE being used. + + Returns: + The name of the IDE as a string. + """ + return self._result + + @rpc_method + def diff_apply(self, filepath, content) -> bool: + """ + Applies a given diff to a file. + + This method uses the content provided to apply changes to the file + specified by the filepath. It's an RPC method that achieves file synchronization + by updating the local version of the file with the changes described in the + content parameter. + + Args: + filepath: The path to the file that needs to be updated. + content: A string containing the new code that should be applied to the file. + + Returns: + A boolean indicating if the diff was successfully applied. + """ + return self._result + + def get_visible_range(self): + """ + Determines and returns the visible range of code in the current IDE. + + Returns: + A tuple denoting the visible range if the IDE is VSCode, or defers to + IdeaIDEService's get_visible_range method for other IDEs. + """ + if self.ide_name() == "vscode": + return visible_range() + else: + return IdeaIDEService().get_visible_range() + + def get_selected_range(self): + """ + Retrieves the selected range of code in the current IDE. + + Returns: + Calls and returns the result of `selected_range()` if the IDE is VSCode, + otherwise, it defers to IdeaIDEService's `get_selected_range()` method. + """ + if self.ide_name() == "vscode": + return selected_range() + else: + return IdeaIDEService().get_selected_range() diff --git a/devchat/ide/types.py b/devchat/ide/types.py index dbd7ed46..01fb463b 100644 --- a/devchat/ide/types.py +++ b/devchat/ide/types.py @@ -23,3 +23,9 @@ class SymbolNode(BaseModel): kind: str range: Range children: List["SymbolNode"] + + +class LocationWithText(BaseModel): + abspath: str + range: Range + text: str diff --git a/devchat/ide/vscode_services.py b/devchat/ide/vscode_services.py index cb9a410e..b8021e56 100644 --- a/devchat/ide/vscode_services.py +++ b/devchat/ide/vscode_services.py @@ -1,6 +1,7 @@ import os from .rpc import rpc_call +from .types import LocationWithText @rpc_call @@ -109,6 +110,23 @@ def visible_lines(): "visibleRange": [start_line, end_line], } +def visible_range() -> LocationWithText: + visible_range_text = visible_lines() + return LocationWithText( + text=visible_range_text["visibleText"], + abspath=visible_range_text["filePath"], + range={ + "start": { + "line": visible_range_text["visibleRange"][0], + "character": 0, + }, + "end": { + "line": visible_range_text["visibleRange"][1], + "character": 0, + }, + } + ) + def selected_lines(): active_document = active_text_editor() @@ -140,3 +158,20 @@ def selected_lines(): "selectedText": "".join(_selected_lines), "selectedRange": [start_line, start_col, end_line, end_col], } + +def selected_range() -> LocationWithText: + selected_range_text = selected_lines() + return LocationWithText( + text=selected_range_text["selectedText"], + abspath=selected_range_text["filePath"], + range={ + "start": { + "line": selected_range_text["selectedRange"][0], + "character": selected_range_text["selectedRange"][1], + }, + "end": { + "line": selected_range_text["selectedRange"][2], + "character": selected_range_text["selectedRange"][3], + }, + } + ) \ No newline at end of file From 2e998972f60dfb3cc389fe6fc51db32f5da0d854 Mon Sep 17 00:00:00 2001 From: "bobo.yang" Date: Fri, 9 Feb 2024 13:53:00 +0800 Subject: [PATCH 2/2] Disable pylint warnings and fix formatting in IDE service files --- devchat/ide/rpc.py | 1 + devchat/ide/service.py | 12 +++++------- devchat/ide/vscode_services.py | 2 +- 3 files changed, 7 insertions(+), 8 deletions(-) diff --git a/devchat/ide/rpc.py b/devchat/ide/rpc.py index 480e5df7..b8f56cc0 100644 --- a/devchat/ide/rpc.py +++ b/devchat/ide/rpc.py @@ -2,6 +2,7 @@ # pylint: disable=W3101 # pylint: disable=W0719 # pylint: disable=R1710 +# pylint: disable=W0212 import os from functools import wraps diff --git a/devchat/ide/service.py b/devchat/ide/service.py index e622f198..8d3b887a 100644 --- a/devchat/ide/service.py +++ b/devchat/ide/service.py @@ -2,7 +2,6 @@ # pylint: disable=W0613 # pylint: disable=E1133 # pylint: disable=R1710 -# pylint: disable=W0212 # pylint: disable=W0719 # pylint: disable=W3101 # pylint: disable=C0103 @@ -40,7 +39,8 @@ def get_lsp_brige_port(self) -> str: @rpc_method def install_python_env(self, command_name: str, requirements_file: str) -> str: """ - A method to install a Python environment with the provided command name and requirements file, returning a string result. + A method to install a Python environment with the provided command name + and requirements file, returning a string result. """ return self._result @@ -105,7 +105,7 @@ def ide_name(self) -> str: The name of the IDE as a string. """ return self._result - + @rpc_method def diff_apply(self, filepath, content) -> bool: """ @@ -135,8 +135,7 @@ def get_visible_range(self): """ if self.ide_name() == "vscode": return visible_range() - else: - return IdeaIDEService().get_visible_range() + return IdeaIDEService().get_visible_range() def get_selected_range(self): """ @@ -148,5 +147,4 @@ def get_selected_range(self): """ if self.ide_name() == "vscode": return selected_range() - else: - return IdeaIDEService().get_selected_range() + return IdeaIDEService().get_selected_range() diff --git a/devchat/ide/vscode_services.py b/devchat/ide/vscode_services.py index b8021e56..838a7a22 100644 --- a/devchat/ide/vscode_services.py +++ b/devchat/ide/vscode_services.py @@ -174,4 +174,4 @@ def selected_range() -> LocationWithText: "character": selected_range_text["selectedRange"][3], }, } - ) \ No newline at end of file + )