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
This commit is contained in:
Dominik Jain authored and Dominik Jain committed 2025-07-09 23:38:20 +02:00
1 parent 0057cbef4e
commit ce9a4ef195
6 files changed
+59 -59

No files matched your search

+22
View File
@@ -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,
)
+1 -1
View File
@@ -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()
+2 -7
View File
@@ -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}")
-20
View File
@@ -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.
+22 -19
View File
@@ -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")
+12 -12
View File
@@ -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."""