mirror of
https://github.com/tiennm99/serena.git
synced 2026-10-13 10:16:00 +00:00
LS: support definition and implementation endpoints
Factorized out commonalities between references, definition and implementation endpoints Co-authored-by: Dominik Jain <dominik.jain@oraios-ai.de>
This commit is contained in:
1 parent
c6250aeeab
commit
08fb88a742
1 file changed
+452
-164
+452
-164
@@ -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:
|
||||
"""
|
||||
@@ -846,6 +854,147 @@ 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
|
||||
|
||||
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: str | None
|
||||
if not Path(abs_path).is_relative_to(self.language_server.repository_root_path):
|
||||
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
|
||||
rel_path_str = PathUtils.get_relative_path(abs_path, self.language_server.repository_root_path)
|
||||
assert rel_path_str is not None, f"Failed to compute repository-relative path for {abs_path}"
|
||||
if 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 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 +1006,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 +1071,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 +1123,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 +2021,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 +2199,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:
|
||||
"""
|
||||
|
||||
Reference in new issue
Block a user