mirror of
https://github.com/tiennm99/serena.git
synced 2026-10-16 12:15:40 +00:00
Merge pull request #1702 from oraios/jb-read-ext-file
JetBrains: External file processing
This commit is contained in:
6 files changed
+101
-27
No files matched your search
@@ -10,6 +10,12 @@ Status of the `main` branch. Changes prior to the next official version change w
|
||||
the `line_ending` setting is meant to be the single point of line-ending translation on write. The
|
||||
fallback now normalizes line endings to LF, consistently with the primary path.
|
||||
|
||||
* JetBrains:
|
||||
- Allow external files from dependencies (specified via references like "<ext:FileUtil.class|472e0a13>") to be
|
||||
- read via `ReadFileTool`
|
||||
- searched via `SearchForPatternTool`
|
||||
when using plugin version 2023.3.3+
|
||||
|
||||
# v1.6.0 (2026-07-16)
|
||||
|
||||
* General:
|
||||
|
||||
@@ -734,6 +734,16 @@ class JetBrainsPluginClient(ToStringMixin):
|
||||
request_data["groupPathContains"] = group_path_contains
|
||||
return cast(jb.ListInspectionsResponse, self._make_request("POST", "/listInspections", request_data))
|
||||
|
||||
def read_file(self, relative_path: str) -> str:
|
||||
self._require_version_at_least(2023, 3, 3)
|
||||
request_data = {
|
||||
"relativePath": relative_path,
|
||||
}
|
||||
response = self._make_request("POST", "/readFile", request_data)
|
||||
if "content" not in response:
|
||||
raise PluginServerError(f"Unexpected response from readFile: {response}")
|
||||
return response["content"]
|
||||
|
||||
def close(self) -> None:
|
||||
self._session.close()
|
||||
|
||||
|
||||
@@ -6,6 +6,14 @@ Prefix used for in relative paths of symbols that are from external libraries (i
|
||||
"""
|
||||
|
||||
|
||||
def is_external_path(relative_path: str):
|
||||
"""
|
||||
:param relative_path: a relative path (e.g., from a symbol's `relative_path` field)
|
||||
:return: whether the path is an external path (i.e., from a library, not the user's codebase)
|
||||
"""
|
||||
return relative_path.startswith(JB_EXTERNAL_FILE_PREFIX)
|
||||
|
||||
|
||||
class PluginStatusDTO(TypedDict):
|
||||
project_root: str
|
||||
plugin_version: str
|
||||
|
||||
+37
-23
@@ -16,12 +16,11 @@ from serena.config.serena_config import (
|
||||
)
|
||||
from serena.ls_manager import LanguageServerFactory, LanguageServerManager
|
||||
from serena.memories.memory_manager import MemoryManager
|
||||
from serena.util.file_proxy import FileCollection
|
||||
from serena.util.file_proxy import FileCollection, FileProxy
|
||||
from serena.util.file_system import GitignoreParser, match_path, scan_directory
|
||||
from serena.util.text_utils import MatchedConsecutiveLines, search_files
|
||||
from solidlsp import SolidLanguageServer
|
||||
from solidlsp.ls_config import Language
|
||||
from solidlsp.ls_utils import FileUtils
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from serena.agent import SerenaAgent
|
||||
@@ -169,13 +168,13 @@ class Project(ToStringMixin):
|
||||
|
||||
def read_file(self, relative_path: str) -> str:
|
||||
"""
|
||||
Reads a file relative to the project root.
|
||||
Reads a project file.
|
||||
|
||||
:param relative_path: the path to the file relative to the project root
|
||||
:param relative_path: the path to the file relative to the project root or an external
|
||||
path token like "<ext:FileUtil.class|472e0a13>"
|
||||
:return: the content of the file
|
||||
"""
|
||||
abs_path = Path(self.project_root) / relative_path
|
||||
return FileUtils.read_file(str(abs_path), self.project_config.encoding)
|
||||
return FileProxy.from_project_relative_path(self, relative_path).get_contents()
|
||||
|
||||
@property
|
||||
def _ignore_spec(self) -> pathspec.PathSpec:
|
||||
@@ -316,6 +315,9 @@ class Project(ToStringMixin):
|
||||
:param relative_path: the path to validate, relative to the project root
|
||||
:param require_not_ignored: if True, the path must not be ignored according to the project's ignore settings
|
||||
"""
|
||||
if FileProxy.is_external_path(relative_path):
|
||||
return
|
||||
|
||||
if not self.is_path_in_project(relative_path):
|
||||
raise ValueError(f"{relative_path=} points outside the project root ({self.project_root})")
|
||||
|
||||
@@ -360,6 +362,34 @@ class Project(ToStringMixin):
|
||||
)
|
||||
return rel_file_paths
|
||||
|
||||
def _create_file_collection(self, relative_path: str, code_files_only: bool) -> FileCollection:
|
||||
if FileProxy.is_external_path(relative_path):
|
||||
# single external path: create appropriate proxy
|
||||
file_collection = FileCollection([FileProxy.from_project_relative_path(self, relative_path)])
|
||||
else:
|
||||
# path is a local project path
|
||||
abs_path = os.path.join(self.project_root, relative_path)
|
||||
if not os.path.exists(abs_path):
|
||||
raise FileNotFoundError(f"Relative path {relative_path} does not exist.")
|
||||
|
||||
if code_files_only:
|
||||
relative_file_paths = self.gather_source_files(relative_path=relative_path)
|
||||
file_collection = FileCollection.from_local_project_paths(relative_file_paths, self)
|
||||
else:
|
||||
abs_path = os.path.join(self.project_root, relative_path)
|
||||
if os.path.isfile(abs_path):
|
||||
rel_paths_to_search = [relative_path]
|
||||
else:
|
||||
_dirs, rel_paths_to_search = scan_directory(
|
||||
path=abs_path,
|
||||
recursive=True,
|
||||
is_ignored_dir=self.is_ignored_path,
|
||||
is_ignored_file=self.is_ignored_path,
|
||||
relative_to=self.project_root,
|
||||
)
|
||||
file_collection = FileCollection.from_local_project_paths(rel_paths_to_search, self)
|
||||
return file_collection
|
||||
|
||||
def search_project_files_for_pattern(
|
||||
self,
|
||||
pattern: str,
|
||||
@@ -383,23 +413,7 @@ class Project(ToStringMixin):
|
||||
:param multiline: Whether to compile the regex with the DOTALL flag (``.`` matches newlines).
|
||||
:return: List of matched consecutive lines with context
|
||||
"""
|
||||
if code_files_only:
|
||||
relative_file_paths = self.gather_source_files(relative_path=relative_path)
|
||||
file_collection = FileCollection.from_local_project_paths(relative_file_paths, self)
|
||||
else:
|
||||
abs_path = os.path.join(self.project_root, relative_path)
|
||||
if os.path.isfile(abs_path):
|
||||
rel_paths_to_search = [relative_path]
|
||||
else:
|
||||
_dirs, rel_paths_to_search = scan_directory(
|
||||
path=abs_path,
|
||||
recursive=True,
|
||||
is_ignored_dir=self.is_ignored_path,
|
||||
is_ignored_file=self.is_ignored_path,
|
||||
relative_to=self.project_root,
|
||||
)
|
||||
file_collection = FileCollection.from_local_project_paths(rel_paths_to_search, self)
|
||||
|
||||
file_collection = self._create_file_collection(relative_path, code_files_only)
|
||||
return search_files(
|
||||
file_collection,
|
||||
pattern,
|
||||
|
||||
@@ -593,10 +593,6 @@ class SearchForPatternTool(Tool):
|
||||
if relative_path:
|
||||
self.project.validate_relative_path(relative_path, require_not_ignored=True)
|
||||
|
||||
abs_path = os.path.join(self.get_project_root(), relative_path)
|
||||
if not os.path.exists(abs_path):
|
||||
raise FileNotFoundError(f"Relative path {relative_path} does not exist.")
|
||||
|
||||
matches = self.project.search_project_files_for_pattern(
|
||||
pattern=substring_pattern,
|
||||
relative_path=relative_path,
|
||||
|
||||
@@ -4,6 +4,8 @@ from abc import ABC, abstractmethod
|
||||
from collections.abc import Iterator
|
||||
from typing import TYPE_CHECKING, Self
|
||||
|
||||
from serena.jetbrains import jetbrains_types as jb
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from serena.project import Project
|
||||
|
||||
@@ -25,6 +27,21 @@ class FileProxy(ABC):
|
||||
:return: whether the proxy supports glob filtering based on its relative path
|
||||
"""
|
||||
|
||||
@staticmethod
|
||||
def is_external_path(relative_path: str) -> bool:
|
||||
"""
|
||||
:return: whether the given relative path is an encoded external path (not a local project file)
|
||||
"""
|
||||
# This is intended to be extended once we also support external paths in other backends
|
||||
return jb.is_external_path(relative_path)
|
||||
|
||||
@classmethod
|
||||
def from_project_relative_path(cls, project: "Project", relative_path: str) -> "FileProxy":
|
||||
if cls.is_external_path(relative_path):
|
||||
if project.language_backend.is_jetbrains():
|
||||
return JetBrainsFileProxy(relative_path, project)
|
||||
return LocalProjectFileProxy(relative_path, project)
|
||||
|
||||
|
||||
class LocalProjectFileProxy(FileProxy):
|
||||
def __init__(self, relative_path: str, project: "Project"):
|
||||
@@ -43,6 +60,29 @@ class LocalProjectFileProxy(FileProxy):
|
||||
return True
|
||||
|
||||
|
||||
class JetBrainsFileProxy(FileProxy):
|
||||
"""
|
||||
Retrieves the contents of a file from the JetBrains plugin via the plugin client, given its relative path,
|
||||
which may be an external path (e.g., "<ext:FileUtil.class|472e0a13>")
|
||||
"""
|
||||
|
||||
def __init__(self, relative_path: str, project: "Project"):
|
||||
self._relative_path = relative_path
|
||||
self._project = project
|
||||
|
||||
def get_contents(self) -> str:
|
||||
from serena.jetbrains.jetbrains_plugin_client import JetBrainsPluginClient
|
||||
|
||||
client = JetBrainsPluginClient.from_project(self._project)
|
||||
return client.read_file(self._relative_path)
|
||||
|
||||
def get_relative_path(self) -> str:
|
||||
return self._relative_path
|
||||
|
||||
def is_glob_supported(self):
|
||||
return False
|
||||
|
||||
|
||||
class FileCollection:
|
||||
def __init__(self, file_proxies: list[FileProxy]):
|
||||
self._file_proxies = file_proxies
|
||||
|
||||
Reference in new issue
Block a user