Skip to content

Commit a90ce60

Browse files
committed
feat(langserver): support for workspace symbols
closes #323
1 parent e550fb0 commit a90ce60

File tree

88 files changed

+155250
-66
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

88 files changed

+155250
-66
lines changed
Lines changed: 132 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,132 @@
1+
from concurrent.futures import CancelledError
2+
from typing import (
3+
TYPE_CHECKING,
4+
Any,
5+
Callable,
6+
Final,
7+
Iterable,
8+
List,
9+
Optional,
10+
Protocol,
11+
TypeVar,
12+
Union,
13+
cast,
14+
runtime_checkable,
15+
)
16+
17+
from robotcode.core.event import event
18+
from robotcode.core.lsp.types import (
19+
Location,
20+
ServerCapabilities,
21+
SymbolInformation,
22+
WorkspaceSymbol,
23+
WorkspaceSymbolClientCapabilitiesResolveSupportType,
24+
WorkspaceSymbolClientCapabilitiesSymbolKindType,
25+
WorkspaceSymbolClientCapabilitiesTagSupportType,
26+
WorkspaceSymbolParams,
27+
)
28+
from robotcode.core.utils.logging import LoggingDescriptor
29+
from robotcode.jsonrpc2.protocol import rpc_method
30+
from robotcode.language_server.common.parts.protocol_part import (
31+
LanguageServerProtocolPart,
32+
)
33+
34+
if TYPE_CHECKING:
35+
from robotcode.language_server.common.protocol import LanguageServerProtocol
36+
37+
38+
@runtime_checkable
39+
class HasSymbolInformationLabel(Protocol):
40+
symbol_information_label: str
41+
42+
43+
_F = TypeVar("_F", bound=Callable[..., Any])
44+
45+
46+
def symbol_information_label(label: str) -> Callable[[_F], _F]:
47+
def decorator(func: _F) -> _F:
48+
setattr(func, "symbol_information_label", label)
49+
return func
50+
51+
return decorator
52+
53+
54+
class WorkspaceSymbolsProtocolPart(LanguageServerProtocolPart):
55+
_logger: Final = LoggingDescriptor()
56+
57+
def __init__(self, parent: "LanguageServerProtocol") -> None:
58+
super().__init__(parent)
59+
self.symbol_kind: Optional[WorkspaceSymbolClientCapabilitiesSymbolKindType] = None
60+
self.tag_support: Optional[WorkspaceSymbolClientCapabilitiesTagSupportType] = None
61+
self.resolve_support: Optional[WorkspaceSymbolClientCapabilitiesResolveSupportType] = None
62+
63+
@event
64+
def collect(
65+
sender,
66+
query: str,
67+
) -> Optional[Union[List[WorkspaceSymbol], List[SymbolInformation], None]]: ...
68+
69+
def extend_capabilities(self, capabilities: ServerCapabilities) -> None:
70+
if (
71+
self.parent.client_capabilities
72+
and self.parent.client_capabilities.workspace
73+
and self.parent.client_capabilities.workspace.symbol is not None
74+
):
75+
workspace_symbol = self.parent.client_capabilities.workspace.symbol
76+
77+
self.symbol_kind = workspace_symbol.symbol_kind
78+
self.tag_support = workspace_symbol.tag_support
79+
self.resolve_support = workspace_symbol.resolve_support
80+
81+
if len(self.collect):
82+
# TODO: Implement workspace resolve
83+
capabilities.workspace_symbol_provider = True
84+
85+
@rpc_method(name="workspace/symbol", param_type=WorkspaceSymbolParams, threaded=True)
86+
def _workspace_symbol(
87+
self, query: str, *args: Any, **kwargs: Any
88+
) -> Optional[Union[List[WorkspaceSymbol], List[SymbolInformation], None]]:
89+
workspace_symbols: List[WorkspaceSymbol] = []
90+
symbol_informations: List[SymbolInformation] = []
91+
92+
for result in self.collect(self, query):
93+
if isinstance(result, BaseException):
94+
if not isinstance(result, CancelledError):
95+
self._logger.exception(result, exc_info=result)
96+
else:
97+
if result is not None:
98+
if all(isinstance(e, WorkspaceSymbol) for e in result):
99+
workspace_symbols.extend(cast(Iterable[WorkspaceSymbol], result))
100+
elif all(isinstance(e, SymbolInformation) for e in result):
101+
symbol_informations.extend(cast(Iterable[SymbolInformation], result))
102+
else:
103+
self._logger.warning(
104+
"Result contains WorkspaceSymbol and SymbolInformation results, result is skipped."
105+
)
106+
107+
if workspace_symbols:
108+
for symbol in workspace_symbols:
109+
if isinstance(symbol.location, Location):
110+
doc = self.parent.documents.get(symbol.location.uri)
111+
if doc is not None:
112+
symbol.location.range = doc.range_to_utf16(symbol.location.range)
113+
114+
if symbol_informations:
115+
for symbol_information in symbol_informations:
116+
doc = self.parent.documents.get(symbol_information.location.uri)
117+
if doc is not None:
118+
symbol_information.location.range = doc.range_to_utf16(symbol_information.location.range)
119+
120+
if workspace_symbols and symbol_informations:
121+
self._logger.warning(
122+
"Result contains WorksapceSymbol and SymbolInformation results, only WorkspaceSymbols returned."
123+
)
124+
return workspace_symbols
125+
126+
if workspace_symbols:
127+
return workspace_symbols
128+
129+
if symbol_informations:
130+
return symbol_informations
131+
132+
return None

