Skip to content

Commit f11d37e

Browse files
committed
start implementing rename support, some code generalization
1 parent 43b3070 commit f11d37e

File tree

16 files changed

+742
-151
lines changed

16 files changed

+742
-151
lines changed

robotcode/language_server/common/decorators.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
from typing import Any, Callable, List, Protocol, TypeVar, runtime_checkable
22

3-
from robotcode.language_server.common.text_document import TextDocument
3+
from .text_document import TextDocument
44

55
_F = TypeVar("_F", bound=Callable[..., Any])
66

robotcode/language_server/common/lsp_types.py

Lines changed: 43 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -965,6 +965,16 @@ class SelectionRangeRegistrationOptions(
965965
pass
966966

967967

968+
@dataclass(repr=False)
969+
class RenameOptions(WorkDoneProgressOptions):
970+
prepare_provider: Optional[bool] = None
971+
972+
973+
@dataclass(repr=False)
974+
class RenameRegistrationOptions(TextDocumentRegistrationOptions, RenameOptions, StaticRegistrationOptions):
975+
pass
976+
977+
968978
@dataclass(repr=False)
969979
class ServerCapabilities(Model):
970980
text_document_sync: Union[TextDocumentSyncOptions, TextDocumentSyncKind, None] = None
@@ -984,7 +994,7 @@ class ServerCapabilities(Model):
984994
document_formatting_provider: Union[bool, DocumentFormattingOptions, None] = None
985995
document_range_formatting_provider: Union[bool, DocumentRangeFormattingOptions, None] = None
986996
# TODO document_on_type_formatting_provider: Optional[DocumentOnTypeFormattingOptions] = None
987-
# TODO rename_provider: Union[bool, RenameOptions, None] = None
997+
rename_provider: Union[bool, RenameOptions, None] = None
988998
folding_range_provider: Union[bool, FoldingRangeOptions, FoldingRangeRegistrationOptions, None] = None
989999
execute_command_provider: Optional[ExecuteCommandOptions] = None
9901000
selection_range_provider: Union[bool, SelectionRangeOptions, SelectionRangeRegistrationOptions, None] = None
@@ -1139,7 +1149,7 @@ class TextDocumentIdentifier(Model):
11391149

11401150
@dataclass(repr=False)
11411151
class OptionalVersionedTextDocumentIdentifier(TextDocumentIdentifier):
1142-
version: Optional[int] = None
1152+
version: Optional[int]
11431153

11441154

11451155
@dataclass(repr=False)
@@ -1429,7 +1439,7 @@ class AnnotatedTextEdit(TextEdit):
14291439
@dataclass(repr=False)
14301440
class TextDocumentEdit(Model):
14311441
text_document: OptionalVersionedTextDocumentIdentifier
1432-
edits: Union[TextEdit, AnnotatedTextEdit]
1442+
edits: List[Union[TextEdit, AnnotatedTextEdit]]
14331443

14341444

14351445
@dataclass(repr=False)
@@ -1442,7 +1452,7 @@ class ChangeAnnotation(Model):
14421452
@dataclass(repr=False)
14431453
class WorkspaceEdit(Model):
14441454
changes: Optional[Dict[DocumentUri, List[TextEdit]]] = None
1445-
document_changes: Union[List[TextDocumentEdit], TextDocumentEdit, CreateFile, RenameFile, DeleteFile, None] = None
1455+
document_changes: Optional[List[Union[TextDocumentEdit, CreateFile, RenameFile, DeleteFile]]] = None
14461456
change_annotations: Optional[Dict[ChangeAnnotationIdentifier, ChangeAnnotation]] = None
14471457

14481458

@@ -1978,3 +1988,32 @@ class _WorkDoneProgressEnd(Model):
19781988
@dataclass(repr=False)
19791989
class WorkDoneProgressEnd(WorkDoneProgressBase, _WorkDoneProgressEnd):
19801990
kind: Literal["end"] = "end"
1991+
1992+
1993+
@dataclass(repr=False)
1994+
class _RenameParams(Model):
1995+
new_name: str
1996+
1997+
1998+
@dataclass(repr=False)
1999+
class RenameParams(WorkDoneProgressParams, TextDocumentPositionParams, _RenameParams):
2000+
pass
2001+
2002+
2003+
@dataclass(repr=False)
2004+
class PrepareRenameParams(TextDocumentPositionParams):
2005+
pass
2006+
2007+
2008+
@dataclass(repr=False)
2009+
class PrepareRenameResultWithPlaceHolder(Model):
2010+
range: Range
2011+
placeholder: str
2012+
2013+
2014+
@dataclass(repr=False)
2015+
class PrepareRenameResultWithDefaultBehavior(Model):
2016+
default_behavior: bool
2017+
2018+
2019+
PrepareRenameResult = Union[Range, PrepareRenameResultWithPlaceHolder, PrepareRenameResultWithDefaultBehavior]
Lines changed: 130 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,130 @@
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_tools import async_tasking_event
8+
from ....utils.logging import LoggingDescriptor
9+
from ..decorators import language_id_filter
10+
from ..has_extend_capabilities import HasExtendCapabilities
11+
from ..lsp_types import (
12+
Position,
13+
PrepareRenameParams,
14+
PrepareRenameResult,
15+
RenameOptions,
16+
RenameParams,
17+
ServerCapabilities,
18+
TextDocumentIdentifier,
19+
WorkspaceEdit,
20+
)
21+
from ..text_document import TextDocument
22+
23+
if TYPE_CHECKING:
24+
from ..protocol import LanguageServerProtocol
25+
26+
from .protocol_part import LanguageServerProtocolPart
27+
28+
29+
class RenameProtocolPart(LanguageServerProtocolPart, HasExtendCapabilities):
30+
31+
_logger = LoggingDescriptor()
32+
33+
def __init__(self, parent: LanguageServerProtocol) -> None:
34+
super().__init__(parent)
35+
36+
def extend_capabilities(self, capabilities: ServerCapabilities) -> None:
37+
if len(self.collect):
38+
capabilities.rename_provider = RenameOptions(
39+
prepare_provider=len(self.collect_prepare) > 0, work_done_progress=True
40+
)
41+
42+
@async_tasking_event
43+
async def collect(
44+
sender, document: TextDocument, position: Position, new_name: str # NOSONAR
45+
) -> Optional[WorkspaceEdit]:
46+
...
47+
48+
@async_tasking_event
49+
async def collect_prepare(
50+
sender, document: TextDocument, position: Position # NOSONAR
51+
) -> Optional[PrepareRenameResult]:
52+
...
53+
54+
@rpc_method(name="textDocument/rename", param_type=RenameParams)
55+
async def _text_document_rename(
56+
self,
57+
text_document: TextDocumentIdentifier,
58+
position: Position,
59+
new_name: str,
60+
*args: Any,
61+
**kwargs: Any,
62+
) -> Optional[WorkspaceEdit]:
63+
64+
edits: List[WorkspaceEdit] = []
65+
66+
document = await self.parent.documents.get(text_document.uri)
67+
if document is None:
68+
return None
69+
70+
for result in await self.collect(
71+
self, document, position, new_name, callback_filter=language_id_filter(document)
72+
):
73+
if isinstance(result, BaseException):
74+
if not isinstance(result, CancelledError):
75+
self._logger.exception(result, exc_info=result)
76+
else:
77+
if result is not None:
78+
edits.append(result)
79+
80+
if len(edits) == 0:
81+
return None
82+
83+
result = WorkspaceEdit()
84+
for we in edits:
85+
if we.changes:
86+
if result.changes is None:
87+
result.changes = {}
88+
result.changes.update(we.changes)
89+
90+
if we.document_changes:
91+
if result.document_changes is None:
92+
result.document_changes = []
93+
result.document_changes.extend(we.document_changes)
94+
95+
if we.change_annotations:
96+
if result.change_annotations is None:
97+
result.change_annotations = {}
98+
result.change_annotations.update(we.change_annotations)
99+
100+
return result
101+
102+
@rpc_method(name="textDocument/prepareRename", param_type=PrepareRenameParams)
103+
async def _text_document_prepare_rename(
104+
self,
105+
text_document: TextDocumentIdentifier,
106+
position: Position,
107+
*args: Any,
108+
**kwargs: Any,
109+
) -> Optional[PrepareRenameResult]:
110+
111+
results: List[PrepareRenameResult] = []
112+
113+
document = await self.parent.documents.get(text_document.uri)
114+
if document is None:
115+
return None
116+
117+
for result in await self.collect_prepare(
118+
self, document, position, callback_filter=language_id_filter(document)
119+
):
120+
if isinstance(result, BaseException):
121+
if not isinstance(result, CancelledError):
122+
self._logger.exception(result, exc_info=result)
123+
else:
124+
if result is not None:
125+
results.append(result)
126+
127+
if len(results) == 0:
128+
return None
129+
130+
return results[-1]

robotcode/language_server/common/protocol.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@
5151
from .parts.implementation import ImplementationProtocolPart
5252
from .parts.linked_editing_ranges import LinkedEditingRangeProtocolPart
5353
from .parts.references import ReferencesProtocolPart
54+
from .parts.rename import RenameProtocolPart
5455
from .parts.selection_range import SelectionRangeProtocolPart
5556
from .parts.semantic_tokens import SemanticTokensProtocolPart
5657
from .parts.signature_help import SignatureHelpProtocolPart
@@ -86,6 +87,7 @@ class LanguageServerProtocol(JsonRPCProtocol):
8687
document_highlight = ProtocolPartDescriptor(DocumentHighlightProtocolPart)
8788
linked_editing_range = ProtocolPartDescriptor(LinkedEditingRangeProtocolPart)
8889
selection_range = ProtocolPartDescriptor(SelectionRangeProtocolPart)
90+
rename = ProtocolPartDescriptor(RenameProtocolPart)
8991

9092
name: Optional[str] = None
9193
version: Optional[str] = None

robotcode/language_server/robotframework/diagnostics/library_doc.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -229,7 +229,7 @@ class ArgumentSpec:
229229
defaults: Any
230230
types: Any
231231

232-
__robot_arguments: Optional[Any] = None
232+
__robot_arguments: Optional[Any] = field(default=None, compare=False)
233233

234234
@staticmethod
235235
def from_robot_argument_spec(spec: Any) -> ArgumentSpec:
@@ -470,6 +470,9 @@ def get(self, key: str, default: Optional[KeywordDoc] = None) -> Optional[Keywor
470470
except KeyError:
471471
return default
472472

473+
def get_all(self, key: str) -> List[KeywordDoc]:
474+
return [v for k, v in self._matchers.items() if k == key]
475+
473476

474477
@dataclass
475478
class ModuleSpec:

robotcode/language_server/robotframework/parts/document_highlight.py

Lines changed: 11 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -194,17 +194,8 @@ async def highlight_KeywordCall( # noqa: N802
194194
and keyword_doc.source
195195
):
196196
return [
197-
*(
198-
[DocumentHighlight(keyword_doc.range, DocumentHighlightKind.TEXT)]
199-
if keyword_doc.source == str(document.uri.to_path())
200-
else []
201-
),
202-
*(
203-
DocumentHighlight(e.range, DocumentHighlightKind.TEXT)
204-
for e in await self.parent.robot_references.find_keyword_references_in_file(
205-
document, keyword_doc
206-
)
207-
),
197+
DocumentHighlight(e.range, DocumentHighlightKind.TEXT)
198+
for e in await self.parent.robot_references.find_keyword_references_in_file(document, keyword_doc)
208199
]
209200

210201
return None
@@ -228,18 +219,12 @@ async def highlight_KeywordName( # noqa: N802
228219

229220
doc = await namespace.get_library_doc()
230221
if doc is not None:
231-
keyword = next(
232-
(v for v in doc.keywords.keywords if v.name == name_token.value and v.line_no == kw_node.lineno),
233-
None,
234-
)
222+
keyword = await self.get_keyword_definition_at_token(namespace, name_token)
235223

236224
if keyword is not None and keyword.source and not keyword.is_error_handler:
237225
return [
238-
DocumentHighlight(keyword.range, DocumentHighlightKind.TEXT),
239-
*(
240-
DocumentHighlight(e.range, DocumentHighlightKind.TEXT)
241-
for e in await self.parent.robot_references.find_keyword_references_in_file(document, keyword)
242-
),
226+
DocumentHighlight(e.range, DocumentHighlightKind.TEXT)
227+
for e in await self.parent.robot_references.find_keyword_references_in_file(document, keyword)
243228
]
244229

245230
return None
@@ -287,17 +272,8 @@ async def highlight_Fixture( # noqa: N802
287272

288273
if position in kw_range and keyword_doc is not None and not keyword_doc.is_error_handler:
289274
return [
290-
*(
291-
[DocumentHighlight(keyword_doc.range, DocumentHighlightKind.TEXT)]
292-
if keyword_doc.source == str(document.uri.to_path())
293-
else []
294-
),
295-
*(
296-
DocumentHighlight(e.range, DocumentHighlightKind.TEXT)
297-
for e in await self.parent.robot_references.find_keyword_references_in_file(
298-
document, keyword_doc
299-
)
300-
),
275+
DocumentHighlight(e.range, DocumentHighlightKind.TEXT)
276+
for e in await self.parent.robot_references.find_keyword_references_in_file(document, keyword_doc)
301277
]
302278

303279
return None
@@ -340,17 +316,10 @@ async def _highlight_Template_or_TestTemplate( # noqa: N802
340316

341317
if not keyword_doc.is_error_handler:
342318
return [
343-
*(
344-
[DocumentHighlight(keyword_doc.range, DocumentHighlightKind.TEXT)]
345-
if keyword_doc.source == str(document.uri.to_path())
346-
else []
347-
),
348-
*(
349-
DocumentHighlight(e.range, DocumentHighlightKind.TEXT)
350-
for e in await self.parent.robot_references.find_keyword_references_in_file(
351-
document, keyword_doc
352-
)
353-
),
319+
DocumentHighlight(e.range, DocumentHighlightKind.TEXT)
320+
for e in await self.parent.robot_references.find_keyword_references_in_file(
321+
document, keyword_doc
322+
)
354323
]
355324
return None
356325

robotcode/language_server/robotframework/parts/goto.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -254,7 +254,7 @@ async def definition_KeywordName( # noqa: N802
254254
if not name_token:
255255
return None
256256

257-
result = await namespace.find_keyword(name_token.value)
257+
result = await self.get_keyword_definition_at_token(namespace, name_token)
258258

259259
if result is not None and not result.is_error_handler and result.source:
260260
token_range = range_from_token(name_token)

robotcode/language_server/robotframework/parts/hover.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -537,11 +537,11 @@ async def hover_KeywordName( # noqa: N802
537537
return None
538538

539539
kw_node = cast(KeywordName, node)
540-
name_token = kw_node.get_token(RobotToken.KEYWORD_NAME)
540+
name_token = cast(RobotToken, kw_node.get_token(RobotToken.KEYWORD_NAME))
541541
if not name_token:
542542
return None
543543

544-
result = (await namespace.get_library_doc()).keywords.get(name_token.value, None)
544+
result = await self.get_keyword_definition_at_token(namespace, name_token)
545545

546546
if result is not None and not result.is_error_handler:
547547
return Hover(

robotcode/language_server/robotframework/parts/model_helper.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -514,3 +514,10 @@ def strip_bdd_prefix(cls, token: Token) -> Token:
514514
def is_bdd_token(cls, token: Token) -> bool:
515515
bdd_match = cls.BDD_TOKEN.match(token.value)
516516
return bool(bdd_match)
517+
518+
@classmethod
519+
async def get_keyword_definition_at_token(cls, namespace: Namespace, token: Token) -> Optional[KeywordDoc]:
520+
return next(
521+
(k for k in (await namespace.get_library_doc()).keywords.get_all(token.value) if k.line_no == token.lineno),
522+
None,
523+
)

0 commit comments

Comments
 (0)