Skip to content

Commit bba00aa

Browse files
committed
feat(langserver): new code action quick fixes - assign kw result to variable, create local variable, disable robot code diagnostics for line
1 parent dc71f0e commit bba00aa

File tree

3 files changed

+282
-33
lines changed

3 files changed

+282
-33
lines changed

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

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -199,9 +199,6 @@ def server_bind(self) -> None:
199199
return super().server_bind()
200200

201201

202-
CODEACTIONKINDS_SOURCE_OPENDOCUMENTATION = f"{CodeActionKind.SOURCE.value}.openDocumentation"
203-
204-
205202
class RobotCodeActionDocumentationProtocolPart(RobotLanguageServerProtocolPart, ModelHelperMixin):
206203
_logger = LoggingDescriptor()
207204

@@ -256,7 +253,7 @@ async def _ensure_http_server_started(self) -> None:
256253
@language_id("robotframework")
257254
@code_action_kinds(
258255
[
259-
CODEACTIONKINDS_SOURCE_OPENDOCUMENTATION,
256+
CodeActionKind.SOURCE,
260257
]
261258
)
262259
@_logger.call
@@ -383,7 +380,7 @@ async def collect(
383380
def open_documentation_code_action(self, url: str) -> CodeAction:
384381
return CodeAction(
385382
"Open Documentation",
386-
kind=CODEACTIONKINDS_SOURCE_OPENDOCUMENTATION,
383+
kind=CodeActionKind.SOURCE,
387384
command=Command(
388385
"Open Documentation",
389386
"robotcode.showDocumentation",

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

Lines changed: 279 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,13 @@
2323
from robotcode.core.utils.inspect import iter_methods
2424
from robotcode.language_server.common.decorators import code_action_kinds, command, language_id
2525
from robotcode.language_server.common.text_document import TextDocument
26-
from robotcode.language_server.robotframework.utils.ast_utils import Token, get_node_at_position, range_from_node
26+
from robotcode.language_server.robotframework.utils.ast_utils import (
27+
Token,
28+
get_node_at_position,
29+
get_nodes_at_position,
30+
range_from_node,
31+
range_from_token,
32+
)
2733
from robotcode.language_server.robotframework.utils.async_ast import Visitor
2834

2935
from .model_helper import ModelHelperMixin
@@ -78,7 +84,7 @@ def __init__(self, parent: RobotLanguageServerProtocol) -> None:
7884
self.parent.commands.register_all(self)
7985

8086
@language_id("robotframework")
81-
@code_action_kinds([CodeActionKind.QUICK_FIX])
87+
@code_action_kinds([CodeActionKind.QUICK_FIX, "other"])
8288
async def collect(
8389
self, sender: Any, document: TextDocument, range: Range, context: CodeActionContext
8490
) -> Optional[List[Union[Command, CodeAction]]]:
@@ -96,24 +102,31 @@ async def collect(
96102
async def code_action_create_keyword(
97103
self, sender: Any, document: TextDocument, range: Range, context: CodeActionContext
98104
) -> Optional[List[Union[Command, CodeAction]]]:
99-
kw_not_found_in_diagnostics = next((d for d in context.diagnostics if d.code == "KeywordNotFoundError"), None)
100-
101-
if kw_not_found_in_diagnostics and (
102-
(context.only and CodeActionKind.QUICK_FIX.value in context.only)
105+
if range.start == range.end and (
106+
(context.only and CodeActionKind.QUICK_FIX in context.only)
103107
or context.trigger_kind in [CodeActionTriggerKind.INVOKED, CodeActionTriggerKind.AUTOMATIC]
104108
):
105-
return [
106-
CodeAction(
107-
"Create Keyword",
108-
kind=CodeActionKind.QUICK_FIX,
109-
command=Command(
110-
self.parent.commands.get_command_name(self.create_keyword_command),
111-
self.parent.commands.get_command_name(self.create_keyword_command),
112-
[document.document_uri, range],
113-
),
114-
diagnostics=[kw_not_found_in_diagnostics],
115-
)
116-
]
109+
diagnostics = next(
110+
(
111+
d
112+
for d in context.diagnostics
113+
if d.source == "robotcode.namespace" and d.code == "KeywordNotFoundError"
114+
),
115+
None,
116+
)
117+
if diagnostics is not None:
118+
return [
119+
CodeAction(
120+
"Create Keyword",
121+
kind=CodeActionKind.QUICK_FIX,
122+
command=Command(
123+
self.parent.commands.get_command_name(self.create_keyword_command),
124+
self.parent.commands.get_command_name(self.create_keyword_command),
125+
[document.document_uri, range],
126+
),
127+
diagnostics=[diagnostics],
128+
)
129+
]
117130

118131
return None
119132

@@ -133,8 +146,6 @@ async def create_keyword_command(self, document_uri: DocumentUri, range: Range)
133146
if document is None:
134147
return
135148

136-
namespace = await self.parent.documents_cache.get_namespace(document)
137-
138149
model = await self.parent.documents_cache.get_model(document, False)
139150
node = await get_node_at_position(model, range.start)
140151

@@ -148,6 +159,8 @@ async def create_keyword_command(self, document_uri: DocumentUri, range: Range)
148159
if keyword_token is None:
149160
return
150161

162+
namespace = await self.parent.documents_cache.get_namespace(document)
163+
151164
bdd_token, token = self.split_bdd_prefix(namespace, keyword_token)
152165
if bdd_token is not None and token is not None:
153166
keyword = token.value
@@ -205,11 +218,250 @@ async def create_keyword_command(self, document_uri: DocumentUri, range: Range)
205218
change_annotations={"create_keyword": ChangeAnnotation("Create Keyword", False)},
206219
)
207220

208-
await self.parent.workspace.apply_edit(we, "Rename Keyword")
221+
if (await self.parent.workspace.apply_edit(we)).applied:
222+
lines = insert_text.rstrip().splitlines()
223+
insert_range.start.line += len(lines) - 1
224+
insert_range.start.character = 4
225+
insert_range.end = Position(insert_range.start.line, insert_range.start.character)
226+
insert_range.end.character += len(lines[-1])
227+
await self.parent.window.show_document(str(document.uri), take_focus=True, selection=insert_range)
228+
229+
async def code_action_assign_result_to_variable(
230+
self, sender: Any, document: TextDocument, range: Range, context: CodeActionContext
231+
) -> Optional[List[Union[Command, CodeAction]]]:
232+
from robot.parsing.lexer import Token as RobotToken
233+
from robot.parsing.model.statements import (
234+
Fixture,
235+
KeywordCall,
236+
Template,
237+
TestTemplate,
238+
)
239+
240+
if range.start == range.end and (
241+
(context.only and "other" in context.only)
242+
or context.trigger_kind in [CodeActionTriggerKind.INVOKED, CodeActionTriggerKind.AUTOMATIC]
243+
):
244+
model = await self.parent.documents_cache.get_model(document, False)
245+
node = await get_node_at_position(model, range.start)
246+
247+
if not isinstance(node, KeywordCall) or node.assign:
248+
return None
249+
250+
keyword_token = (
251+
node.get_token(RobotToken.NAME)
252+
if isinstance(node, (TestTemplate, Template, Fixture))
253+
else node.get_token(RobotToken.KEYWORD)
254+
)
255+
256+
if keyword_token is None or range.start not in range_from_token(keyword_token):
257+
return None
258+
259+
return [
260+
CodeAction(
261+
"Assign Result To Variable",
262+
kind="other",
263+
command=Command(
264+
self.parent.commands.get_command_name(self.assign_result_to_variable_command),
265+
self.parent.commands.get_command_name(self.assign_result_to_variable_command),
266+
[document.document_uri, range],
267+
),
268+
)
269+
]
270+
271+
return None
272+
273+
@command("robotcode.assignResultToVariable")
274+
async def assign_result_to_variable_command(self, document_uri: DocumentUri, range: Range) -> None:
275+
from robot.parsing.lexer import Token as RobotToken
276+
from robot.parsing.model.statements import (
277+
Fixture,
278+
KeywordCall,
279+
Template,
280+
TestTemplate,
281+
)
282+
283+
if range.start == range.end:
284+
document = await self.parent.documents.get(document_uri)
285+
if document is None:
286+
return
287+
288+
model = await self.parent.documents_cache.get_model(document, False)
289+
node = await get_node_at_position(model, range.start)
290+
291+
if not isinstance(node, KeywordCall) or node.assign:
292+
return
293+
294+
keyword_token = (
295+
node.get_token(RobotToken.NAME)
296+
if isinstance(node, (TestTemplate, Template, Fixture))
297+
else node.get_token(RobotToken.KEYWORD)
298+
)
299+
300+
if keyword_token is None or range.start not in range_from_token(keyword_token):
301+
return
302+
303+
start = range_from_token(keyword_token).start
304+
we = WorkspaceEdit(
305+
document_changes=[
306+
TextDocumentEdit(
307+
OptionalVersionedTextDocumentIdentifier(str(document.uri), document.version),
308+
[AnnotatedTextEdit("assign_result_to_variable", Range(start, start), "${result} ")],
309+
)
310+
],
311+
change_annotations={"assign_result_to_variable": ChangeAnnotation("Assign result to variable", False)},
312+
)
313+
314+
if (await self.parent.workspace.apply_edit(we)).applied:
315+
insert_range = Range(start, start).extend(start_character=2, end_character=8)
316+
317+
await self.parent.window.show_document(str(document.uri), take_focus=True, selection=insert_range)
318+
319+
async def code_action_create_local_variable(
320+
self, sender: Any, document: TextDocument, range: Range, context: CodeActionContext
321+
) -> Optional[List[Union[Command, CodeAction]]]:
322+
from robot.parsing.model.blocks import Keyword, TestCase
323+
from robot.parsing.model.statements import Documentation, Fixture, Statement, Template
324+
325+
if range.start == range.end and (
326+
(context.only and CodeActionKind.QUICK_FIX in context.only)
327+
or context.trigger_kind in [CodeActionTriggerKind.INVOKED, CodeActionTriggerKind.AUTOMATIC]
328+
):
329+
diagnostics = next(
330+
(d for d in context.diagnostics if d.source == "robotcode.namespace" and d.code == "VariableNotFound"),
331+
None,
332+
)
333+
if (
334+
diagnostics is not None
335+
and diagnostics.range.start.line == diagnostics.range.end.line
336+
and diagnostics.range.start.character < diagnostics.range.end.character
337+
):
338+
model = await self.parent.documents_cache.get_model(document, False)
339+
nodes = await get_nodes_at_position(model, range.start)
340+
341+
if not any(n for n in nodes if isinstance(n, (Keyword, TestCase))):
342+
return None
343+
344+
node = nodes[-1] if nodes else None
345+
if node is None or isinstance(node, (Documentation, Fixture, Template)):
346+
return None
347+
348+
if not isinstance(node, Statement):
349+
return None
350+
return [
351+
CodeAction(
352+
"Create Local Variable",
353+
kind=CodeActionKind.QUICK_FIX,
354+
command=Command(
355+
self.parent.commands.get_command_name(self.create_local_variable_command),
356+
self.parent.commands.get_command_name(self.create_local_variable_command),
357+
[document.document_uri, diagnostics.range],
358+
),
359+
diagnostics=[diagnostics],
360+
)
361+
]
362+
363+
return None
364+
365+
@command("robotcode.createLocalVariable")
366+
async def create_local_variable_command(self, document_uri: DocumentUri, range: Range) -> None:
367+
from robot.parsing.model.blocks import Keyword, TestCase
368+
from robot.parsing.model.statements import Documentation, Fixture, Statement, Template
369+
370+
if range.start.line == range.end.line and range.start.character < range.end.character:
371+
document = await self.parent.documents.get(document_uri)
372+
if document is None:
373+
return
374+
375+
model = await self.parent.documents_cache.get_model(document, False)
376+
nodes = await get_nodes_at_position(model, range.start)
377+
378+
if not any(n for n in nodes if isinstance(n, (Keyword, TestCase))):
379+
return
380+
381+
node = nodes[-1] if nodes else None
382+
if node is None or isinstance(node, (Documentation, Fixture, Template)):
383+
return
384+
385+
if not isinstance(node, Statement):
386+
return
387+
388+
text = document.get_lines()[range.start.line][range.start.character : range.end.character]
389+
if not text:
390+
return
391+
392+
spaces = node.tokens[0].value if node.tokens and node.tokens[0].type == "SEPARATOR" else " "
393+
394+
insert_text = f"{spaces}${{{text}}} Set Variable value\n"
395+
node_range = range_from_node(node)
396+
insert_range = Range(start=Position(node_range.start.line, 0), end=Position(node_range.start.line, 0))
397+
we = WorkspaceEdit(
398+
document_changes=[
399+
TextDocumentEdit(
400+
OptionalVersionedTextDocumentIdentifier(str(document.uri), document.version),
401+
[AnnotatedTextEdit("create_local_variable", insert_range, insert_text)],
402+
)
403+
],
404+
change_annotations={"create_local_variable": ChangeAnnotation("Create Local Variable", False)},
405+
)
406+
407+
if (await self.parent.workspace.apply_edit(we)).applied:
408+
insert_range.start.character += insert_text.index("value")
409+
insert_range.end.character = insert_range.start.character + len("value")
410+
411+
await self.parent.window.show_document(str(document.uri), take_focus=False, selection=insert_range)
412+
413+
async def code_action_disable_robotcode_diagnostics_for_line(
414+
self, sender: Any, document: TextDocument, range: Range, context: CodeActionContext
415+
) -> Optional[List[Union[Command, CodeAction]]]:
416+
if range.start == range.end and (
417+
(context.only and CodeActionKind.QUICK_FIX in context.only)
418+
or context.trigger_kind in [CodeActionTriggerKind.INVOKED, CodeActionTriggerKind.AUTOMATIC]
419+
):
420+
diagnostics = next((d for d in context.diagnostics if d.source and d.source.startswith("robotcode.")), None)
421+
422+
if diagnostics is not None:
423+
return [
424+
CodeAction(
425+
f"Disable '{diagnostics.code}' for this line",
426+
kind=CodeActionKind.QUICK_FIX,
427+
command=Command(
428+
self.parent.commands.get_command_name(self.disable_robotcode_diagnostics_for_line_command),
429+
self.parent.commands.get_command_name(self.disable_robotcode_diagnostics_for_line_command),
430+
[document.document_uri, range],
431+
),
432+
diagnostics=[diagnostics],
433+
)
434+
]
435+
436+
return None
437+
438+
@command("robotcode.disableRobotcodeDiagnosticsForLine")
439+
async def disable_robotcode_diagnostics_for_line_command(self, document_uri: DocumentUri, range: Range) -> None:
440+
if range.start.line == range.end.line:
441+
document = await self.parent.documents.get(document_uri)
442+
if document is None:
443+
return
444+
445+
insert_text = " # robotcode: ignore"
446+
447+
line = document.get_lines()[range.start.line]
448+
stripped_line = line.rstrip()
449+
450+
insert_range = Range(
451+
start=Position(range.start.line, len(stripped_line)), end=Position(range.start.line, len(line))
452+
)
453+
we = WorkspaceEdit(
454+
document_changes=[
455+
TextDocumentEdit(
456+
OptionalVersionedTextDocumentIdentifier(str(document.uri), document.version),
457+
[AnnotatedTextEdit("disable_robotcode_diagnostics_for_line", insert_range, insert_text)],
458+
)
459+
],
460+
change_annotations={
461+
"disable_robotcode_diagnostics_for_line": ChangeAnnotation(
462+
"Disable robotcode diagnostics for line", False
463+
)
464+
},
465+
)
209466

210-
lines = insert_text.rstrip().splitlines()
211-
insert_range.start.line += len(lines) - 1
212-
insert_range.start.character = 4
213-
insert_range.end = Position(insert_range.start.line, insert_range.start.character)
214-
insert_range.end.character += len(lines[-1])
215-
await self.parent.window.show_document(str(document.uri), take_focus=True, selection=insert_range)
467+
await self.parent.workspace.apply_edit(we)

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2147,7 +2147,7 @@ def _complete_keyword_arguments_at_position(
21472147
)
21482148

21492149
for i in range(len(positional)):
2150-
if i != argument_index:
2150+
if i != argument_index and i < len(kw_arguments):
21512151
known_names.append(kw_arguments[i].name)
21522152
for n, _ in named:
21532153
known_names.append(n)

0 commit comments

Comments
 (0)