packages/language_server/src/robotcode/language_server/common/protocol.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,7 @@
7171
from .parts.signature_help import SignatureHelpProtocolPart
7272
from .parts.window import WindowProtocolPart
7373
from .parts.workspace import Workspace
74+
from .parts.workspace_symbols import WorkspaceSymbolsProtocolPart
7475

7576
__all__ = ["LanguageServerException", "LanguageServerProtocol"]
7677

@@ -133,6 +134,7 @@ class LanguageServerProtocol(JsonRPCProtocol):
133134
inline_value: Final = ProtocolPartDescriptor(InlineValueProtocolPart)
134135
inlay_hint: Final = ProtocolPartDescriptor(InlayHintProtocolPart)
135136
code_action: Final = ProtocolPartDescriptor(CodeActionProtocolPart)
137+
workspace_symbols: Final = ProtocolPartDescriptor(WorkspaceSymbolsProtocolPart)
136138

137139
name: Optional[str] = None
138140
short_name: Optional[str] = None

packages/language_server/src/robotcode/language_server/robotframework/parts/document_symbols.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -94,7 +94,7 @@ def visit_TestCase(self, node: TestCase) -> None: # noqa: N802
9494
r = range_from_node(node)
9595
symbol = DocumentSymbol(
9696
name=node.name,
97-
kind=SymbolKind.METHOD,
97+
kind=SymbolKind.CLASS,
9898
range=r,
9999
selection_range=r,
100100
children=[],
Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
from typing import TYPE_CHECKING, Any, List, Optional, Union
2+
3+
from robotcode.core.lsp.types import (
4+
Location,
5+
SymbolInformation,
6+
SymbolKind,
7+
SymbolTag,
8+
WorkspaceSymbol,
9+
)
10+
from robotcode.core.utils.logging import LoggingDescriptor
11+
12+
from .protocol_part import RobotLanguageServerProtocolPart
13+
14+
if TYPE_CHECKING:
15+
from ..protocol import RobotLanguageServerProtocol
16+
17+
18+
def contains_characters_in_order(main_string: str, check_string: str) -> bool:
19+
main_iter = iter(main_string.lower())
20+
return all(char in main_iter for char in check_string.lower())
21+
22+
23+
class RobotWorkspaceSymbolsProtocolPart(RobotLanguageServerProtocolPart):
24+
_logger = LoggingDescriptor()
25+
26+
def __init__(self, parent: "RobotLanguageServerProtocol") -> None:
27+
super().__init__(parent)
28+
29+
parent.workspace_symbols.collect.add(self.collect)
30+
31+
@_logger.call
32+
def collect(self, sender: Any, query: str) -> Optional[Union[List[WorkspaceSymbol], List[SymbolInformation], None]]:
33+
result: List[WorkspaceSymbol] = []
34+
35+
for document in self.parent.documents.documents:
36+
if document.language_id == "robotframework":
37+
namespace = self.parent.documents_cache.get_only_initialized_namespace(document)
38+
if namespace is not None:
39+
container_name = namespace.get_library_doc().name
40+
41+
for kw_doc in [
42+
v
43+
for v in namespace.get_keyword_references().keys()
44+
if v.source == namespace.source and contains_characters_in_order(v.name, query)
45+
]:
46+
result.append(
47+
WorkspaceSymbol(
48+
name=kw_doc.name,
49+
kind=SymbolKind.FUNCTION,
50+
location=Location(
51+
uri=document.document_uri,
52+
range=kw_doc.range,
53+
),
54+
tags=[SymbolTag.DEPRECATED] if kw_doc.is_deprecated else None,
55+
container_name=container_name,
56+
)
57+
)
58+
for var in [
59+
v
60+
for v in namespace.get_variable_references().keys()
61+
if v.source == namespace.source and contains_characters_in_order(v.name, query)
62+
]:
63+
result.append(
64+
WorkspaceSymbol(
65+
name=var.name,
66+
kind=SymbolKind.VARIABLE,
67+
location=Location(
68+
uri=document.document_uri,
69+
range=var.range,
70+
),
71+
container_name=container_name,
72+
)
73+
)
74+
75+
for test in [
76+
v for v in namespace.get_testcase_definitions() if contains_characters_in_order(v.name, query)
77+
]:
78+
result.append(
79+
WorkspaceSymbol(
80+
name=test.name,
81+
kind=SymbolKind.CLASS,
82+
location=Location(
83+
uri=document.document_uri,
84+
range=test.range,
85+
),
86+
container_name=container_name,
87+
)
88+
)
89+
return result

packages/language_server/src/robotcode/language_server/robotframework/protocol.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@
5252
from .parts.selection_range import RobotSelectionRangeProtocolPart
5353
from .parts.semantic_tokens import RobotSemanticTokenProtocolPart
5454
from .parts.signature_help import RobotSignatureHelpProtocolPart
55+
from .parts.workspace_symbols import RobotWorkspaceSymbolsProtocolPart
5556

5657
if TYPE_CHECKING:
5758
from .server import RobotLanguageServer
@@ -118,6 +119,7 @@ class RobotLanguageServerProtocol(LanguageServerProtocol):
118119
robot_debugging_utils = ProtocolPartDescriptor(RobotDebuggingUtilsProtocolPart)
119120
robot_keywords_treeview = ProtocolPartDescriptor(RobotKeywordsTreeViewPart)
120121
robot_project_info = ProtocolPartDescriptor(ProjectInfoPart)
122+
robot_workspace_symbols = ProtocolPartDescriptor(RobotWorkspaceSymbolsProtocolPart)
121123

122124
http_server = ProtocolPartDescriptor(HttpServerProtocolPart)
123125

packages/robot/src/robotcode/robot/diagnostics/entities.py

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -394,3 +394,39 @@ def __hash__(self) -> int:
394394
self.import_source,
395395
)
396396
)
397+
398+
399+
@dataclass
400+
class TestCaseDefinition(SourceEntity):
401+
name: str
402+
403+
@single_call
404+
def __hash__(self) -> int:
405+
return hash(
406+
(
407+
self.line_no,
408+
self.col_offset,
409+
self.end_line_no,
410+
self.end_col_offset,
411+
self.source,
412+
self.name,
413+
)
414+
)
415+
416+
417+
@dataclass
418+
class TagDefinition(SourceEntity):
419+
name: str
420+
421+
@single_call
422+
def __hash__(self) -> int:
423+
return hash(
424+
(
425+
self.line_no,
426+
self.col_offset,
427+
self.end_line_no,
428+
self.end_col_offset,
429+
self.source,
430+
self.name,
431+
)
432+
)

