Skip to content

Commit b140122

Browse files
committed
find references, highlight, rename tags
1 parent d1aad3a commit b140122

File tree

13 files changed

+285
-21
lines changed

13 files changed

+285
-21
lines changed

robotcode/jsonrpc2/protocol.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -581,7 +581,11 @@ def handle_response(self, message: JsonRPCResponse) -> None:
581581
if entry.future._loop == asyncio.get_running_loop():
582582
entry.future.set_result(res)
583583
else:
584-
entry.future._loop.call_soon_threadsafe(entry.future.set_result, res)
584+
if entry.future._loop.is_running():
585+
entry.future._loop.call_soon_threadsafe(entry.future.set_result, res)
586+
else:
587+
self._logger.warning("Response loop is not running.")
588+
585589
except (SystemExit, KeyboardInterrupt):
586590
raise
587591
except BaseException as e:

robotcode/language_server/common/parts/code_lens.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -93,4 +93,4 @@ async def refresh(self) -> None:
9393
):
9494
return
9595

96-
await self.parent.send_request_async("workspace/codeLens/refresh")
96+
return await self.parent.send_request("workspace/codeLens/refresh")

robotcode/language_server/robotframework/parts/codelens.py

Lines changed: 29 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
import ast
44
from typing import TYPE_CHECKING, Any, List, Optional, cast
55

6-
from ....utils.async_tools import threaded
6+
from ....utils.async_tools import create_sub_task, threaded
77
from ....utils.logging import LoggingDescriptor
88
from ...common.decorators import language_id
99
from ...common.lsp_types import CodeLens, Command
@@ -105,14 +105,35 @@ async def resolve(self, sender: Any, code_lens: CodeLens) -> Optional[CodeLens]:
105105
kw_doc = await self.get_keyword_definition_at_line(namespace, name, line)
106106

107107
if kw_doc is not None and not kw_doc.is_error_handler:
108-
references = await self.parent.robot_references.find_keyword_references(
108+
if not await self.parent.robot_references.has_cached_keyword_references(
109109
document, kw_doc, include_declaration=False
110-
)
111-
code_lens.command = Command(
112-
f"{len(references)} references",
113-
"editor.action.showReferences",
114-
[str(document.uri), code_lens.range.start, references],
115-
)
110+
):
111+
code_lens.command = Command(
112+
"...",
113+
"editor.action.showReferences",
114+
[str(document.uri), code_lens.range.start, []],
115+
)
116+
117+
async def find_refs() -> None:
118+
if document is None or kw_doc is None:
119+
return
120+
121+
await self.parent.robot_references.find_keyword_references(
122+
document, kw_doc, include_declaration=False
123+
)
124+
125+
await self.parent.code_lens.refresh()
126+
127+
create_sub_task(find_refs(), loop=self.parent.loop)
128+
else:
129+
references = await self.parent.robot_references.find_keyword_references(
130+
document, kw_doc, include_declaration=False
131+
)
132+
code_lens.command = Command(
133+
f"{len(references)} references",
134+
"editor.action.showReferences",
135+
[str(document.uri), code_lens.range.start, references],
136+
)
116137
else:
117138
code_lens.command = Command(
118139
"0 references",

robotcode/language_server/robotframework/parts/document_highlight.py

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -332,3 +332,39 @@ async def highlight_Template( # noqa: N802
332332
self, result_node: ast.AST, document: TextDocument, position: Position
333333
) -> Optional[List[DocumentHighlight]]:
334334
return await self._highlight_Template_or_TestTemplate(result_node, document, position)
335+
336+
async def _highlight_ForceTags_DefaultTags_Tags( # noqa: N802
337+
self, result_node: ast.AST, document: TextDocument, position: Position
338+
) -> Optional[List[DocumentHighlight]]:
339+
from robot.parsing.lexer.tokens import Token as RobotToken
340+
from robot.utils.normalizing import normalize
341+
342+
token = get_tokens_at_position(cast(HasTokens, result_node), position)[-1]
343+
344+
if token is None:
345+
return None
346+
347+
if token.type in [RobotToken.ARGUMENT] and token.value:
348+
return [
349+
DocumentHighlight(e.range, DocumentHighlightKind.TEXT)
350+
for e in await self.parent.robot_references.find_tag_references_in_file(
351+
document, normalize(token.value, ignore="_")
352+
)
353+
]
354+
355+
return None
356+
357+
async def highlight_ForceTags( # noqa: N802
358+
self, result_node: ast.AST, document: TextDocument, position: Position
359+
) -> Optional[List[DocumentHighlight]]:
360+
return await self._highlight_ForceTags_DefaultTags_Tags(result_node, document, position)
361+
362+
async def highlight_DefaultTags( # noqa: N802
363+
self, result_node: ast.AST, document: TextDocument, position: Position
364+
) -> Optional[List[DocumentHighlight]]:
365+
return await self._highlight_ForceTags_DefaultTags_Tags(result_node, document, position)
366+
367+
async def highlight_Tags( # noqa: N802
368+
self, result_node: ast.AST, document: TextDocument, position: Position
369+
) -> Optional[List[DocumentHighlight]]:
370+
return await self._highlight_ForceTags_DefaultTags_Tags(result_node, document, position)

robotcode/language_server/robotframework/parts/references.py

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
cast,
1919
)
2020

