Skip to content

Commit 66cb701

Browse files
committed
feat(langserver): support for workspace symbols
1 parent e550fb0 commit 66cb701

File tree

8 files changed

+290
-8
lines changed

8 files changed

+290
-8
lines changed
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

+2
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
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
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+
class RobotWorkspaceSymbolsProtocolPart(RobotLanguageServerProtocolPart):
19+
_logger = LoggingDescriptor()
20+
21+
def __init__(self, parent: "RobotLanguageServerProtocol") -> None:
22+
super().__init__(parent)
23+
24+
parent.workspace_symbols.collect.add(self.collect)
25+
26+
@_logger.call
27+
def collect(self, sender: Any, query: str) -> Optional[Union[List[WorkspaceSymbol], List[SymbolInformation], None]]:
28+
result: List[WorkspaceSymbol] = []
29+
query_set = set(query)
30+
for document in self.parent.documents.documents:
31+
if document.language_id == "robotframework":
32+
namespace = self.parent.documents_cache.get_only_initialized_namespace(document)
33+
if namespace is not None:
34+
container_name = namespace.get_library_doc().name
35+
36+
for kw_doc in [
37+
v
38+
for v in namespace.get_keyword_references().keys()
39+
if v.source == namespace.source and query_set.issubset(v.name)
40+
]:
41+
result.append(
42+
WorkspaceSymbol(
43+
name=kw_doc.name,
44+
kind=SymbolKind.FUNCTION,
45+
location=Location(
46+
uri=document.document_uri,
47+
range=kw_doc.range,
48+
),
49+
tags=[SymbolTag.DEPRECATED] if kw_doc.is_deprecated else None,
50+
container_name=container_name,
51+
)
52+
)
53+
for var in [
54+
v
55+
for v in namespace.get_variable_references().keys()
56+
if v.source == namespace.source and query_set.issubset(v.name)
57+
]:
58+
result.append(
59+
WorkspaceSymbol(
60+
name=var.name,
61+
kind=SymbolKind.VARIABLE,
62+
location=Location(
63+
uri=document.document_uri,
64+
range=var.range,
65+
),
66+
container_name=container_name,
67+
)
68+
)
69+
70+
for test in [v for v in namespace.get_testcase_definitions() if set(query).issubset(v.name)]:
71+
result.append(
72+
WorkspaceSymbol(
73+
name=test.name,
74+
kind=SymbolKind.OBJECT,
75+
location=Location(
76+
uri=document.document_uri,
77+
range=test.range,
78+
),
79+
container_name=container_name,
80+
)
81+
)
82+
return result

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

+2
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

+36
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

+16-6
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

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

+18
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,8 @@
6363
InvalidVariableError,
6464
LibraryEntry,
6565
LocalVariableDefinition,
66+
TagDefinition,
67+
TestCaseDefinition,
6668
TestVariableDefinition,
6769
VariableDefinition,
6870
VariableDefinitionType,
@@ -92,6 +94,8 @@ class AnalyzerResult:
9294
variable_references: Dict[VariableDefinition, Set[Location]]
9395
local_variable_assignments: Dict[VariableDefinition, Set[Range]]
9496
namespace_references: Dict[LibraryEntry, Set[Location]]
97+
test_case_definitions: List[TestCaseDefinition]
98+
tag_definitions: List[TagDefinition]
9599

96100
# TODO Tag references
97101

@@ -121,6 +125,8 @@ def __init__(
121125
self._variable_references: Dict[VariableDefinition, Set[Location]] = defaultdict(set)
122126
self._local_variable_assignments: Dict[VariableDefinition, Set[Range]] = defaultdict(set)
123127
self._namespace_references: Dict[LibraryEntry, Set[Location]] = defaultdict(set)
128+
self._test_case_definitions: List[TestCaseDefinition] = []
129+
self._tag_definitions: List[TagDefinition] = []
124130

125131
self._variables: Dict[VariableMatcher, VariableDefinition] = {
126132
**{v.matcher: v for v in self._namespace.get_builtin_variables()},
@@ -166,6 +172,8 @@ def run(self) -> AnalyzerResult:
166172
self._variable_references,
167173
self._local_variable_assignments,
168174
self._namespace_references,
175+
self._test_case_definitions,
176+
self._tag_definitions,
169177
)
170178

171179
def _visit_VariableSection(self, node: VariableSection) -> None: # noqa: N802
@@ -1048,6 +1056,16 @@ def visit_TestCaseName(self, node: TestCaseName) -> None: # noqa: N802
10481056
name_token = node.get_token(Token.TESTCASE_NAME)
10491057
if name_token is not None and name_token.value:
10501058
self._analyze_token_variables(name_token, DiagnosticSeverity.HINT)
1059+
self._test_case_definitions.append(
1060+
TestCaseDefinition(
1061+
line_no=name_token.lineno,
1062+
col_offset=name_token.col_offset,
1063+
end_line_no=name_token.lineno,
1064+
end_col_offset=name_token.end_col_offset,
1065+
source=self._namespace.source,
1066+
name=name_token.value,
1067+
)
1068+
)
10511069

10521070
@functools.cached_property
10531071
def _namespace_lib_doc(self) -> LibraryDoc:

0 commit comments

Comments
 (0)