packages/robot/src/robotcode/robot/diagnostics/namespace.py

Lines changed: 16 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,8 @@
7272
LocalVariableDefinition,
7373
ResourceEntry,
7474
ResourceImport,
75+
TagDefinition,
76+
TestCaseDefinition,
7577
TestVariableDefinition,
7678
VariableDefinition,
7779
VariableMatcher,
@@ -720,6 +722,9 @@ def __init__(
720722
self._local_variable_assignments: Dict[VariableDefinition, Set[Range]] = {}
721723
self._namespace_references: Dict[LibraryEntry, Set[Location]] = {}
722724

725+
self._test_case_definitions: List[TestCaseDefinition] = []
726+
self._tag_definitions: List[TagDefinition] = []
727+
723728
self._imported_keywords: Optional[List[KeywordDoc]] = None
724729
self._imported_keywords_lock = RLock(default_timeout=120, name="Namespace.imported_keywords")
725730
self._keywords: Optional[List[KeywordDoc]] = None
@@ -847,18 +852,21 @@ def get_keyword_references(self) -> Dict[KeywordDoc, Set[Location]]:
847852

848853
return self._keyword_references
849854

850-
def get_variable_references(
851-
self,
852-
) -> Dict[VariableDefinition, Set[Location]]:
855+
def get_variable_references(self) -> Dict[VariableDefinition, Set[Location]]:
853856
self.ensure_initialized()
854857

855858
self.analyze()
856859

857860
return self._variable_references
858861

859-
def get_local_variable_assignments(
860-
self,
861-
) -> Dict[VariableDefinition, Set[Range]]:
862+
def get_testcase_definitions(self) -> List[TestCaseDefinition]:
863+
self.ensure_initialized()
864+
865+
self.analyze()
866+
867+
return self._test_case_definitions
868+
869+
def get_local_variable_assignments(self) -> Dict[VariableDefinition, Set[Range]]:
862870
self.ensure_initialized()
863871

864872
self.analyze()
@@ -1910,6 +1918,8 @@ def analyze(self) -> None:
19101918
self._variable_references = result.variable_references
19111919
self._local_variable_assignments = result.local_variable_assignments
19121920
self._namespace_references = result.namespace_references
1921+
self._test_case_definitions = result.test_case_definitions
1922+
self._tag_definitions = result.tag_definitions
19131923

19141924
lib_doc = self.get_library_doc()
19151925

0 commit comments

Comments
 (0)