21+
from ....utils.async_cache import AsyncSimpleCache
2122
from ....utils.async_itertools import async_next
2223
from ....utils.async_tools import create_sub_task, run_coroutine_in_thread, threaded
2324
from ....utils.glob_path import iter_files
@@ -71,6 +72,8 @@ def __init__(self, parent: RobotLanguageServerProtocol) -> None:
7172

7273
parent.references.collect.add(self.collect)
7374

75+
self._keyword_reference_cache = AsyncSimpleCache(max_items=128)
76+
7477
def _find_method(self, cls: Type[Any]) -> Optional[_ReferencesMethod]:
7578
if cls is ast.AST:
7679
return None
@@ -733,9 +736,21 @@ async def get_keyword_references_from_any_run_keyword(
733736
async for e in self.get_keyword_references_from_tokens(namespace, kw_doc, node, t, args, True):
734737
yield e
735738

739+
async def has_cached_keyword_references(
740+
self, document: TextDocument, kw_doc: KeywordDoc, include_declaration: bool = True
741+
) -> bool:
742+
return await self._keyword_reference_cache.has(document, kw_doc, include_declaration)
743+
736744
async def find_keyword_references(
737745
self, document: TextDocument, kw_doc: KeywordDoc, include_declaration: bool = True
738746
) -> List[Location]:
747+
return await self._keyword_reference_cache.get(
748+
self._find_keyword_references, document, kw_doc, include_declaration
749+
)
750+
751+
async def _find_keyword_references(
752+
self, document: TextDocument, kw_doc: KeywordDoc, include_declaration: bool = True
753+
) -> List[Location]:
739754

740755
namespace = await self.parent.documents_cache.get_namespace(document)
741756
if namespace is None:
@@ -923,3 +938,63 @@ async def references_VariablesImport( # noqa: N802
923938
)
924939

925940
return None
941+
942+
async def find_tag_references_in_file(
943+
self, doc: TextDocument, tag: str, is_normalized: bool = False
944+
) -> List[Location]:
945+
from robot.parsing.lexer.tokens import Token as RobotToken
946+
from robot.parsing.model.statements import DefaultTags, ForceTags, Tags
947+
from robot.utils.normalizing import normalize
948+
949+
model = await self.parent.documents_cache.get_model(doc)
950+
if model is None:
951+
return []
952+
953+
result: List[Location] = []
954+
if not is_normalized:
955+
tag = normalize(tag, ignore="_")
956+
957+
async for node in iter_nodes(model):
958+
if isinstance(node, (ForceTags, DefaultTags, Tags)):
959+
for token in node.get_tokens(RobotToken.ARGUMENT):
960+
if token.value and normalize(token.value, ignore="_") == tag:
961+
result.append(Location(str(doc.uri), range_from_token(token)))
962+
963+
return result
964+
965+
async def _references_ForceTags_DefaultTags_Tags( # noqa: N802
966+
self, node: ast.AST, document: TextDocument, position: Position, context: ReferenceContext
967+
) -> Optional[List[Location]]:
968+
from robot.parsing.lexer.tokens import Token as RobotToken
969+
970+
token = get_tokens_at_position(cast(HasTokens, node), position)[-1]
971+
972+
if token is None:
973+
return None
974+
975+
if token.type in [RobotToken.ARGUMENT] and token.value:
976+
return await self.find_tag_references(document, token.value)
977+
978+
return None
979+
980+
async def find_tag_references(self, document: TextDocument, tag: str) -> List[Location]:
981+
from robot.utils.normalizing import normalize
982+
983+
return await self._find_references_in_workspace(
984+
document, self.find_tag_references_in_file, normalize(tag, ignore="_"), True
985+
)
986+
987+
async def references_ForceTags( # noqa: N802
988+
self, node: ast.AST, document: TextDocument, position: Position, context: ReferenceContext
989+
) -> Optional[List[Location]]:
990+
return await self._references_ForceTags_DefaultTags_Tags(node, document, position, context)
991+
992+
async def references_DefaultTags( # noqa: N802
993+
self, node: ast.AST, document: TextDocument, position: Position, context: ReferenceContext
994+
) -> Optional[List[Location]]:
995+
return await self._references_ForceTags_DefaultTags_Tags(node, document, position, context)
996+
997+
async def references_Tags( # noqa: N802
998+
self, node: ast.AST, document: TextDocument, position: Position, context: ReferenceContext
999+
) -> Optional[List[Location]]:
1000+
return await self._references_ForceTags_DefaultTags_Tags(node, document, position, context)

robotcode/language_server/robotframework/parts/rename.py

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -550,3 +550,79 @@ async def rename_Template( # noqa: N802
550550
return await self._rename_keyword(
551551
document, new_name, await self._find_Template_or_TestTemplate(node, document, position)
552552
)
553+
554+
async def _prepare_rename_tags( # noqa: N802
555+
self, node: ast.AST, document: TextDocument, position: Position
556+
) -> Optional[PrepareRenameResult]:
557+
from robot.parsing.lexer.tokens import Token as RobotToken
558+
559+
token = get_tokens_at_position(cast(HasTokens, node), position)[-1]
560+
561+
if token is None:
562+
return None
563+
564+
if token.type in [RobotToken.ARGUMENT] and token.value:
565+
return PrepareRenameResultWithPlaceHolder(range_from_token(token), token.value)
566+
567+
return None
568+
569+
async def prepare_rename_ForceTags( # noqa: N802
570+
self, node: ast.AST, document: TextDocument, position: Position
571+
) -> Optional[PrepareRenameResult]:
572+
return await self._prepare_rename_tags(node, document, position)
573+
574+
async def prepare_rename_DefaultTags( # noqa: N802
575+
self, node: ast.AST, document: TextDocument, position: Position
576+
) -> Optional[PrepareRenameResult]:
577+
return await self._prepare_rename_tags(node, document, position)
578+
579+
async def prepare_rename_Tags( # noqa: N802
580+
self, node: ast.AST, document: TextDocument, position: Position
581+
) -> Optional[PrepareRenameResult]:
582+
return await self._prepare_rename_tags(node, document, position)
583+
584+
async def _rename_tags( # noqa: N802
585+
self, node: ast.AST, document: TextDocument, position: Position, new_name: str
586+
) -> Optional[WorkspaceEdit]:
587+
588+
from robot.parsing.lexer.tokens import Token as RobotToken
589+
590+
token = get_tokens_at_position(cast(HasTokens, node), position)[-1]
591+
592+
if token is None:
593+
return None
594+
595+
if token.type in [RobotToken.ARGUMENT] and token.value:
596+
references = await self.parent.robot_references.find_tag_references(document, token.value)
597+
598+
changes: List[Union[TextDocumentEdit, CreateFile, RenameFile, DeleteFile]] = []
599+
600+
for reference in references:
601+
changes.append(
602+
TextDocumentEdit(
603+
OptionalVersionedTextDocumentIdentifier(reference.uri, None),
604+
[AnnotatedTextEdit(reference.range, new_name, annotation_id="rename_tag")],
605+
)
606+
)
607+
608+
return WorkspaceEdit(
609+
document_changes=changes,
610+
change_annotations={"rename_keyword": ChangeAnnotation("Rename Tag", False)},
611+
)
612+
613+
return None
614+
615+
async def rename_ForceTags( # noqa: N802
616+
self, node: ast.AST, document: TextDocument, position: Position, new_name: str
617+
) -> Optional[WorkspaceEdit]:
618+
return await self._rename_tags(node, document, position, new_name)
619+
620+
async def rename_DefaultTags( # noqa: N802
621+
self, node: ast.AST, document: TextDocument, position: Position, new_name: str
622+
) -> Optional[WorkspaceEdit]:
623+
return await self._rename_tags(node, document, position, new_name)
624+
625+
async def rename_Tags( # noqa: N802
626+
self, node: ast.AST, document: TextDocument, position: Position, new_name: str
627+
) -> Optional[WorkspaceEdit]:
628+
return await self._rename_tags(node, document, position, new_name)

robotcode/language_server/robotframework/parts/robot_workspace.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,7 @@ async def _collect_workspace_documents(self, sender: Any) -> List[WorkspaceDocum
102102
self._logger.exception(e)
103103

104104
self.workspace_loaded = True
105+
105106
if config.analysis.references_code_lens:
106107
await self.parent.code_lens.refresh()
107108

robotcode/utils/async_cache.py

Lines changed: 22 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
from collections import defaultdict
2+
from dataclasses import dataclass
13
from typing import Any, Awaitable, Callable, Dict, List, Tuple, TypeVar, cast
24

35
from .async_tools import Lock
@@ -7,37 +9,47 @@
79

810
def _freeze(v: Any) -> Any:
911
if isinstance(v, dict):
10-
return frozenset(v.items())
12+
return hash(frozenset(v.items()))
1113
return v
1214

1315

16+
@dataclass
17+
class CacheEntry:
18+
value: Any = None
19+
has_value: bool = False
20+
lock: Lock = Lock()
21+
22+
1423
class AsyncSimpleCache:
1524
def __init__(self, max_items: int = 128) -> None:
1625
self.max_items = max_items
1726

18-
self._cache: Dict[Tuple[Any, ...], Any] = {}
27+
self._cache: Dict[Tuple[Any, ...], CacheEntry] = defaultdict(CacheEntry)
1928
self._order: List[Tuple[Any, ...]] = []
2029
self._lock = Lock()
2130

31+
async def has(self, *args: Any, **kwargs: Any) -> bool:
32+
return self._make_key(*args, **kwargs) in self._cache
33+
2234
async def get(self, func: Callable[..., Awaitable[_T]], *args: Any, **kwargs: Any) -> _T:
2335
key = self._make_key(*args, **kwargs)
2436

2537
async with self._lock:
26-
try:
27-
return cast(_T, self._cache[key])
28-
except KeyError:
29-
pass
38+
entry = self._cache[key]
39+
if entry.has_value:
40+
return cast(_T, entry.value)
3041

31-
res = await func(*args, **kwargs)
42+
async with entry.lock:
43+
entry.value = await func(*args, **kwargs)
44+
entry.has_value = True
3245

33-
self._cache[key] = res
3446
self._order.insert(0, key)
3547

3648
if len(self._order) > self.max_items:
3749
del self._cache[self._order.pop()]
3850

39-
return res
51+
return entry.value
4052

4153
@staticmethod
4254
def _make_key(*args: Any, **kwargs: Any) -> Tuple[Any, ...]:
43-
return (tuple(_freeze(v) for v in args), frozenset({k: _freeze(v) for k, v in kwargs.items()}))
55+
return (tuple(_freeze(v) for v in args), hash(frozenset({k: _freeze(v) for k, v in kwargs.items()})))
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
11
*** Settings ***
22
Resource firstresource.resource
33

4-
Suite Setup do something in a resource
4+
Suite Setup do something in a resource
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
*** Settings ***
2+
Default Tags blah-tag bluf-tag
3+
4+
5+
*** Test Cases ***
6+
first
7+
[Tags] no-ci_1
8+
No Operation
9+
10+
second
11+
[Tags] unknown 1
12+
No Operation

0 commit comments

Comments
 (0)