Skip to content

Commit cce9bdc

Browse files
committed
implement keyword references
1 parent ba69600 commit cce9bdc

File tree

20 files changed

+792
-88
lines changed

20 files changed

+792
-88
lines changed

package.json

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -348,6 +348,21 @@
348348
"description": "Defines if the test report should be opened a run session automatically.",
349349
"scope": "resource"
350350
},
351+
"robotcode.workspace.excludePatterns": {
352+
"type": "array",
353+
"default": [
354+
"**/.git/**",
355+
"**/node_modules/**",
356+
"**/.pytest_cache/**",
357+
"**/__pycache__/**",
358+
"**/.mypy_cache/**"
359+
],
360+
"items": {
361+
"type": "string"
362+
},
363+
"description": "Specifies glob patterns for excluding files and folders from analysing by the language server.",
364+
"scope": "resource"
365+
},
351366
"robotcode.robocop.enabled": {
352367
"type": "boolean",
353368
"default": true,

robotcode/language_server/common/lsp_types.py

Lines changed: 35 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,15 @@ def __repr__(self) -> str:
2828
args = ", ".join(
2929
f"{f.name}={getattr(self, f.name)!r}"
3030
for f in dataclasses.fields(self)
31-
if f.repr and f.default != getattr(self, f.name)
31+
if f.repr
32+
and (
33+
(f.default == dataclasses.MISSING and f.default_factory == dataclasses.MISSING) # type: ignore
34+
or (f.default != dataclasses.MISSING and f.default != getattr(self, f.name))
35+
or (
36+
f.default_factory != dataclasses.MISSING # type: ignore
37+
and getattr(self, f.name) != f.default_factory() # type: ignore
38+
)
39+
)
3240
)
3341
return f"{self.__class__.__qualname__}({args})"
3442

@@ -913,6 +921,16 @@ class SemanticTokensRegistrationOptions(
913921
pass
914922

915923

924+
@dataclass(repr=False)
925+
class ReferenceOptions(WorkDoneProgressOptions):
926+
pass
927+
928+
929+
@dataclass(repr=False)
930+
class ReferenceRegistrationOptions(TextDocumentRegistrationOptions, ReferenceOptions):
931+
pass
932+
933+
916934
@dataclass(repr=False)
917935
class ServerCapabilities(Model):
918936
text_document_sync: Union[TextDocumentSyncOptions, TextDocumentSyncKind, None] = None
@@ -922,7 +940,7 @@ class ServerCapabilities(Model):
922940
declaration_provider: Union[bool, DeclarationOptions, DeclarationRegistrationOptions, None] = None
923941
definition_provider: Union[bool, DefinitionOptions, None] = None
924942
implementation_provider: Union[bool, ImplementationOptions, ImplementationRegistrationOptions, None] = None
925-
# references_provider: Union[bool, ReferenceOptions, None] = None
943+
references_provider: Union[bool, ReferenceOptions, None] = None
926944
# document_highlight_provider: Union[bool, DocumentHighlightOptions, None] = None
927945
document_symbol_provider: Union[bool, DocumentSymbolOptions, None] = None
928946
# code_action_provider: Union[bool, CodeActionOptions] = None
@@ -1798,3 +1816,18 @@ class SemanticTokenModifiers(Enum):
17981816

17991817
def __repr__(self) -> str: # pragma: no cover
18001818
return super().__str__()
1819+
1820+
1821+
@dataclass(repr=False)
1822+
class ReferenceContext(Model):
1823+
include_declaration: bool
1824+
1825+
1826+
@dataclass(repr=False)
1827+
class _ReferenceParams(Model):
1828+
context: ReferenceContext
1829+
1830+
1831+
@dataclass(repr=False)
1832+
class ReferenceParams(WorkDoneProgressParams, PartialResultParams, TextDocumentPositionParams, _ReferenceParams):
1833+
pass
Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
from __future__ import annotations
2+
3+
from asyncio import CancelledError
4+
from typing import TYPE_CHECKING, Any, List, Optional
5+
6+
from ....jsonrpc2.protocol import rpc_method
7+
from ....utils.async_event import async_tasking_event
8+
from ....utils.logging import LoggingDescriptor
9+
from ..has_extend_capabilities import HasExtendCapabilities
10+
from ..language import HasLanguageId
11+
from ..lsp_types import (
12+
Location,
13+
Position,
14+
ReferenceContext,
15+
ReferenceOptions,
16+
ReferenceParams,
17+
ServerCapabilities,
18+
TextDocumentIdentifier,
19+
)
20+
from ..text_document import TextDocument
21+
22+
if TYPE_CHECKING:
23+
from ..protocol import LanguageServerProtocol
24+
25+
from .protocol_part import LanguageServerProtocolPart
26+
27+
28+
class ReferencesProtocolPart(LanguageServerProtocolPart, HasExtendCapabilities):
29+
30+
_logger = LoggingDescriptor()
31+
32+
def __init__(self, parent: LanguageServerProtocol) -> None:
33+
super().__init__(parent)
34+
35+
def extend_capabilities(self, capabilities: ServerCapabilities) -> None:
36+
if len(self.collect):
37+
capabilities.references_provider = ReferenceOptions(work_done_progress=True)
38+
39+
@async_tasking_event
40+
async def collect(
41+
sender, document: TextDocument, position: Position, context: ReferenceContext
42+
) -> Optional[List[Location]]:
43+
...
44+
45+
@rpc_method(name="textDocument/references", param_type=ReferenceParams)
46+
async def _text_document_references(
47+
self,
48+
text_document: TextDocumentIdentifier,
49+
position: Position,
50+
context: ReferenceContext,
51+
*args: Any,
52+
**kwargs: Any,
53+
) -> Optional[List[Location]]:
54+
55+
locations: List[Location] = []
56+
57+
document = self.parent.documents[text_document.uri]
58+
for result in await self.collect(
59+
self,
60+
document,
61+
position,
62+
context,
63+
callback_filter=lambda c: not isinstance(c, HasLanguageId) or c.__language_id__ == document.language_id,
64+
):
65+
if isinstance(result, BaseException):
66+
if not isinstance(result, CancelledError):
67+
self._logger.exception(result, exc_info=result)
68+
else:
69+
if result is not None:
70+
locations.extend(result)
71+
72+
if len(locations) == 0:
73+
return None
74+
75+
return locations

robotcode/language_server/common/parts/workspace.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@
2929
from ....utils.logging import LoggingDescriptor
3030
from ....utils.path import path_is_relative_to
3131
from ....utils.uri import Uri
32+
from ...common.has_extend_capabilities import HasExtendCapabilities
3233
from ..lsp_types import (
3334
ConfigurationItem,
3435
ConfigurationParams,
@@ -128,7 +129,7 @@ class ConfigBase(Model):
128129
_F = TypeVar("_F", bound=Callable[..., Any])
129130

130131

131-
class Workspace(LanguageServerProtocolPart):
132+
class Workspace(LanguageServerProtocolPart, HasExtendCapabilities):
132133
_logger = LoggingDescriptor()
133134

134135
def __init__(
@@ -140,6 +141,7 @@ def __init__(
140141
):
141142
super().__init__(parent)
142143
self.root_uri = root_uri
144+
143145
self.root_path = root_path
144146
self.workspace_folders_lock = asyncio.Lock()
145147
self.workspace_folders: List[WorkspaceFolder] = (

robotcode/language_server/common/protocol.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@
4747
from .parts.formatting import FormattingProtocolPart
4848
from .parts.hover import HoverProtocolPart
4949
from .parts.implementation import ImplementationProtocolPart
50+
from .parts.references import ReferencesProtocolPart
5051
from .parts.semantic_tokens import SemanticTokensProtocolPart
5152
from .parts.signature_help import SignatureHelpProtocolPart
5253
from .parts.window import WindowProtocolPart
@@ -77,6 +78,7 @@ class LanguageServerProtocol(JsonRPCProtocol):
7778
document_symbols = ProtocolPartDescriptor(DocumentSymbolsProtocolPart)
7879
formatting = ProtocolPartDescriptor(FormattingProtocolPart)
7980
semantic_tokens = ProtocolPartDescriptor(SemanticTokensProtocolPart)
81+
references = ProtocolPartDescriptor(ReferencesProtocolPart)
8082

8183
name: Optional[str] = None
8284
version: Optional[str] = None

robotcode/language_server/robotframework/configuration.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,9 +43,16 @@ class RoboTidyConfig(ConfigBase):
4343
enabled: bool = True
4444

4545

46+
@config_section("robotcode.workspace")
47+
@dataclass
48+
class WorkspaceConfig(ConfigBase):
49+
exclude_patterns: List[str] = field(default_factory=list)
50+
51+
4652
@config_section("robotcode")
4753
@dataclass
4854
class RobotCodeConfig(ConfigBase):
4955
language_server: LanguageServerConfig = LanguageServerConfig()
5056
robot: RobotConfig = RobotConfig()
5157
syntax: SyntaxConfig = SyntaxConfig()
58+
workspace: WorkspaceConfig = WorkspaceConfig()

robotcode/language_server/robotframework/diagnostics/imports_manager.py

Lines changed: 2 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313
from ....utils.logging import LoggingDescriptor
1414
from ....utils.path import path_is_relative_to
1515
from ....utils.uri import Uri
16-
from ...common.lsp_types import DocumentUri, FileChangeType, FileEvent
16+
from ...common.lsp_types import FileChangeType, FileEvent
1717
from ...common.parts.workspace import FileWatcherEntry
1818
from ...common.text_document import TextDocument
1919
from ..configuration import RobotConfig
@@ -641,8 +641,6 @@ async def _get_entry_for_resource_import(self, name: str, base_dir: str, sentine
641641
source = await self.find_file(name, base_dir, "Resource")
642642

643643
async def _get_document() -> TextDocument:
644-
from robot.utils import FileReader
645-
646644
self._logger.debug(lambda: f"Load resource {name} from source {source}")
647645

648646
source_path = Path(source).resolve()
@@ -653,18 +651,7 @@ async def _get_document() -> TextDocument:
653651
f"Supported extensions are {', '.join(repr(s) for s in RESOURCE_EXTENSIONS)}."
654652
)
655653

656-
source_uri = DocumentUri(Uri.from_path(source_path).normalized())
657-
658-
result = self.parent_protocol.documents.get(source_uri, None)
659-
if result is not None:
660-
return result
661-
662-
with FileReader(source_path) as reader:
663-
text = str(reader.read())
664-
665-
return self.parent_protocol.documents.append_document(
666-
document_uri=source_uri, language_id="robotframework", text=text
667-
)
654+
return self.parent_protocol.robot_workspace.get_or_open_document(source_path, "robotframework")
668655

669656
async with self._resources_lock:
670657
entry_key = _ResourcesEntryKey(source)

robotcode/language_server/robotframework/diagnostics/namespace.py

Lines changed: 50 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -887,7 +887,6 @@ def __init__(
887887
self._diagnostics: List[Diagnostic] = []
888888

889889
self._keywords: Optional[List[KeywordDoc]] = None
890-
self._loop = asyncio.get_event_loop()
891890

892891
# TODO: how to get the search order from model
893892
self.search_order: Tuple[str, ...] = ()
@@ -954,29 +953,33 @@ async def get_library_doc(self) -> LibraryDoc:
954953

955954
@_logger.call
956955
async def ensure_initialized(self) -> bool:
957-
async with self._initialize_lock:
956+
if not self._initialized:
957+
async with self._initialize_lock:
958+
if not self._initialized:
959+
imports = await self.get_imports()
960+
961+
if self.document is not None:
962+
963+
old_imports: List[Import] = self.document.get_data(Namespace)
964+
if old_imports is None:
965+
self.document.set_data(Namespace, imports)
966+
elif old_imports != imports:
967+
new_imports = []
968+
for e in old_imports:
969+
if e in imports:
970+
new_imports.append(e)
971+
for e in imports:
972+
if e not in new_imports:
973+
new_imports.append(e)
974+
self.document.set_data(Namespace, new_imports)
975+
976+
await self._import_default_libraries()
977+
await self._import_imports(imports, str(Path(self.source).parent), top_level=True)
978+
979+
self._initialized = True
980+
958981
if not self._initialized:
959-
imports = await self.get_imports()
960-
961-
if self.document is not None:
962-
963-
old_imports: List[Import] = self.document.get_data(Namespace)
964-
if old_imports is None:
965-
self.document.set_data(Namespace, imports)
966-
elif old_imports != imports:
967-
new_imports = []
968-
for e in old_imports:
969-
if e in imports:
970-
new_imports.append(e)
971-
for e in imports:
972-
if e not in new_imports:
973-
new_imports.append(e)
974-
self.document.set_data(Namespace, new_imports)
975-
976-
await self._import_default_libraries()
977-
await self._import_imports(imports, str(Path(self.source).parent), top_level=True)
978-
979-
self._initialized = True
982+
raise Exception("Namespace not initialized")
980983
return self._initialized
981984

982985
@property
@@ -1383,42 +1386,40 @@ async def get_keywords(self) -> List[KeywordDoc]:
13831386
async def _analyze(self) -> None:
13841387
if not self._analyzed:
13851388
async with self._analyze_lock:
1386-
try:
1387-
self._diagnostics += await Analyzer().get(self.model, self)
1389+
if not self._analyzed:
1390+
try:
1391+
self._diagnostics += await Analyzer().get(self.model, self)
13881392

1389-
lib_doc = await self.get_library_doc()
1393+
lib_doc = await self.get_library_doc()
13901394

1391-
if lib_doc.errors is not None:
1392-
for err in lib_doc.errors:
1393-
self._diagnostics.append(
1394-
Diagnostic(
1395-
range=Range(
1396-
start=Position(
1397-
line=((err.line_no - 1) if err.line_no is not None else 0),
1398-
character=0,
1399-
),
1400-
end=Position(
1401-
line=((err.line_no - 1) if err.line_no is not None else 0),
1402-
character=0,
1395+
if lib_doc.errors is not None:
1396+
for err in lib_doc.errors:
1397+
self._diagnostics.append(
1398+
Diagnostic(
1399+
range=Range(
1400+
start=Position(
1401+
line=((err.line_no - 1) if err.line_no is not None else 0),
1402+
character=0,
1403+
),
1404+
end=Position(
1405+
line=((err.line_no - 1) if err.line_no is not None else 0),
1406+
character=0,
1407+
),
14031408
),
1404-
),
1405-
message=err.message,
1406-
severity=DiagnosticSeverity.ERROR,
1407-
source=DIAGNOSTICS_SOURCE_NAME,
1408-
code=err.type_name,
1409+
message=err.message,
1410+
severity=DiagnosticSeverity.ERROR,
1411+
source=DIAGNOSTICS_SOURCE_NAME,
1412+
code=err.type_name,
1413+
)
14091414
)
1410-
)
1411-
finally:
1412-
self._analyzed = True
1415+
finally:
1416+
self._analyzed = True
14131417

14141418
async def find_keyword(self, name: Optional[str]) -> Optional[KeywordDoc]:
14151419
await self.ensure_initialized()
14161420

14171421
return await KeywordFinder(self).find_keyword(name)
14181422

1419-
async def find_keyword_threadsafe(self, name: Optional[str]) -> Optional[KeywordDoc]:
1420-
return await asyncio.wrap_future(asyncio.run_coroutine_threadsafe(self.find_keyword(name), self._loop))
1421-
14221423

14231424
class DiagnosticsEntry(NamedTuple):
14241425
message: str
@@ -1475,7 +1476,6 @@ async def _find_keyword(self, name: Optional[str]) -> Optional[KeywordDoc]:
14751476
return result
14761477

14771478
async def _get_keyword_from_self(self, name: str) -> Optional[KeywordDoc]:
1478-
14791479
return (await self.namespace.get_library_doc()).keywords.get(name, None)
14801480

14811481
async def _yield_owner_and_kw_names(self, full_name: str) -> AsyncIterator[Tuple[str, ...]]:

0 commit comments

Comments
 (0)