Merge pull request #1228 from oraios/prepare_find_implementation_tool

Prepare find implementation tool
This commit is contained in:
Dr. Dominik Jain authored and GitHub committed 2026-03-28 14:27:12 +01:00
commit 926640f463
5 files changed
+520 -177

No files matched your search

+32
View File
@@ -523,3 +523,35 @@ class ContentReplacer:
"Please revise the expression to be more specific or enable allow_multiple_occurrences if this is expected."
)
return updated_content
@dataclass
class TextCoords:
line: int
"""
0-based line number
"""
col: int
"""
0-based column number
"""
def find_text_coordinates(content: str, regex: str) -> TextCoords | None:
"""
Finds the line and column number of the first match of a regex pattern in the given content.
:param content: the text content to search through
:param regex: the regular expression pattern to search for; it must match part of a single line,
and contain exactly one group that captures the position of interest (e.g., the exact variable name to find the coordinates of)
:return: a tuple of (line_number, column_number) for the first match found, both 0-based, or None if no match is found
"""
pattern = re.compile(regex)
for line_number, line in enumerate(content.splitlines()):
match = pattern.search(line)
if match:
if len(match.groups()) != 1:
raise ValueError(f"Regex must contain exactly one group to capture the position, but found {len(match.groups())} groups.")
column_number = match.start(1)
return TextCoords(line_number, column_number)
return None
@@ -21,7 +21,7 @@ from solidlsp.language_servers.typescript_language_server import (
prefer_non_node_modules_definition,
)
from solidlsp.ls import LSPFileBuffer, SolidLanguageServer
from solidlsp.ls_config import Language, LanguageServerConfig
from solidlsp.ls_config import FilenameMatcher, Language, LanguageServerConfig
from solidlsp.ls_exceptions import SolidLSPException
from solidlsp.ls_types import Location
from solidlsp.ls_utils import PathUtils
@@ -47,6 +47,11 @@ class VueTypeScriptServer(TypeScriptLanguageServer):
"""
return Language.TYPESCRIPT
def get_source_fn_matcher(self) -> FilenameMatcher:
# must override with Vue-specific matcher to ensure .vue files are included (as they can be discovered via references,
# for instance; otherwise, we may find references in .vue files but then filter the results out, because .vue files are ignored.)
return Language.VUE.get_source_fn_matcher()
class DependencyProvider(TypeScriptLanguageServer.DependencyProvider):
override_ts_ls_executable: str | None = None
+464 -166
View File
@@ -23,7 +23,7 @@ from sensai.util.string import ToStringMixin
from serena.util.file_system import match_path
from serena.util.text_utils import MatchedConsecutiveLines
from solidlsp import ls_types
from solidlsp.ls_config import Language, LanguageServerConfig
from solidlsp.ls_config import FilenameMatcher, Language, LanguageServerConfig
from solidlsp.ls_exceptions import SolidLSPException
from solidlsp.ls_process import LanguageServerProcess
from solidlsp.ls_types import UnifiedSymbolInformation
@@ -35,6 +35,7 @@ from solidlsp.lsp_protocol_handler.lsp_types import (
Definition,
DefinitionParams,
DocumentSymbol,
ImplementationParams,
LocationLink,
RenameParams,
SymbolInformation,
@@ -378,6 +379,13 @@ class SolidLanguageServer(ABC):
def get_language_enum_instance(cls) -> Language:
return Language.from_ls_class(cls)
@classmethod
def supports_implementation_request(cls) -> bool:
"""
Return whether this language server supports ``textDocument/implementation``.
"""
return False
@classmethod
def ls_resources_dir(cls, solidlsp_settings: SolidLSPSettings, mkdir: bool = True) -> str:
"""
@@ -575,6 +583,14 @@ class SolidLanguageServer(ABC):
"""
return self._ignore_spec
def get_source_fn_matcher(self) -> FilenameMatcher:
"""
:return: the source filename matcher for this language server, which must positively match all files that
are understood by this language server or are discovered as containing sources indirectly, e.g. via references
"""
# By default, use the matcher of the language
return self.language.get_source_fn_matcher()
def is_ignored_path(self, relative_path: str, ignore_unsupported_files: bool = True) -> bool:
"""
Determine if a path should be ignored based on file type
@@ -592,7 +608,7 @@ class SolidLanguageServer(ABC):
# Check file extension if it's a file
is_file = os.path.isfile(abs_path)
if is_file and ignore_unsupported_files:
fn_matcher = self.language.get_source_fn_matcher()
fn_matcher = self.get_source_fn_matcher()
if not fn_matcher.is_relevant_filename(abs_path):
return True
@@ -846,6 +862,149 @@ class SolidLanguageServer(ABC):
def _send_definition_request(self, definition_params: DefinitionParams) -> Definition | list[LocationLink] | None:
return self.server.send.definition(definition_params)
class SymbolLocationRequest(ABC):
def __init__(
self,
language_server: "SolidLanguageServer",
relative_file_path: str,
line: int,
column: int,
*,
request_name: str,
) -> None:
self.language_server = language_server
self.relative_file_path = relative_file_path
self.line = line
self.column = column
self.request_name = request_name
self.skip_ignored_paths = True
def execute(self) -> list[ls_types.Location]:
self._ensure_server_started()
t0 = perf_counter() if _debug_enabled else None
with self.language_server.open_file(self.relative_file_path):
self.language_server._wait_for_cross_file_references_if_needed()
try:
response = self.send_request()
except Exception as e:
mapped_exception = self.map_exception(e)
if mapped_exception is not None:
raise mapped_exception from e
raise
result = self.normalize_response(response)
if t0 is not None:
self.log_perf_result(t0, result)
return result
def _ensure_server_started(self) -> None:
if not self.language_server.server_started:
log.error("%s called before language server started", self.request_name)
raise SolidLSPException("Language Server not started")
@abstractmethod
def send_request(self) -> object | None:
pass
def map_exception(self, error: Exception) -> Exception | None:
if isinstance(error, LSPError) and getattr(error, "code", None) == -32603:
return RuntimeError(
f"LSP internal error (-32603) when requesting {self.request_name} for "
f"{self.relative_file_path}:{self.line}:{self.column}. "
"This often occurs when requesting a symbol in a way the language server cannot resolve."
)
return None
@abstractmethod
def normalize_response(self, response: object | None) -> list[ls_types.Location]:
pass
def convert_location_item(self, item: dict[str, object], *, allow_location_links: bool = False) -> ls_types.Location | None:
if LSPConstants.URI in item and LSPConstants.RANGE in item:
uri = cast(str, item[LSPConstants.URI])
range_d = cast(ls_types.Range, item[LSPConstants.RANGE])
elif (
allow_location_links
and LSPConstants.TARGET_URI in item
and LSPConstants.TARGET_RANGE in item
and LSPConstants.TARGET_SELECTION_RANGE in item
):
uri = cast(str, item[LSPConstants.TARGET_URI])
range_d = cast(ls_types.Range, item[LSPConstants.TARGET_SELECTION_RANGE])
else:
raise AssertionError(f"Unexpected response from Language Server: {item}")
abs_path = PathUtils.uri_to_path(uri)
rel_path_str = PathUtils.get_relative_path(abs_path, self.language_server.repository_root_path)
if rel_path_str is None:
log.warning(
"Found a %s in a path outside the repository, probably the LS is parsing things in installed packages or in the standardlib! "
"Path: %s. This is a bug but we currently simply skip these locations.",
self.request_name,
abs_path,
)
return None
if self.skip_ignored_paths and self.language_server.is_ignored_path(rel_path_str):
log.info("%s found symbol in ignored path: %s", self.request_name, rel_path_str)
return None
return ls_types.Location(uri=uri, range=range_d, absolutePath=str(abs_path), relativePath=rel_path_str)
def log_perf_result(self, t0: float, result: list[ls_types.Location]) -> None:
return
class DefinitionLocationRequest(SymbolLocationRequest):
def __init__(
self,
language_server: "SolidLanguageServer",
relative_file_path: str,
line: int,
column: int,
*,
request_name: str = "request_definition",
) -> None:
super().__init__(
language_server,
relative_file_path,
line,
column,
request_name=request_name,
)
def send_request(self) -> object | None:
return self.language_server._send_definition_request(
self.language_server._create_text_document_position_params(self.relative_file_path, self.line, self.column)
)
def normalize_response(self, response: object | None) -> list[ls_types.Location]:
if response is None:
log.warning(
"Language server returned None for %s request at %s:%s:%s",
self.request_name,
self.relative_file_path,
self.line,
self.column,
)
return []
ret: list[ls_types.Location] = []
if isinstance(response, list):
for item in response:
assert isinstance(item, dict), f"Unexpected response from Language Server (expected dict, got {type(item)}): {item}"
if location := self.convert_location_item(cast(dict[str, object], item), allow_location_links=True):
ret.append(location)
return ret
if isinstance(response, dict):
if location := self.convert_location_item(cast(dict[str, object], response), allow_location_links=True):
ret.append(location)
return ret
assert False, f"Unexpected response from Language Server: {response}"
def request_definition(self, relative_file_path: str, line: int, column: int) -> list[ls_types.Location]:
"""
Raise a [textDocument/definition](https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#textDocument_definition) request to the Language Server
@@ -857,70 +1016,60 @@ class SolidLanguageServer(ABC):
:return: the list of locations where the symbol is defined
"""
if not self.server_started:
log.error("request_definition called before language server started")
raise SolidLSPException("Language Server not started")
request = self.DefinitionLocationRequest(self, relative_file_path, line, column)
return request.execute()
def _send_implementation_request(self, implementation_params: ImplementationParams) -> Definition | list[LocationLink] | None:
return self.server.send.implementation(implementation_params)
def _create_text_document_position_params(self, relative_file_path: str, line: int, column: int) -> DefinitionParams:
return cast(
DefinitionParams,
{
LSPConstants.TEXT_DOCUMENT: {
LSPConstants.URI: pathlib.Path(str(PurePath(self.repository_root_path, relative_file_path))).as_uri()
},
LSPConstants.POSITION: {
LSPConstants.LINE: line,
LSPConstants.CHARACTER: column,
},
},
)
def _wait_for_cross_file_references_if_needed(self) -> None:
if not self._has_waited_for_cross_file_references:
# Some LS require waiting for a while before they can return cross-file definitions.
# This is a workaround for such LS that don't have a reliable "finished initializing" signal.
# Some LS require waiting for a while before they can return accurate cross-file results.
# The waiting has to happen after at least one file was opened in the LS.
sleep(self._get_wait_time_for_cross_file_referencing())
self._has_waited_for_cross_file_references = True
with self.open_file(relative_file_path):
# sending request to the language server and waiting for response
definition_params = cast(
DefinitionParams,
{
LSPConstants.TEXT_DOCUMENT: {
LSPConstants.URI: pathlib.Path(str(PurePath(self.repository_root_path, relative_file_path))).as_uri()
},
LSPConstants.POSITION: {
LSPConstants.LINE: line,
LSPConstants.CHARACTER: column,
},
},
class ImplementationLocationRequest(DefinitionLocationRequest):
def __init__(self, language_server: "SolidLanguageServer", relative_file_path: str, line: int, column: int) -> None:
super().__init__(
language_server,
relative_file_path,
line,
column,
request_name="request_implementation",
)
response = self._send_definition_request(definition_params)
ret: list[ls_types.Location] = []
if isinstance(response, list):
# response is either of type Location[] or LocationLink[]
for item in response:
assert isinstance(item, dict)
if LSPConstants.URI in item and LSPConstants.RANGE in item:
new_item: dict = {}
new_item.update(item)
new_item["absolutePath"] = PathUtils.uri_to_path(new_item["uri"])
new_item["relativePath"] = PathUtils.get_relative_path(new_item["absolutePath"], self.repository_root_path)
ret.append(ls_types.Location(**new_item)) # type: ignore
elif LSPConstants.TARGET_URI in item and LSPConstants.TARGET_RANGE in item and LSPConstants.TARGET_SELECTION_RANGE in item:
new_item: dict = {} # type: ignore
new_item["uri"] = item[LSPConstants.TARGET_URI] # type: ignore
new_item["absolutePath"] = PathUtils.uri_to_path(new_item["uri"])
new_item["relativePath"] = PathUtils.get_relative_path(new_item["absolutePath"], self.repository_root_path)
new_item["range"] = item[LSPConstants.TARGET_SELECTION_RANGE] # type: ignore
ret.append(ls_types.Location(**new_item)) # type: ignore
else:
assert False, f"Unexpected response from Language Server: {item}"
elif isinstance(response, dict):
# response is of type Location
assert LSPConstants.URI in response
assert LSPConstants.RANGE in response
def send_request(self) -> object | None:
return self.language_server._send_implementation_request(
self.language_server._create_text_document_position_params(self.relative_file_path, self.line, self.column),
)
new_item: dict = {} # type: ignore
new_item.update(response)
new_item["absolutePath"] = PathUtils.uri_to_path(new_item["uri"])
new_item["relativePath"] = PathUtils.get_relative_path(new_item["absolutePath"], self.repository_root_path)
ret.append(ls_types.Location(**new_item)) # type: ignore
elif response is None:
# Some language servers return None when they cannot find a definition
# This is expected for certain symbol types like generics or types with incomplete information
log.warning(f"Language server returned None for definition request at {relative_file_path}:{line}:{column}")
else:
assert False, f"Unexpected response from Language Server: {response}"
def request_implementation(self, relative_file_path: str, line: int, column: int) -> list[ls_types.Location]:
"""
Raise a [textDocument/implementation](https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#textDocument_implementation)
request to the Language Server for the symbol at the given line and column in the given file.
return ret
:param relative_file_path: The relative path of the file that has the symbol for which implementations should be looked up
:param line: The line number of the symbol
:param column: The column number of the symbol
:return: the list of locations where the symbol is implemented
"""
request = self.ImplementationLocationRequest(self, relative_file_path, line, column)
return request.execute()
# Some LS cause problems with this, so the call is isolated from the rest to allow overriding in subclasses
def _send_references_request(self, relative_file_path: str, line: int, column: int) -> list[lsp_types.Location] | None:
@@ -932,6 +1081,46 @@ class SolidLanguageServer(ABC):
}
)
class ReferencesLocationRequest(SymbolLocationRequest):
def __init__(self, language_server: "SolidLanguageServer", relative_file_path: str, line: int, column: int) -> None:
super().__init__(
language_server,
relative_file_path,
line,
column,
request_name="request_references",
)
def send_request(self) -> object | None:
return self.language_server._send_references_request(self.relative_file_path, line=self.line, column=self.column)
def normalize_response(self, response: object | None) -> list[ls_types.Location]:
if response is None:
return []
assert isinstance(response, list), f"Unexpected response from Language Server (expected list, got {type(response)}): {response}"
ret: list[ls_types.Location] = []
for item in response:
assert isinstance(item, dict), f"Unexpected response from Language Server (expected dict, got {type(item)}): {item}"
if location := self.convert_location_item(cast(dict[str, object], item)):
ret.append(location)
return ret
def log_perf_result(self, t0: float, result: list[ls_types.Location]) -> None:
elapsed_ms = (perf_counter() - t0) * 1000
if not result:
log.debug("perf: request_references path=%s elapsed_ms=%.2f count=0", self.relative_file_path, elapsed_ms)
return
unique_files = len({r["relativePath"] for r in result})
log.debug(
"perf: request_references path=%s elapsed_ms=%.2f count=%d unique_files=%d",
self.relative_file_path,
elapsed_ms,
len(result),
unique_files,
)
def request_references(self, relative_file_path: str, line: int, column: int) -> list[ls_types.Location]:
"""
Raise a [textDocument/references](https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#textDocument_references) request to the Language Server
@@ -944,111 +1133,8 @@ class SolidLanguageServer(ABC):
:return: A list of locations where the symbol is referenced (excluding ignored directories)
"""
if not self.server_started:
log.error("request_references called before Language Server started")
raise SolidLSPException("Language Server not started")
with self.open_file(relative_file_path):
if not self._has_waited_for_cross_file_references:
# Some LS require waiting for a while before they can return cross-file references.
# This is a workaround for such LS that don't have a reliable "finished initializing" signal.
# The waiting has to happen after at least one file was opened in the ls
sleep(self._get_wait_time_for_cross_file_referencing())
self._has_waited_for_cross_file_references = True
t0 = perf_counter() if _debug_enabled else 0.0
try:
response = self._send_references_request(relative_file_path, line=line, column=column)
except Exception as e:
# Catch LSP internal error (-32603) and raise a more informative exception
if isinstance(e, LSPError) and getattr(e, "code", None) == -32603:
raise RuntimeError(
f"LSP internal error (-32603) when requesting references for {relative_file_path}:{line}:{column}. "
"This often occurs when requesting references for a symbol not referenced in the expected way. "
) from e
raise
if response is None:
if _debug_enabled:
elapsed_ms = (perf_counter() - t0) * 1000
log.debug("perf: request_references path=%s elapsed_ms=%.2f count=0", relative_file_path, elapsed_ms)
return []
ret: list[ls_types.Location] = []
assert isinstance(response, list), f"Unexpected response from Language Server (expected list, got {type(response)}): {response}"
for item in response:
assert isinstance(item, dict), f"Unexpected response from Language Server (expected dict, got {type(item)}): {item}"
assert LSPConstants.URI in item
assert LSPConstants.RANGE in item
abs_path = PathUtils.uri_to_path(item[LSPConstants.URI]) # type: ignore
if not Path(abs_path).is_relative_to(self.repository_root_path):
log.warning(
"Found a reference in a path outside the repository, probably the LS is parsing things in installed packages or in the standardlib! "
f"Path: {abs_path}. This is a bug but we currently simply skip these references."
)
continue
rel_path = Path(abs_path).relative_to(self.repository_root_path)
if self.is_ignored_path(str(rel_path)):
log.debug("Ignoring reference in %s since it should be ignored", rel_path)
continue
new_item: dict = {}
new_item.update(item)
new_item["absolutePath"] = str(abs_path)
new_item["relativePath"] = str(rel_path)
ret.append(ls_types.Location(**new_item)) # type: ignore
if _debug_enabled:
elapsed_ms = (perf_counter() - t0) * 1000
unique_files = len({r["relativePath"] for r in ret})
log.debug(
"perf: request_references path=%s elapsed_ms=%.2f count=%d unique_files=%d",
relative_file_path,
elapsed_ms,
len(ret),
unique_files,
)
return ret
def request_text_document_diagnostics(self, relative_file_path: str) -> list[ls_types.Diagnostic]:
"""
Raise a [textDocument/diagnostic](https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#textDocument_diagnostic) request to the Language Server
to find diagnostics for the given file. Wait for the response and return the result.
:param relative_file_path: The relative path of the file to retrieve diagnostics for
:return: A list of diagnostics for the file
"""
if not self.server_started:
log.error("request_text_document_diagnostics called before Language Server started")
raise SolidLSPException("Language Server not started")
with self.open_file(relative_file_path):
response = self.server.send.text_document_diagnostic(
{
LSPConstants.TEXT_DOCUMENT: { # type: ignore
LSPConstants.URI: pathlib.Path(str(PurePath(self.repository_root_path, relative_file_path))).as_uri()
}
}
)
if response is None:
return [] # type: ignore
assert isinstance(response, dict), f"Unexpected response from Language Server (expected list, got {type(response)}): {response}"
ret: list[ls_types.Diagnostic] = []
for item in response["items"]: # type: ignore
new_item: ls_types.Diagnostic = {
"uri": pathlib.Path(str(PurePath(self.repository_root_path, relative_file_path))).as_uri(),
"severity": item["severity"],
"message": item["message"],
"range": item["range"],
"code": item["code"], # type: ignore
}
ret.append(ls_types.Diagnostic(**new_item))
return ret
request = self.ReferencesLocationRequest(self, relative_file_path, line, column)
return request.execute()
def retrieve_full_file_content(self, file_path: str) -> str:
"""
@@ -1945,6 +2031,153 @@ class SolidLanguageServer(ABC):
"""
return definitions[0]
def _get_document_symbols_with_locations(self, relative_file_path: str) -> list[ls_types.UnifiedSymbolInformation]:
absolute_file_path = str(PurePath(self.repository_root_path, relative_file_path))
document_symbols = self.request_document_symbols(relative_file_path)
symbols = list(document_symbols.iter_symbols())
# Make SymbolInformation and DocumentSymbol shapes consistent by ensuring every
# symbol exposes a normalized location/range in the current workspace.
for symbol in symbols:
location = symbol["location"]
location["absolutePath"] = absolute_file_path
location["relativePath"] = relative_file_path
location["uri"] = Path(absolute_file_path).as_uri()
return symbols
@staticmethod
def _position_matches_range(range_d: ls_types.Range, line: int, column: int | None = None) -> bool:
start = range_d["start"]
end = range_d["end"]
if not (start["line"] <= line <= end["line"]):
return False
if column is None:
return True
if line == start["line"] and column < start["character"]:
return False
if line == end["line"] and column > end["character"]:
return False
return True
@staticmethod
def _symbol_match_sort_key(symbol: ls_types.UnifiedSymbolInformation, match_priority: int) -> tuple[int, int, int, int, int]:
location = symbol["location"]
symbol_range = location["range"]
start = symbol_range["start"]
end = symbol_range["end"]
line_span = end["line"] - start["line"]
character_span = end["character"] - start["character"] if line_span == 0 else end["character"]
return match_priority, line_span, character_span, start["line"], start["character"]
def _request_symbol_at_location(
self,
relative_file_path: str,
line: int,
column: int,
include_body: bool = False,
body_factory: SymbolBodyFactory | None = None,
) -> ls_types.UnifiedSymbolInformation | None:
candidates: list[tuple[tuple[int, int, int, int, int], ls_types.UnifiedSymbolInformation]] = []
for symbol in self._get_document_symbols_with_locations(relative_file_path):
location = symbol["location"]
symbol_range = location["range"]
selection_range = symbol.get("selectionRange") or symbol_range
match_priority: int | None = None
if self._position_matches_range(selection_range, line, column):
match_priority = 0
elif self._position_matches_range(symbol_range, line, column):
match_priority = 1
else:
selection_start = selection_range["start"]
symbol_start = symbol_range["start"]
if (selection_start["line"], selection_start["character"]) == (line, column):
match_priority = 2
elif (symbol_start["line"], symbol_start["character"]) == (line, column):
match_priority = 3
elif selection_start["line"] == line and column <= selection_start["character"]:
match_priority = 4
elif symbol_start["line"] == line and column <= symbol_start["character"]:
match_priority = 5
if match_priority is None:
continue
candidates.append((self._symbol_match_sort_key(symbol, match_priority), symbol))
if not candidates:
return None
candidates.sort(key=lambda item: item[0])
best_symbol = candidates[0][1]
if include_body:
best_symbol["body"] = self.create_symbol_body(best_symbol, factory=body_factory)
return best_symbol
@staticmethod
def _iter_symbol_descendants(symbol: ls_types.UnifiedSymbolInformation) -> Iterator[ls_types.UnifiedSymbolInformation]:
"""Yield descendant symbols in depth-first order."""
for child in symbol.get("children", []):
yield child
yield from SolidLanguageServer._iter_symbol_descendants(child)
def _refine_implementing_symbol(
self,
target_symbol: ls_types.UnifiedSymbolInformation | None,
implementing_symbol: ls_types.UnifiedSymbolInformation,
include_body: bool = False,
) -> ls_types.UnifiedSymbolInformation:
"""Resolve member-level implementation symbols when the LS returns a containing type."""
if target_symbol is None:
return implementing_symbol
target_kind = target_symbol["kind"]
if target_kind not in (ls_types.SymbolKind.Method, ls_types.SymbolKind.Function):
return implementing_symbol
if implementing_symbol["kind"] == target_kind and implementing_symbol.get("name") == target_symbol.get("name"):
return implementing_symbol
candidate_descendants: list[ls_types.UnifiedSymbolInformation] = []
for descendant in self._iter_symbol_descendants(implementing_symbol):
if descendant.get("name") != target_symbol.get("name"):
continue
if descendant["kind"] != target_kind:
continue
candidate_descendants.append(descendant)
if not candidate_descendants:
return implementing_symbol
refined_symbol = min(
candidate_descendants,
key=lambda symbol: self._symbol_match_sort_key(symbol, match_priority=0),
)
if include_body:
refined_symbol["body"] = self.create_symbol_body(refined_symbol)
return refined_symbol
def request_symbol_at_location(
self,
relative_file_path: str,
line: int,
column: int,
include_body: bool = False,
) -> ls_types.UnifiedSymbolInformation | None:
"""
Finds the symbol at the given position, preferring exact identifier matches and otherwise
falling back to the innermost symbol whose body contains the position.
:param relative_file_path: The relative path to the file.
:param line: The 0-indexed line number.
:param column: The 0-indexed column number.
:param include_body: whether to include the body of the symbol in the result.
:return: The symbol at the given location, or None if no symbol could be resolved.
"""
if not self.server_started:
log.error("request_symbol_at_location called before language server started")
raise SolidLSPException("Language Server not started")
return self._request_symbol_at_location(relative_file_path, line, column, include_body=include_body)
def request_defining_symbol(
self,
relative_file_path: str,
@@ -1976,14 +2209,79 @@ class SolidLanguageServer(ABC):
# Select the preferred definition (subclasses can override _get_preferred_definition)
definition = self._get_preferred_definition(definitions)
def_path = definition["relativePath"]
assert def_path is not None
if def_path is None:
return None
def_line = definition["range"]["start"]["line"]
def_col = definition["range"]["start"]["character"]
# Find the symbol at or containing this location
defining_symbol = self.request_containing_symbol(def_path, def_line, def_col, strict=False, include_body=include_body)
return self._request_symbol_at_location(
def_path,
def_line,
def_col,
include_body=include_body,
)
return defining_symbol
def request_implementing_symbols(
self,
relative_file_path: str,
line: int,
column: int,
include_body: bool = False,
) -> list[ls_types.UnifiedSymbolInformation]:
"""
Finds the symbols that implement the symbol at the given location.
This method first finds implementation locations for the symbol at the given position,
then retrieves the full symbol information for each implementation and de-duplicates
results that map to the same containing symbol.
:param relative_file_path: The relative path to the file.
:param line: The 0-indexed line number.
:param column: The 0-indexed column number.
:param include_body: whether to include the body of the symbols in the result.
:return: The symbol information for each implementation.
"""
if not self.server_started:
log.error("request_implementing_symbols called before language server started")
raise SolidLSPException("Language Server not started")
target_symbol = self._request_symbol_at_location(relative_file_path, line, column, include_body=False)
implementation_locations = self.request_implementation(relative_file_path, line, column)
if not implementation_locations:
return []
result: list[ls_types.UnifiedSymbolInformation] = []
seen_keys: set[tuple[str, int, int, int]] = set()
for implementation in implementation_locations:
implementation_path = implementation["relativePath"]
assert implementation_path is not None
implementation_line = implementation["range"]["start"]["line"]
implementation_col = implementation["range"]["start"]["character"]
implementing_symbol = self._request_symbol_at_location(
implementation_path,
implementation_line,
implementation_col,
include_body=include_body,
body_factory=None,
)
if implementing_symbol is None:
continue
implementing_symbol = self._refine_implementing_symbol(target_symbol, implementing_symbol, include_body=include_body)
if "location" not in implementing_symbol:
continue
symbol_location = implementing_symbol["location"]
symbol_key = (
cast(str, symbol_location["relativePath"]),
symbol_location["range"]["start"]["line"],
symbol_location["range"]["start"]["character"],
implementing_symbol["kind"],
)
if symbol_key in seen_keys:
continue
seen_keys.add(symbol_key)
result.append(implementing_symbol)
return result
def _document_symbols_cache_fingerprint(self) -> Hashable | None:
"""
+7 -2
View File
@@ -3,6 +3,7 @@ from pathlib import Path
import pytest
from serena.util.text_utils import find_text_coordinates
from solidlsp import SolidLanguageServer
from solidlsp.ls_config import Language
from solidlsp.ls_types import SymbolKind
@@ -243,8 +244,12 @@ class TestDartLanguageServer:
def test_request_defining_symbol_variable(self, language_server: SolidLanguageServer) -> None:
"""Test request_defining_symbol for a variable usage."""
file_path = os.path.join("lib", "main.dart")
# Line 14 contains 'final result = a + b;' - test position on 'result'
defining_symbol = language_server.request_defining_symbol(file_path, 13, 10)
# Find coordinates of 'final result = a + b;' - test position on 'result'
with language_server.open_file(file_path, open_in_ls=False) as f:
pos = find_text_coordinates(f.contents, r"final (result) = a \+ b;")
defining_symbol = language_server.request_defining_symbol(file_path, pos.line, pos.col)
# The defining symbol might be the variable itself or the containing method
# This is acceptable behavior - different language servers handle this differently
@@ -12,6 +12,7 @@ import os
import pytest
from serena.util.text_utils import find_text_coordinates
from solidlsp import SolidLanguageServer
from solidlsp.ls_config import Language
from solidlsp.ls_types import SymbolKind
@@ -583,15 +584,17 @@ class TestRubyLanguageServerSymbols:
"""Test request_defining_symbol for a method call."""
# Test finding the definition of a method being called
file_path = os.path.join("examples", "user_management.rb")
# Position at a method call like create_user
defining_symbol = language_server.request_defining_symbol(file_path, 30, 15)
# Verify that we can find method definitions
if defining_symbol is not None:
assert "name" in defining_symbol
assert "kind" in defining_symbol
# Should be a method or constructor
assert defining_symbol.get("kind") in [SymbolKind.Method.value, SymbolKind.Constructor.value, SymbolKind.Function.value]
# Position at a method call like create_user
with language_server.open_file(file_path, open_in_ls=False) as fb:
pos = find_text_coordinates(fb.contents, r"user = @service\.(create_user)")
# Verify that we can find the method definition
defining_symbol = language_server.request_defining_symbol(file_path, pos.line, pos.col)
assert "name" in defining_symbol
assert "kind" in defining_symbol
assert defining_symbol.get("name") == "create_user"
assert defining_symbol.get("kind") == SymbolKind.Method.value
@pytest.mark.parametrize("language_server", [Language.RUBY], indirect=True)
def test_request_defining_symbol_nested_function(self, language_server: SolidLanguageServer) -> None: