From ce9a4ef195b6e1e53bb97e0c3c736d9dd6ebd256 Mon Sep 17 00:00:00 2001 From: Dominik Jain Date: Wed, 9 Jul 2025 23:35:32 +0200 Subject: [PATCH] Add retrieve_content_around_line to Project (keeping LS version, too, for the time being), such that FindReferencingSymbolsTool is now also implemented without a direct reference to the language server All references to Component.language_server have been removed --- src/serena/project.py | 22 +++++++++++ src/serena/tools/symbol_tools.py | 2 +- src/serena/tools/tools_base.py | 9 +---- src/solidlsp/ls.py | 20 ---------- test/solidlsp/clojure/test_clojure_basic.py | 41 +++++++++++---------- test/solidlsp/python/test_python_basic.py | 24 ++++++------ 6 files changed, 59 insertions(+), 59 deletions(-) diff --git a/src/serena/project.py b/src/serena/project.py index 97edbde..fd4813a 100644 --- a/src/serena/project.py +++ b/src/serena/project.py @@ -177,3 +177,25 @@ class Project: paths_include_glob=paths_include_glob, paths_exclude_glob=paths_exclude_glob, ) + + def retrieve_content_around_line( + self, relative_file_path: str, line: int, context_lines_before: int = 0, context_lines_after: int = 0 + ) -> MatchedConsecutiveLines: + """ + Retrieve the content of the given file around the given line. + + :param relative_file_path: The relative path of the file to retrieve the content from + :param line: The line number to retrieve the content around + :param context_lines_before: The number of lines to retrieve before the given line + :param context_lines_after: The number of lines to retrieve after the given line + + :return MatchedConsecutiveLines: A container with the desired lines. + """ + file_contents = self.read_file(relative_file_path) + return MatchedConsecutiveLines.from_file_contents( + file_contents, + line=line, + context_lines_before=context_lines_before, + context_lines_after=context_lines_after, + source_file_path=relative_file_path, + ) diff --git a/src/serena/tools/symbol_tools.py b/src/serena/tools/symbol_tools.py index e513832..792984d 100644 --- a/src/serena/tools/symbol_tools.py +++ b/src/serena/tools/symbol_tools.py @@ -189,7 +189,7 @@ class FindReferencingSymbolsTool(Tool): if not include_body: ref_relative_path = ref.symbol.location.relative_path assert ref_relative_path is not None, f"Referencing symbol {ref.symbol.name} has no relative path, this is likely a bug." - content_around_ref = self.language_server.retrieve_content_around_line( + content_around_ref = self.project.retrieve_content_around_line( relative_file_path=ref_relative_path, line=ref.line, context_lines_before=1, context_lines_after=1 ) ref_dict["content_around_reference"] = content_around_ref.to_display_string() diff --git a/src/serena/tools/tools_base.py b/src/serena/tools/tools_base.py index a4d087e..074119c 100644 --- a/src/serena/tools/tools_base.py +++ b/src/serena/tools/tools_base.py @@ -16,7 +16,6 @@ from serena.prompt_factory import PromptFactory from serena.symbol import SymbolManager from serena.util.class_decorators import singleton from serena.util.inspection import iter_subclasses -from solidlsp import SolidLanguageServer if TYPE_CHECKING: from serena.agent import LinesRead, MemoriesManager, SerenaAgent @@ -31,11 +30,6 @@ class Component(ABC): def __init__(self, agent: "SerenaAgent"): self.agent = agent - @property - def language_server(self) -> SolidLanguageServer: - assert self.agent.language_server is not None - return self.agent.language_server - def get_project_root(self) -> str: """ :return: the root directory of the active project, raises a ValueError if no active project configuration is set @@ -275,7 +269,8 @@ class Tool(Component, ToolInterface): log.info(f"Result: {result}") try: - self.language_server.save_cache() + if self.agent.language_server is not None: + self.agent.language_server.save_cache() except Exception as e: log.error(f"Error saving language server cache: {e}") diff --git a/src/solidlsp/ls.py b/src/solidlsp/ls.py index 5a3aa48..f80ef67 100644 --- a/src/solidlsp/ls.py +++ b/src/solidlsp/ls.py @@ -709,26 +709,6 @@ class SolidLanguageServer(ABC): return ret - def request_references_with_content( - self, relative_file_path: str, line: int, column: int, context_lines_before: int = 0, context_lines_after: int = 0 - ) -> list[MatchedConsecutiveLines]: - """ - Like request_references, but returns the content of the lines containing the references, not just the locations. - - :param relative_file_path: The relative path of the file that has the symbol for which references should be looked up - :param line: The line number of the symbol - :param column: The column number of the symbol - :param context_lines_before: The number of lines to include in the context before the line containing the reference - :param context_lines_after: The number of lines to include in the context after the line containing the reference - - :return: A list of MatchedConsecutiveLines objects, one for each reference. - """ - references = self.request_references(relative_file_path, line, column) - return [ - self.retrieve_content_around_line(ref["relativePath"], ref["range"]["start"]["line"], context_lines_before, context_lines_after) - for ref in references - ] - def retrieve_full_file_content(self, file_path: str) -> str: """ Retrieve the full content of the given file. diff --git a/test/solidlsp/clojure/test_clojure_basic.py b/test/solidlsp/clojure/test_clojure_basic.py index 28c2cd3..dc10b88 100644 --- a/test/solidlsp/clojure/test_clojure_basic.py +++ b/test/solidlsp/clojure/test_clojure_basic.py @@ -91,24 +91,6 @@ class TestLanguageServerBasics: symbol_names = [symbol["name"] for symbol in result] assert any("add" in name.lower() for name in symbol_names), f"Should find 'add' function in symbols: {symbol_names}" - @pytest.mark.parametrize("language_server", [Language.CLOJURE], indirect=True) - def test_retrieve_content_around_line(self, language_server: SolidLanguageServer): - """Test retrieving content around specific lines""" - # Test retrieving content around the greet function definition (line 2) - result = language_server.retrieve_content_around_line(CORE_PATH, 2, 2) - - assert result is not None, "Should retrieve content around line 2" - content_str = result.to_display_string() - assert "greet" in content_str, "Should contain the greet function definition" - assert "defn" in content_str, "Should contain defn keyword" - - # Test retrieving content around multiply function (around line 13) - result = language_server.retrieve_content_around_line(CORE_PATH, 13, 1) - - assert result is not None, "Should retrieve content around line 13" - content_str = result.to_display_string() - assert "multiply" in content_str, "Should contain multiply function" - @pytest.mark.parametrize("language_server", [Language.CLOJURE], indirect=True) def test_namespace_functions(self, language_server: SolidLanguageServer): """Test definition lookup for core/greet usage in utils.clj""" @@ -124,7 +106,10 @@ class TestLanguageServerBasics: @pytest.mark.parametrize("language_server", [Language.CLOJURE], indirect=True) def test_request_references_with_content(self, language_server: SolidLanguageServer): """Test references to multiply function with content""" - result = language_server.request_references_with_content(CORE_PATH, 12, 6, 3) + references = language_server.request_references(CORE_PATH, 12, 6) + result = [ + language_server.retrieve_content_around_line(ref1["relativePath"], ref1["range"]["start"]["line"], 3, 0) for ref1 in references + ] assert result is not None, "Should find references with content" assert isinstance(result, list) @@ -202,6 +187,24 @@ class TestLanguageServerBasics: class TestProjectBasics: + @pytest.mark.parametrize("project", [Language.CLOJURE], indirect=True) + def test_retrieve_content_around_line(self, project: Project): + """Test retrieving content around specific lines""" + # Test retrieving content around the greet function definition (line 2) + result = project.retrieve_content_around_line(CORE_PATH, 2, 2) + + assert result is not None, "Should retrieve content around line 2" + content_str = result.to_display_string() + assert "greet" in content_str, "Should contain the greet function definition" + assert "defn" in content_str, "Should contain defn keyword" + + # Test retrieving content around multiply function (around line 13) + result = project.retrieve_content_around_line(CORE_PATH, 13, 1) + + assert result is not None, "Should retrieve content around line 13" + content_str = result.to_display_string() + assert "multiply" in content_str, "Should contain multiply function" + @pytest.mark.parametrize("project", [Language.CLOJURE], indirect=True) def test_search_files_for_pattern(self, project: Project) -> None: result = project.search_source_files_for_pattern("defn.*greet") diff --git a/test/solidlsp/python/test_python_basic.py b/test/solidlsp/python/test_python_basic.py index d19c380..dbfd8ce 100644 --- a/test/solidlsp/python/test_python_basic.py +++ b/test/solidlsp/python/test_python_basic.py @@ -79,20 +79,22 @@ class TestLanguageServerBasics: references = language_server.request_references(file_path, sel_start["line"], sel_start["character"]) assert len(references) > 1, "Should get valid references for create_user (using selectionRange if present)" - @pytest.mark.parametrize("language_server", [Language.PYTHON], indirect=True) - def test_retrieve_content_around_line(self, language_server: SolidLanguageServer) -> None: + +class TestProjectBasics: + @pytest.mark.parametrize("project", [Language.PYTHON], indirect=True) + def test_retrieve_content_around_line(self, project: Project) -> None: """Test retrieve_content_around_line functionality with various scenarios.""" file_path = os.path.join("test_repo", "models.py") # Scenario 1: Just a single line (User class definition) - line_31 = language_server.retrieve_content_around_line(file_path, 31) + line_31 = project.retrieve_content_around_line(file_path, 31) assert len(line_31.lines) == 1 assert "class User(BaseModel):" in line_31.lines[0].line_content assert line_31.lines[0].line_number == 31 assert line_31.lines[0].match_type == LineType.MATCH # Scenario 2: Context above and below - with_context_around_user = language_server.retrieve_content_around_line(file_path, 31, 2, 2) + with_context_around_user = project.retrieve_content_around_line(file_path, 31, 2, 2) assert len(with_context_around_user.lines) == 5 # Check line content assert "class User(BaseModel):" in with_context_around_user.matched_lines[0].line_content @@ -112,7 +114,7 @@ class TestLanguageServerBasics: assert with_context_around_user.lines[4].match_type == LineType.AFTER_MATCH # Scenario 3a: Only context above - with_context_above = language_server.retrieve_content_around_line(file_path, 31, 3, 0) + with_context_above = project.retrieve_content_around_line(file_path, 31, 3, 0) assert len(with_context_above.lines) == 4 assert "return cls(id=id, name=name)" in with_context_above.lines[0].line_content assert "class User(BaseModel):" in with_context_above.matched_lines[0].line_content @@ -129,7 +131,7 @@ class TestLanguageServerBasics: assert with_context_above.lines[3].match_type == LineType.MATCH # Scenario 3b: Only context below - with_context_below = language_server.retrieve_content_around_line(file_path, 31, 0, 3) + with_context_below = project.retrieve_content_around_line(file_path, 31, 0, 3) assert len(with_context_below.lines) == 4 assert "class User(BaseModel):" in with_context_below.matched_lines[0].line_content assert with_context_below.num_matched_lines == 1 @@ -144,7 +146,7 @@ class TestLanguageServerBasics: assert with_context_below.lines[3].match_type == LineType.AFTER_MATCH # Scenario 4a: Edge case - context above but line is at 0 - first_line_with_context_around = language_server.retrieve_content_around_line(file_path, 0, 2, 1) + first_line_with_context_around = project.retrieve_content_around_line(file_path, 0, 2, 1) assert len(first_line_with_context_around.lines) <= 4 # Should have at most 4 lines (line 0 + 1 below + up to 2 above) assert first_line_with_context_around.lines[0].line_number <= 2 # First line should be at most line 2 # Check match type for the target line @@ -157,7 +159,7 @@ class TestLanguageServerBasics: assert line.match_type == LineType.AFTER_MATCH # Scenario 4b: Edge case - context above but line is at 1 - second_line_with_context_above = language_server.retrieve_content_around_line(file_path, 1, 3, 1) + second_line_with_context_above = project.retrieve_content_around_line(file_path, 1, 3, 1) assert len(second_line_with_context_above.lines) <= 5 # Should have at most 5 lines (line 1 + 1 below + up to 3 above) assert second_line_with_context_above.lines[0].line_number <= 1 # First line should be at most line 1 # Check match type for the target line @@ -171,10 +173,10 @@ class TestLanguageServerBasics: # Scenario 4c: Edge case - context below but line is at the end of file # First get the total number of lines in the file - all_content = language_server.retrieve_full_file_content(file_path) + all_content = project.read_file(file_path) total_lines = len(all_content.split("\n")) - last_line_with_context_around = language_server.retrieve_content_around_line(file_path, total_lines - 1, 1, 3) + last_line_with_context_around = project.retrieve_content_around_line(file_path, total_lines - 1, 1, 3) assert len(last_line_with_context_around.lines) <= 5 # Should have at most 5 lines (last line + 1 above + up to 3 below) assert last_line_with_context_around.lines[-1].line_number >= total_lines - 4 # Last line should be at least total_lines - 4 # Check match type for the target line @@ -186,8 +188,6 @@ class TestLanguageServerBasics: else: assert line.match_type == LineType.AFTER_MATCH - -class TestProjectBasics: @pytest.mark.parametrize("project", [Language.PYTHON], indirect=True) def test_search_files_for_pattern(self, project: Project) -> None: """Test search_files_for_pattern with various patterns and glob filters."""