mirror of
https://github.com/tiennm99/serena.git
synced 2026-10-13 06:15:43 +00:00
JetBrains: Allow external files from dependencies (e.g. "<ext:FileUtil.class|472e0a13>") to be
- read via `ReadFileTool` - searched via `SearchForPatternTool` when using plugin version 2023.3.3+ (yet unreleased) This is enabled via JetBrainsFileProxy (which reads the file through the IDE)
This commit is contained in:
1 parent
f1486f7d60
commit
f3cf5d3f8d
5 files changed
+91
-27
No files matched your search
@@ -2,6 +2,12 @@
|
||||
|
||||
Status of the `main` branch. Changes prior to the next official version change will appear here.
|
||||
|
||||
* 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:
|
||||
|
||||
@@ -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