Skip to content

Commit 9f92775

Browse files
committed
feat(langserver): code action - extract keyword
closes #158
1 parent 43a6dc0 commit 9f92775

File tree

8 files changed

+373
-97
lines changed

8 files changed

+373
-97
lines changed

packages/language_server/src/robotcode/language_server/common/parts/code_action.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -100,7 +100,7 @@ async def _text_document_code_action(
100100

101101
return results
102102

103-
@rpc_method(name="textDocument/codeAction/resolve", param_type=CodeAction)
103+
@rpc_method(name="codeAction/resolve", param_type=CodeAction)
104104
@threaded()
105105
async def _text_document_code_action_resolve(
106106
self,

packages/language_server/src/robotcode/language_server/robotframework/diagnostics/analyzer.py

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -36,9 +36,11 @@
3636
from robotcode.language_server.robotframework.utils.version import get_robot_version
3737

3838
from .entities import (
39+
ArgumentDefinition,
3940
CommandLineVariableDefinition,
4041
EnvironmentVariableDefinition,
4142
LibraryEntry,
43+
LocalVariableDefinition,
4244
ResourceEntry,
4345
VariableDefinition,
4446
VariableDefinitionType,
@@ -57,6 +59,7 @@ class AnalyzerResult:
5759
diagnostics: List[Diagnostic]
5860
keyword_references: Dict[KeywordDoc, Set[Location]]
5961
variable_references: Dict[VariableDefinition, Set[Location]]
62+
local_variable_assignments: Dict[VariableDefinition, Set[Range]]
6063
namespace_references: Dict[LibraryEntry, Set[Location]]
6164

6265

@@ -86,6 +89,7 @@ def __init__(
8689
self._diagnostics: List[Diagnostic] = []
8790
self._keyword_references: Dict[KeywordDoc, Set[Location]] = defaultdict(set)
8891
self._variable_references: Dict[VariableDefinition, Set[Location]] = defaultdict(set)
92+
self._local_variable_assignments: Dict[VariableDefinition, Set[Range]] = defaultdict(set)
8993
self._namespace_references: Dict[LibraryEntry, Set[Location]] = defaultdict(set)
9094

9195
async def run(self) -> AnalyzerResult:
@@ -95,7 +99,11 @@ async def run(self) -> AnalyzerResult:
9599
await self.visit(self.model)
96100

97101
return AnalyzerResult(
98-
self._diagnostics, self._keyword_references, self._variable_references, self._namespace_references
102+
self._diagnostics,
103+
self._keyword_references,
104+
self._variable_references,
105+
self._local_variable_assignments,
106+
self._namespace_references,
99107
)
100108

101109
def yield_argument_name_and_rest(self, node: ast.AST, token: Token) -> Iterator[Token]:
@@ -243,9 +251,7 @@ async def visit(self, node: ast.AST) -> None:
243251
if isinstance(var, EnvironmentVariableDefinition):
244252
var_token.value, _, _ = var_token.value.partition("=")
245253

246-
var_range = range_from_token(var_token)
247-
else:
248-
var_range = range_from_token(var_token)
254+
var_range = range_from_token(var_token)
249255

250256
suite_var = None
251257
if isinstance(var, CommandLineVariableDefinition):
@@ -267,6 +273,10 @@ async def visit(self, node: ast.AST) -> None:
267273
self._variable_references[suite_var].add(
268274
Location(self.namespace.document.document_uri, var_range)
269275
)
276+
if token1.type in [RobotToken.ASSIGN] and isinstance(
277+
var, (LocalVariableDefinition, ArgumentDefinition)
278+
):
279+
self._local_variable_assignments[var].add(var_range)
270280

271281
elif var not in self._variable_references and token1.type in [
272282
RobotToken.ASSIGN,

packages/language_server/src/robotcode/language_server/robotframework/diagnostics/namespace.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -558,6 +558,7 @@ def __init__(
558558
self._diagnostics: List[Diagnostic] = []
559559
self._keyword_references: Dict[KeywordDoc, Set[Location]] = {}
560560
self._variable_references: Dict[VariableDefinition, Set[Location]] = {}
561+
self._local_variable_assignments: Dict[VariableDefinition, Set[Range]] = {}
561562
self._namespace_references: Dict[LibraryEntry, Set[Location]] = {}
562563

563564
self._imported_keywords: Optional[List[KeywordDoc]] = None
@@ -711,6 +712,13 @@ async def get_variable_references(self) -> Dict[VariableDefinition, Set[Location
711712

712713
return self._variable_references
713714

715+
async def get_local_variable_assignments(self) -> Dict[VariableDefinition, Set[Range]]:
716+
await self.ensure_initialized()
717+
718+
await self._analyze()
719+
720+
return self._local_variable_assignments
721+
714722
async def get_namespace_references(self) -> Dict[LibraryEntry, Set[Location]]:
715723
await self.ensure_initialized()
716724

@@ -1577,6 +1585,7 @@ async def _analyze(self) -> None:
15771585
self._diagnostics += result.diagnostics
15781586
self._keyword_references = result.keyword_references
15791587
self._variable_references = result.variable_references
1588+
self._local_variable_assignments = result.local_variable_assignments
15801589
self._namespace_references = result.namespace_references
15811590

15821591
lib_doc = await self.get_library_doc()
Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
from __future__ import annotations
2+
3+
import ast
4+
from typing import List, Optional, Tuple
5+
6+
from robotcode.core.lsp.types import (
7+
Position,
8+
Range,
9+
)
10+
from robotcode.language_server.common.text_document import TextDocument
11+
from robotcode.language_server.robotframework.diagnostics.namespace import Namespace
12+
from robotcode.language_server.robotframework.utils.ast_utils import (
13+
range_from_node,
14+
)
15+
from robotcode.language_server.robotframework.utils.async_ast import Visitor
16+
17+
18+
class FindSectionsVisitor(Visitor):
19+
def __init__(self) -> None:
20+
self.keyword_sections: List[ast.AST] = []
21+
self.variable_sections: List[ast.AST] = []
22+
self.setting_sections: List[ast.AST] = []
23+
self.testcase_sections: List[ast.AST] = []
24+
self.sections: List[ast.AST] = []
25+
26+
def visit_KeywordSection(self, node: ast.AST) -> None: # noqa: N802
27+
self.keyword_sections.append(node)
28+
self.sections.append(node)
29+
30+
def visit_VariableSection(self, node: ast.AST) -> None: # noqa: N802
31+
self.variable_sections.append(node)
32+
self.sections.append(node)
33+
34+
def visit_SettingSection(self, node: ast.AST) -> None: # noqa: N802
35+
self.setting_sections.append(node)
36+
self.sections.append(node)
37+
38+
def visit_TestCaseSection(self, node: ast.AST) -> None: # noqa: N802
39+
self.testcase_sections.append(node)
40+
self.sections.append(node)
41+
42+
def visit_CommentSection(self, node: ast.AST) -> None: # noqa: N802
43+
self.sections.append(node)
44+
45+
46+
def find_keyword_sections(node: ast.AST) -> Optional[List[ast.AST]]:
47+
visitor = FindSectionsVisitor()
48+
visitor.visit(node)
49+
return visitor.keyword_sections if visitor.keyword_sections else None
50+
51+
52+
class CodeActionHelperMixin:
53+
async def create_insert_keyword_workspace_edit(
54+
self, document: TextDocument, model: ast.AST, namespace: Namespace, insert_text: str
55+
) -> Tuple[str, Range]:
56+
keyword_sections = find_keyword_sections(model)
57+
keyword_section = keyword_sections[-1] if keyword_sections else None
58+
59+
lines = document.get_lines()
60+
61+
if keyword_section is not None:
62+
node_range = range_from_node(keyword_section, skip_non_data=True, allow_comments=True)
63+
insert_pos = Position(node_range.end.line + 1, 0)
64+
insert_range = Range(insert_pos, insert_pos)
65+
insert_text = f"\n\n{insert_text}"
66+
else:
67+
if namespace.languages is None or not namespace.languages.languages:
68+
keywords_text = "Keywords"
69+
else:
70+
keywords_text = namespace.languages.languages[-1].keywords_header
71+
72+
insert_text = f"\n\n*** {keywords_text} ***\n{insert_text}"
73+
74+
end_line = len(lines) - 1
75+
while end_line >= 0 and not lines[end_line].strip():
76+
end_line -= 1
77+
doc_pos = Position(end_line + 1, 0)
78+
79+
insert_range = Range(doc_pos, doc_pos)
80+
81+
if insert_range.start.line >= len(lines) and lines[-1].strip():
82+
doc_pos = Position(len(lines) - 1, len(lines[-1]))
83+
insert_range = Range(doc_pos, doc_pos)
84+
insert_text = "\n" + insert_text
85+
86+
if insert_range.start.line <= len(lines) and lines[insert_range.start.line].startswith("*"):
87+
insert_text = insert_text + "\n\n"
88+
if (
89+
insert_range.start.line + 1 < len(lines)
90+
and not lines[insert_range.start.line].strip()
91+
and lines[insert_range.start.line + 1].startswith("*")
92+
):
93+
insert_text = insert_text + "\n"
94+
return insert_text, insert_range

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

Lines changed: 5 additions & 75 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
from __future__ import annotations
22

3-
import ast
43
from collections import defaultdict
54
from string import Template
65
from typing import TYPE_CHECKING, Any, List, Optional, Union, cast
@@ -35,8 +34,8 @@
3534
range_from_node,
3635
range_from_token,
3736
)
38-
from robotcode.language_server.robotframework.utils.async_ast import Visitor
3937

38+
from .code_action_helper_mixin import CodeActionHelperMixin, FindSectionsVisitor
4039
from .model_helper import ModelHelperMixin
4140
from .protocol_part import RobotLanguageServerProtocolPart
4241

@@ -62,41 +61,7 @@
6261
)
6362

6463

65-
class FindSectionsVisitor(Visitor):
66-
def __init__(self) -> None:
67-
self.keyword_sections: List[ast.AST] = []
68-
self.variable_sections: List[ast.AST] = []
69-
self.setting_sections: List[ast.AST] = []
70-
self.testcase_sections: List[ast.AST] = []
71-
self.sections: List[ast.AST] = []
72-
73-
def visit_KeywordSection(self, node: ast.AST) -> None: # noqa: N802
74-
self.keyword_sections.append(node)
75-
self.sections.append(node)
76-
77-
def visit_VariableSection(self, node: ast.AST) -> None: # noqa: N802
78-
self.variable_sections.append(node)
79-
self.sections.append(node)
80-
81-
def visit_SettingSection(self, node: ast.AST) -> None: # noqa: N802
82-
self.setting_sections.append(node)
83-
self.sections.append(node)
84-
85-
def visit_TestCaseSection(self, node: ast.AST) -> None: # noqa: N802
86-
self.testcase_sections.append(node)
87-
self.sections.append(node)
88-
89-
def visit_CommentSection(self, node: ast.AST) -> None: # noqa: N802
90-
self.sections.append(node)
91-
92-
93-
def find_keyword_sections(node: ast.AST) -> Optional[List[ast.AST]]:
94-
visitor = FindSectionsVisitor()
95-
visitor.visit(node)
96-
return visitor.keyword_sections if visitor.keyword_sections else None
97-
98-
99-
class RobotCodeActionQuickFixesProtocolPart(RobotLanguageServerProtocolPart, ModelHelperMixin):
64+
class RobotCodeActionQuickFixesProtocolPart(RobotLanguageServerProtocolPart, ModelHelperMixin, CodeActionHelperMixin):
10065
_logger = LoggingDescriptor()
10166

10267
def __init__(self, parent: RobotLanguageServerProtocol) -> None:
@@ -261,44 +226,9 @@ async def _apply_create_keyword(self, document: TextDocument, insert_text: str)
261226
model = await self.parent.documents_cache.get_model(document, False)
262227
namespace = await self.parent.documents_cache.get_namespace(document)
263228

264-
keyword_sections = find_keyword_sections(model)
265-
keyword_section = keyword_sections[-1] if keyword_sections else None
266-
267-
lines = document.get_lines()
268-
269-
if keyword_section is not None:
270-
node_range = range_from_node(keyword_section, skip_non_data=True, allow_comments=True)
271-
insert_pos = Position(node_range.end.line + 1, 0)
272-
insert_range = Range(insert_pos, insert_pos)
273-
insert_text = f"\n\n{insert_text}"
274-
else:
275-
if namespace.languages is None or not namespace.languages.languages:
276-
keywords_text = "Keywords"
277-
else:
278-
keywords_text = namespace.languages.languages[-1].keywords_header
279-
280-
insert_text = f"\n\n*** {keywords_text} ***\n{insert_text}"
281-
282-
end_line = len(lines) - 1
283-
while end_line >= 0 and not lines[end_line].strip():
284-
end_line -= 1
285-
doc_pos = Position(end_line + 1, 0)
286-
287-
insert_range = Range(doc_pos, doc_pos)
288-
289-
if insert_range.start.line >= len(lines) and lines[-1].strip():
290-
doc_pos = Position(len(lines) - 1, len(lines[-1]))
291-
insert_range = Range(doc_pos, doc_pos)
292-
insert_text = "\n" + insert_text
293-
294-
if insert_range.start.line <= len(lines) and lines[insert_range.start.line].startswith("*"):
295-
insert_text = insert_text + "\n\n"
296-
if (
297-
insert_range.start.line + 1 <= len(lines)
298-
and not lines[insert_range.start.line].strip()
299-
and lines[insert_range.start.line + 1].startswith("*")
300-
):
301-
insert_text = insert_text + "\n"
229+
insert_text, insert_range = await self.create_insert_keyword_workspace_edit(
230+
document, model, namespace, insert_text
231+
)
302232

303233
we = WorkspaceEdit(
304234
document_changes=[

0 commit comments

Comments
 (0)