Skip to content

Commit 9499d43

Browse files
committed
feat(langserver): improved quickfix create keyword can now add keywords to resource files if valid namespace is given
1 parent 28ff3d5 commit 9499d43

File tree

2 files changed

+123
-62
lines changed

2 files changed

+123
-62
lines changed

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

Lines changed: 121 additions & 60 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
ChangeAnnotation,
1111
CodeAction,
1212
CodeActionContext,
13+
CodeActionDisabledType,
1314
CodeActionKind,
1415
CodeActionTriggerKind,
1516
Command,
@@ -126,34 +127,68 @@ async def collect(
126127
async def code_action_create_keyword(
127128
self, document: TextDocument, range: Range, context: CodeActionContext
128129
) -> Optional[List[Union[Command, CodeAction]]]:
130+
from robot.parsing.model.statements import (
131+
Fixture,
132+
KeywordCall,
133+
Template,
134+
TestTemplate,
135+
)
136+
129137
result: List[Union[Command, CodeAction]] = []
130138

131139
if (context.only and CodeActionKind.QUICK_FIX in context.only) or context.trigger_kind in [
132140
CodeActionTriggerKind.INVOKED,
133141
CodeActionTriggerKind.AUTOMATIC,
134142
]:
143+
model = await self.parent.documents_cache.get_model(document, False)
144+
namespace = await self.parent.documents_cache.get_namespace(document)
145+
135146
for diagnostic in (
136147
d
137148
for d in context.diagnostics
138149
if d.source == DIAGNOSTICS_SOURCE_NAME and d.code == Error.KEYWORD_NOT_FOUND
139150
):
140-
text = document.get_lines()[diagnostic.range.start.line][
141-
diagnostic.range.start.character : diagnostic.range.end.character
142-
]
143-
if not text:
144-
continue
145-
result.append(
146-
CodeAction(
147-
f"Create Keyword `{text}`",
148-
kind=CodeActionKind.QUICK_FIX,
149-
command=Command(
150-
self.parent.commands.get_command_name(self.create_keyword_command),
151-
self.parent.commands.get_command_name(self.create_keyword_command),
152-
[document.document_uri, diagnostic.range],
153-
),
154-
diagnostics=[diagnostic],
151+
disabled = None
152+
node = await get_node_at_position(model, diagnostic.range.start)
153+
154+
if isinstance(node, (KeywordCall, Fixture, TestTemplate, Template)):
155+
tokens = get_tokens_at_position(node, diagnostic.range.start)
156+
if not tokens:
157+
continue
158+
159+
keyword_token = tokens[-1]
160+
161+
bdd_token, token = self.split_bdd_prefix(namespace, keyword_token)
162+
if bdd_token is not None and token is not None:
163+
keyword_token = token
164+
165+
lib_entry, kw_namespace = await self.get_namespace_info_from_keyword(namespace, keyword_token)
166+
167+
if lib_entry is not None and lib_entry.library_doc.type == "LIBRARY":
168+
disabled = CodeActionDisabledType("Keyword is from a library")
169+
170+
text = keyword_token.value
171+
172+
if lib_entry and kw_namespace:
173+
text = text[len(kw_namespace) + 1 :].strip()
174+
175+
if not text:
176+
continue
177+
178+
result.append(
179+
CodeAction(
180+
f"Create Keyword `{text}`",
181+
kind=CodeActionKind.QUICK_FIX,
182+
command=Command(
183+
self.parent.commands.get_command_name(self.create_keyword_command),
184+
self.parent.commands.get_command_name(self.create_keyword_command),
185+
[document.document_uri, diagnostic.range],
186+
),
187+
diagnostics=[diagnostic],
188+
disabled=disabled,
189+
is_preferred=True,
190+
)
155191
)
156-
)
157192

158193
return result if result else None
159194

@@ -187,9 +222,20 @@ async def create_keyword_command(self, document_uri: DocumentUri, range: Range)
187222

188223
bdd_token, token = self.split_bdd_prefix(namespace, keyword_token)
189224
if bdd_token is not None and token is not None:
190-
keyword = token.value
191-
else:
192-
keyword = keyword_token.value
225+
keyword_token = token
226+
227+
lib_entry, kw_namespace = await self.get_namespace_info_from_keyword(namespace, keyword_token)
228+
229+
if lib_entry is not None and lib_entry.library_doc.type == "LIBRARY":
230+
return
231+
232+
text = keyword_token.value
233+
234+
if lib_entry and kw_namespace:
235+
text = text[len(kw_namespace) + 1 :].strip()
236+
237+
if not text:
238+
return
193239

194240
arguments = []
195241

@@ -202,54 +248,65 @@ async def create_keyword_command(self, document_uri: DocumentUri, range: Range)
202248
arguments.append(f"${{arg{len(arguments)+1}}}")
203249

204250
insert_text = (
205-
KEYWORD_WITH_ARGS_TEMPLATE.substitute(name=keyword, args=" ".join(arguments))
251+
KEYWORD_WITH_ARGS_TEMPLATE.substitute(name=text, args=" ".join(arguments))
206252
if arguments
207-
else KEYWORD_TEMPLATE.substitute(name=keyword)
253+
else KEYWORD_TEMPLATE.substitute(name=text)
208254
)
209255

210-
keyword_sections = find_keyword_sections(model)
211-
keyword_section = keyword_sections[-1] if keyword_sections else None
256+
if lib_entry is not None and lib_entry.library_doc.type == "RESOURCE" and lib_entry.library_doc.source:
257+
dest_document = await self.parent.documents.get_or_open_document(lib_entry.library_doc.source)
258+
else:
259+
dest_document = document
212260

213-
if keyword_section is not None:
214-
node_range = range_from_node(keyword_section)
261+
await self._apply_create_keyword(dest_document, insert_text)
215262

216-
insert_pos = Position(node_range.end.line + 1, 0)
217-
insert_range = Range(insert_pos, insert_pos)
263+
async def _apply_create_keyword(self, document: TextDocument, insert_text: str) -> None:
264+
model = await self.parent.documents_cache.get_model(document, False)
265+
namespace = await self.parent.documents_cache.get_namespace(document)
218266

219-
insert_text = f"\n{insert_text}"
220-
else:
221-
if namespace.languages is None or not namespace.languages.languages:
222-
keywords_text = "Keywords"
223-
else:
224-
keywords_text = namespace.languages.languages[-1].keywords_header
267+
keyword_sections = find_keyword_sections(model)
268+
keyword_section = keyword_sections[-1] if keyword_sections else None
225269

226-
insert_text = f"\n\n*** {keywords_text} ***\n{insert_text}"
270+
if keyword_section is not None:
271+
node_range = range_from_node(keyword_section)
227272

228-
lines = document.get_lines()
229-
end_line = len(lines) - 1
230-
while end_line >= 0 and not lines[end_line].strip():
231-
end_line -= 1
232-
doc_pos = Position(end_line + 1, 0)
273+
insert_pos = Position(node_range.end.line + 1, 0)
274+
insert_range = Range(insert_pos, insert_pos)
233275

234-
insert_range = Range(doc_pos, doc_pos)
276+
insert_text = f"\n{insert_text}"
277+
else:
278+
if namespace.languages is None or not namespace.languages.languages:
279+
keywords_text = "Keywords"
280+
else:
281+
keywords_text = namespace.languages.languages[-1].keywords_header
235282

236-
we = WorkspaceEdit(
237-
document_changes=[
238-
TextDocumentEdit(
239-
OptionalVersionedTextDocumentIdentifier(str(document.uri), document.version),
240-
[AnnotatedTextEdit("create_keyword", insert_range, insert_text)],
241-
)
242-
],
243-
change_annotations={"create_keyword": ChangeAnnotation("Create Keyword", False)},
244-
)
283+
insert_text = f"\n\n*** {keywords_text} ***\n{insert_text}"
245284

246-
if (await self.parent.workspace.apply_edit(we)).applied:
247-
lines = insert_text.rstrip().splitlines()
248-
insert_range.start.line += len(lines) - 1
249-
insert_range.start.character = 4
250-
insert_range.end = Position(insert_range.start.line, insert_range.start.character)
251-
insert_range.end.character += len(lines[-1])
252-
await self.parent.window.show_document(str(document.uri), take_focus=True, selection=insert_range)
285+
lines = document.get_lines()
286+
end_line = len(lines) - 1
287+
while end_line >= 0 and not lines[end_line].strip():
288+
end_line -= 1
289+
doc_pos = Position(end_line + 1, 0)
290+
291+
insert_range = Range(doc_pos, doc_pos)
292+
293+
we = WorkspaceEdit(
294+
document_changes=[
295+
TextDocumentEdit(
296+
OptionalVersionedTextDocumentIdentifier(str(document.uri), document.version),
297+
[AnnotatedTextEdit("create_keyword", insert_range, insert_text)],
298+
)
299+
],
300+
change_annotations={"create_keyword": ChangeAnnotation("Create Keyword", False)},
301+
)
302+
303+
if (await self.parent.workspace.apply_edit(we)).applied:
304+
lines = insert_text.rstrip().splitlines()
305+
insert_range.start.line += len(lines) - 1
306+
insert_range.start.character = 4
307+
insert_range.end = Position(insert_range.start.line, insert_range.start.character)
308+
insert_range.end.character += len(lines[-1])
309+
await self.parent.window.show_document(str(document.uri), take_focus=True, selection=insert_range)
253310

254311
async def code_action_assign_result_to_variable(
255312
self, document: TextDocument, range: Range, context: CodeActionContext
@@ -262,10 +319,14 @@ async def code_action_assign_result_to_variable(
262319
TestTemplate,
263320
)
264321

265-
if (context.only and QUICK_FIX_OTHER in context.only) or context.trigger_kind in [
266-
CodeActionTriggerKind.INVOKED,
267-
CodeActionTriggerKind.AUTOMATIC,
268-
]:
322+
if range.start.line == range.end.line and (
323+
(context.only and QUICK_FIX_OTHER in context.only)
324+
or context.trigger_kind
325+
in [
326+
CodeActionTriggerKind.INVOKED,
327+
CodeActionTriggerKind.AUTOMATIC,
328+
]
329+
):
269330
model = await self.parent.documents_cache.get_model(document, False)
270331
node = await get_node_at_position(model, range.start)
271332

packages/language_server/src/robotcode/language_server/robotframework/utils/ast_utils.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -273,15 +273,15 @@ def get_tokens_at_position(node: HasTokens, position: Position, include_end: boo
273273
return [
274274
t
275275
for t in node.tokens
276-
if position.is_in_range(range := range_from_token(t), include_end) or range.end == position
276+
if position.is_in_range(range := range_from_token(t), include_end) or include_end and range.end == position
277277
]
278278

279279

280280
def iter_nodes_at_position(node: ast.AST, position: Position, include_end: bool = False) -> AsyncIterator[ast.AST]:
281281
return (
282282
n
283283
async for n in async_ast.iter_nodes(node)
284-
if position.is_in_range(range := range_from_node(n), include_end) or range.end == position
284+
if position.is_in_range(range := range_from_node(n), include_end) or include_end and range.end == position
285285
)
286286

287287

0 commit comments

Comments
 (0)