robotcode-language-server 2.5.0__py3-none-any.whl → 2.6.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1 +1 @@
1
- __version__ = "2.5.0"
1
+ __version__ = "2.6.0"
@@ -436,7 +436,7 @@ class DiagnosticsProtocolPart(LanguageServerProtocolPart):
436
436
  documents_to_collect = [
437
437
  doc
438
438
  for doc in documents
439
- if doc.opened_in_editor or self.get_diagnostics_mode(document.uri) == DiagnosticsMode.WORKSPACE
439
+ if doc.opened_in_editor or self.get_diagnostics_mode(doc.uri) == DiagnosticsMode.WORKSPACE
440
440
  ]
441
441
 
442
442
  with self._logger.measure_time(
@@ -10,6 +10,7 @@ from robotcode.core.lsp.types import (
10
10
  CodeActionContext,
11
11
  CodeActionKind,
12
12
  Command,
13
+ Position,
13
14
  Range,
14
15
  )
15
16
  from robotcode.core.text_document import TextDocument
@@ -18,9 +19,16 @@ from robotcode.core.utils.dataclasses import CamelSnakeMixin
18
19
  from robotcode.core.utils.logging import LoggingDescriptor
19
20
  from robotcode.jsonrpc2.protocol import rpc_method
20
21
  from robotcode.robot.diagnostics.entities import LibraryEntry
21
- from robotcode.robot.diagnostics.library_doc import resolve_robot_variables
22
+ from robotcode.robot.diagnostics.library_doc import KeywordDoc, resolve_robot_variables
22
23
  from robotcode.robot.diagnostics.model_helper import ModelHelper
23
24
  from robotcode.robot.diagnostics.namespace import Namespace
25
+ from robotcode.robot.diagnostics.semantic_analyzer.enums import ImportType, NodeKind, TokenKind
26
+ from robotcode.robot.diagnostics.semantic_analyzer.model import SemanticModel
27
+ from robotcode.robot.diagnostics.semantic_analyzer.nodes import (
28
+ DefinitionStatement,
29
+ ImportStatement,
30
+ KeywordCallStatement,
31
+ )
24
32
  from robotcode.robot.utils.ast import get_node_at_position, range_from_token
25
33
 
26
34
  from ...common.decorators import code_action_kinds
@@ -53,6 +61,26 @@ class RobotCodeActionDocumentationProtocolPart(RobotLanguageServerProtocolPart,
53
61
  document: TextDocument,
54
62
  range: Range,
55
63
  context: CodeActionContext,
64
+ ) -> Optional[List[Union[Command, CodeAction]]]:
65
+ namespace = self.parent.documents_cache.get_namespace(document)
66
+
67
+ # Tier 2 model-based path — used when the experimental SemanticAnalyzer
68
+ # is enabled. Reads everything off the SemanticModel: statement kind
69
+ # via `model.statement_at()`, the pre-resolved `keyword_doc`, the
70
+ # `import_name`, and SemanticTokens for cursor-position checks. No
71
+ # `find_keyword`, no AST walk.
72
+ semantic_model = namespace.semantic_model
73
+ if semantic_model is not None:
74
+ return self._collect_from_model(document, range, context, namespace, semantic_model)
75
+
76
+ return self._collect_legacy(document, range, context, namespace)
77
+
78
+ def _collect_legacy(
79
+ self,
80
+ document: TextDocument,
81
+ range: Range,
82
+ context: CodeActionContext,
83
+ namespace: Namespace,
56
84
  ) -> Optional[List[Union[Command, CodeAction]]]:
57
85
  from robot.parsing.lexer import Token as RobotToken
58
86
  from robot.parsing.model.statements import (
@@ -65,8 +93,6 @@ class RobotCodeActionDocumentationProtocolPart(RobotLanguageServerProtocolPart,
65
93
  TestTemplate,
66
94
  )
67
95
 
68
- namespace = self.parent.documents_cache.get_namespace(document)
69
-
70
96
  model = self.parent.documents_cache.get_model(document)
71
97
  node = get_node_at_position(model, range.start)
72
98
 
@@ -111,40 +137,7 @@ class RobotCodeActionDocumentationProtocolPart(RobotLanguageServerProtocolPart,
111
137
 
112
138
  if kw_doc is not None:
113
139
  if context.only and CodeActionKind.SOURCE.value in context.only:
114
- entry: Optional[LibraryEntry] = None
115
-
116
- if kw_doc.libtype == "LIBRARY":
117
- entry = next(
118
- (v for v in namespace.libraries.values() if v.library_doc == kw_doc.parent),
119
- None,
120
- )
121
-
122
- elif kw_doc.libtype == "RESOURCE":
123
- entry = next(
124
- (v for v in namespace.resources.values() if v.library_doc == kw_doc.parent),
125
- None,
126
- )
127
-
128
- self_libdoc = namespace.library_doc
129
- if entry is None and self_libdoc == kw_doc.parent:
130
- entry = LibraryEntry(
131
- self_libdoc.name,
132
- str(document.uri.to_path().name),
133
- self_libdoc,
134
- )
135
-
136
- if entry is None:
137
- return None
138
-
139
- url = self.build_url(
140
- entry.import_name,
141
- entry.args,
142
- document,
143
- namespace,
144
- kw_doc.name,
145
- )
146
-
147
- return [self.open_documentation_code_action(url)]
140
+ return self._build_keyword_action(kw_doc, document, namespace)
148
141
 
149
142
  if isinstance(node, KeywordName):
150
143
  name_token = node.get_token(RobotToken.KEYWORD_NAME)
@@ -161,6 +154,159 @@ class RobotCodeActionDocumentationProtocolPart(RobotLanguageServerProtocolPart,
161
154
 
162
155
  return None
163
156
 
157
+ # ------------------------------------------------------------------
158
+ # Tier 2 model-based collection
159
+ # ------------------------------------------------------------------
160
+
161
+ def _collect_from_model(
162
+ self,
163
+ document: TextDocument,
164
+ range: Range,
165
+ context: CodeActionContext,
166
+ namespace: Namespace,
167
+ model: SemanticModel,
168
+ ) -> Optional[List[Union[Command, CodeAction]]]:
169
+ """Mirror legacy three-branch logic (import / keyword-call / keyword-def)
170
+ purely off the SemanticModel — no AST walks, no `find_keyword`.
171
+
172
+ Position checks use SemanticTokens; URL inputs read from pre-resolved
173
+ statement fields (`import_name`, `keyword_doc`, `name`).
174
+ """
175
+ # SemanticModel uses 1-indexed lines; LSP positions are 0-indexed.
176
+ stmt = model.statement_at(range.start.line + 1)
177
+ if stmt is None:
178
+ return None
179
+
180
+ # Branch 1: Library / Resource import — gated on context.only at entry.
181
+ if (
182
+ context.only
183
+ and isinstance(stmt, ImportStatement)
184
+ and stmt.import_type in (ImportType.LIBRARY, ImportType.RESOURCE)
185
+ and CodeActionKind.SOURCE.value in context.only
186
+ ):
187
+ return self._import_action_from_model(stmt, document, range, namespace)
188
+
189
+ # Branch 2: keyword call / fixture / template.
190
+ if isinstance(stmt, KeywordCallStatement):
191
+ if range.start != range.end:
192
+ return None
193
+ kw_doc = stmt.keyword_doc
194
+ if kw_doc is None:
195
+ return None
196
+ if not self._cursor_on_keyword_reference(range.start, stmt):
197
+ return None
198
+ if not (context.only and CodeActionKind.SOURCE.value in context.only):
199
+ return None
200
+ return self._build_keyword_action(kw_doc, document, namespace)
201
+
202
+ # Branch 3: keyword definition header — no context.only check
203
+ # (legacy doesn't gate this branch either).
204
+ if isinstance(stmt, DefinitionStatement) and stmt.kind is NodeKind.KEYWORD_DEF:
205
+ name_tok = next((t for t in stmt.tokens if t.kind is TokenKind.KEYWORD_NAME), None)
206
+ if name_tok is None or range not in name_tok.range:
207
+ return None
208
+ url = self.build_url(
209
+ str(document.uri.to_path().name),
210
+ (),
211
+ document,
212
+ namespace,
213
+ name_tok.value,
214
+ )
215
+ return [self.open_documentation_code_action(url)]
216
+
217
+ return None
218
+
219
+ def _import_action_from_model(
220
+ self,
221
+ stmt: ImportStatement,
222
+ document: TextDocument,
223
+ range: Range,
224
+ namespace: Namespace,
225
+ ) -> Optional[List[Union[Command, CodeAction]]]:
226
+ """Library / Resource import branch built off SemanticTokens.
227
+
228
+ - The import path lives in the IMPORT_NAME token (cursor-position check).
229
+ - Library `args` are the ARGUMENT tokens BEFORE the optional WITH NAME
230
+ marker (CONTROL_FLOW); anything after is the alias and must be
231
+ excluded — matches RF's `LibraryImport.args` semantics.
232
+ - Resource imports never carry args (RF API returns ()).
233
+ """
234
+ name_tok = next((t for t in stmt.tokens if t.kind is TokenKind.IMPORT_NAME), None)
235
+ if name_tok is None or range not in name_tok.range:
236
+ return None
237
+
238
+ if stmt.import_type is ImportType.LIBRARY:
239
+ arg_values: List[str] = []
240
+ for tok in stmt.tokens:
241
+ if tok.kind is TokenKind.CONTROL_FLOW:
242
+ break # WITH NAME — everything after is the alias
243
+ if tok.kind is TokenKind.ARGUMENT:
244
+ arg_values.append(tok.value)
245
+ args: Tuple[str, ...] = tuple(arg_values)
246
+ else:
247
+ args = ()
248
+
249
+ url = self.build_url(stmt.import_name or "", args, document, namespace)
250
+ return [self.open_documentation_code_action(url)]
251
+
252
+ @staticmethod
253
+ def _cursor_on_keyword_reference(pos: Position, stmt: KeywordCallStatement) -> bool:
254
+ """Cursor is within the NAMESPACE / SEPARATOR / KEYWORD SemanticTokens
255
+ that make up the keyword reference (BDD prefix excluded). Mirrors
256
+ the legacy `position.is_in_range(range_from_token(keyword_token))`
257
+ after the BDD-prefix strip that
258
+ `get_keyworddoc_and_token_from_position` does.
259
+ """
260
+ return any(
261
+ pos in t.range
262
+ for t in stmt.tokens
263
+ if t.kind in (TokenKind.NAMESPACE, TokenKind.SEPARATOR, TokenKind.KEYWORD)
264
+ )
265
+
266
+ def _build_keyword_action(
267
+ self,
268
+ kw_doc: KeywordDoc,
269
+ document: TextDocument,
270
+ namespace: Namespace,
271
+ ) -> Optional[List[Union[Command, CodeAction]]]:
272
+ """Resolve the LibraryEntry that owns `kw_doc` and build the
273
+ Open-Documentation action. Shared between legacy and model paths so
274
+ the URL construction stays identical."""
275
+ entry: Optional[LibraryEntry] = None
276
+
277
+ if kw_doc.libtype == "LIBRARY":
278
+ entry = next(
279
+ (v for v in namespace.libraries.values() if v.library_doc == kw_doc.parent),
280
+ None,
281
+ )
282
+
283
+ elif kw_doc.libtype == "RESOURCE":
284
+ entry = next(
285
+ (v for v in namespace.resources.values() if v.library_doc == kw_doc.parent),
286
+ None,
287
+ )
288
+
289
+ self_libdoc = namespace.library_doc
290
+ if entry is None and self_libdoc == kw_doc.parent:
291
+ entry = LibraryEntry(
292
+ self_libdoc.name,
293
+ str(document.uri.to_path().name),
294
+ self_libdoc,
295
+ )
296
+
297
+ if entry is None:
298
+ return None
299
+
300
+ url = self.build_url(
301
+ entry.import_name,
302
+ entry.args,
303
+ document,
304
+ namespace,
305
+ kw_doc.name,
306
+ )
307
+
308
+ return [self.open_documentation_code_action(url)]
309
+
164
310
  def open_documentation_code_action(self, url: str) -> CodeAction:
165
311
  return CodeAction(
166
312
  "Open Documentation",
@@ -4,6 +4,7 @@ from string import Template as StringTemplate
4
4
  from typing import (
5
5
  TYPE_CHECKING,
6
6
  Any,
7
+ Iterable,
7
8
  List,
8
9
  Mapping,
9
10
  Optional,
@@ -49,8 +50,13 @@ from robotcode.core.text_document import TextDocument
49
50
  from robotcode.core.utils.dataclasses import as_dict, from_dict
50
51
  from robotcode.core.utils.inspect import iter_methods
51
52
  from robotcode.core.utils.logging import LoggingDescriptor
53
+ from robotcode.robot.diagnostics.entities import LibraryEntry
52
54
  from robotcode.robot.diagnostics.errors import DIAGNOSTICS_SOURCE_NAME, Error
53
55
  from robotcode.robot.diagnostics.model_helper import ModelHelper
56
+ from robotcode.robot.diagnostics.namespace import Namespace
57
+ from robotcode.robot.diagnostics.semantic_analyzer.enums import TokenKind
58
+ from robotcode.robot.diagnostics.semantic_analyzer.model import SemanticModel
59
+ from robotcode.robot.diagnostics.semantic_analyzer.nodes import KeywordCallStatement
54
60
  from robotcode.robot.utils.ast import (
55
61
  FirstAndLastRealStatementFinder,
56
62
  get_node_at_position,
@@ -97,6 +103,26 @@ ${name}
97
103
  )
98
104
 
99
105
 
106
+ def _format_create_keyword_args(arg_values: Iterable[str]) -> List[str]:
107
+ """Build the placeholder list for the new keyword's `[Arguments]` line.
108
+
109
+ For each argument value:
110
+ - `name=value` with a literal `name` (no variables in it) → `${name}`
111
+ - everything else → `${argN}` (N counted across all arguments)
112
+
113
+ Pure function so both the AST and SemanticModel paths produce
114
+ identical output for the same set of argument values.
115
+ """
116
+ out: List[str] = []
117
+ for value in arg_values:
118
+ name, val = split_from_equals(value)
119
+ if val is not None and not contains_variable(name, "$@&%"):
120
+ out.append(f"${{{name}}}")
121
+ else:
122
+ out.append(f"${{arg{len(out) + 1}}}")
123
+ return out
124
+
125
+
100
126
  class RobotCodeActionQuickFixesProtocolPart(RobotLanguageServerProtocolPart, ModelHelper, CodeActionHelperMixin):
101
127
  _logger = LoggingDescriptor()
102
128
 
@@ -155,7 +181,6 @@ class RobotCodeActionQuickFixesProtocolPart(RobotLanguageServerProtocolPart, Mod
155
181
  CodeActionTriggerKind.INVOKED,
156
182
  CodeActionTriggerKind.AUTOMATIC,
157
183
  ]:
158
- model = self.parent.documents_cache.get_model(document)
159
184
  namespace = self.parent.documents_cache.get_namespace(document)
160
185
 
161
186
  for diagnostic in (
@@ -163,123 +188,176 @@ class RobotCodeActionQuickFixesProtocolPart(RobotLanguageServerProtocolPart, Mod
163
188
  for d in context.diagnostics
164
189
  if d.source == DIAGNOSTICS_SOURCE_NAME and d.code == Error.KEYWORD_NOT_FOUND
165
190
  ):
191
+ resolved = self._resolve_create_keyword_target(document, diagnostic.range.start, namespace)
192
+ if resolved is None:
193
+ continue
194
+ text, lib_entry = resolved
195
+
166
196
  disabled = None
167
- node = get_node_at_position(model, diagnostic.range.start)
197
+ if lib_entry is not None and lib_entry.library_doc.type == "LIBRARY":
198
+ disabled = CodeActionDisabledType("Keyword is from a library")
168
199
 
169
- if isinstance(node, (KeywordCall, Fixture, TestTemplate, Template)):
170
- tokens = get_tokens_at_position(node, diagnostic.range.start)
171
- if not tokens:
172
- continue
200
+ result.append(
201
+ CodeAction(
202
+ f"Create Keyword `{text}`",
203
+ kind=CodeActionKind.QUICK_FIX,
204
+ data=as_dict(
205
+ CodeActionData(
206
+ "quickfix",
207
+ "create_keyword",
208
+ document.document_uri,
209
+ diagnostic.range,
210
+ )
211
+ ),
212
+ diagnostics=[diagnostic],
213
+ disabled=disabled,
214
+ is_preferred=True,
215
+ )
216
+ )
173
217
 
174
- keyword_token = tokens[-1]
218
+ return result if result else None
175
219
 
176
- bdd_token, token = self.split_bdd_prefix(namespace, keyword_token)
177
- if bdd_token is not None and token is not None:
178
- keyword_token = token
220
+ # ------------------------------------------------------------------
221
+ # Tier 2 model-based resolution for the "Create Keyword" quick fix
222
+ # ------------------------------------------------------------------
179
223
 
180
- (
181
- lib_entry,
182
- kw_namespace,
183
- ) = self.get_namespace_info_from_keyword_token(namespace, keyword_token)
224
+ def _resolve_create_keyword_target(
225
+ self,
226
+ document: TextDocument,
227
+ position: Position,
228
+ namespace: Namespace,
229
+ ) -> Optional[Tuple[str, Optional[LibraryEntry]]]:
230
+ """Resolve the bare keyword name + owning LibraryEntry for the
231
+ "Create Keyword" quick fix at `position`.
232
+
233
+ Returns `(text, lib_entry)` or `None` if the position isn't a
234
+ KeywordCall / Fixture / Template AST node, or the keyword name is
235
+ empty after stripping the BDD / namespace prefix.
236
+
237
+ When the experimental SemanticAnalyzer is enabled the data comes
238
+ directly from the SemanticStatement; otherwise we fall back to
239
+ the legacy AST + `ModelHelper` path.
240
+ """
241
+ semantic_model = namespace.semantic_model
242
+ if semantic_model is not None:
243
+ return self._resolve_create_keyword_target_from_model(position, semantic_model)
244
+ return self._resolve_create_keyword_target_legacy(document, position, namespace)
245
+
246
+ def _resolve_create_keyword_target_from_model(
247
+ self,
248
+ position: Position,
249
+ model: SemanticModel,
250
+ ) -> Optional[Tuple[str, Optional[LibraryEntry]]]:
251
+ """SemanticModel branch: KEYWORD SemanticToken already has the bare
252
+ name (BDD prefix and namespace are split into their own tokens),
253
+ and `KeywordCallStatement.lib_entry` is the pre-resolved owner."""
254
+ stmt = model.statement_at(position.line + 1)
255
+ if not isinstance(stmt, KeywordCallStatement):
256
+ return None
257
+ kw_tok = next((t for t in stmt.tokens if t.kind is TokenKind.KEYWORD), None)
258
+ if kw_tok is None or not kw_tok.value:
259
+ return None
260
+ return kw_tok.value, stmt.lib_entry
184
261
 
185
- if lib_entry is not None and lib_entry.library_doc.type == "LIBRARY":
186
- disabled = CodeActionDisabledType("Keyword is from a library")
262
+ def _resolve_create_keyword_target_legacy(
263
+ self,
264
+ document: TextDocument,
265
+ position: Position,
266
+ namespace: Namespace,
267
+ ) -> Optional[Tuple[str, Optional[LibraryEntry]]]:
268
+ """AST + `ModelHelper` fallback (used when the experimental
269
+ analyzer is off). Mirrors the original inline logic exactly so
270
+ the model-path migration stays output-equivalent."""
271
+ model = self.parent.documents_cache.get_model(document)
272
+ node = get_node_at_position(model, position)
273
+ if not isinstance(node, (KeywordCall, Fixture, TestTemplate, Template)):
274
+ return None
187
275
 
188
- text = keyword_token.value
276
+ tokens = get_tokens_at_position(node, position)
277
+ if not tokens:
278
+ return None
279
+ keyword_token = tokens[-1]
189
280
 
190
- if lib_entry and kw_namespace:
191
- text = text[len(kw_namespace) + 1 :].strip()
281
+ bdd_token, token = self.split_bdd_prefix(namespace, keyword_token)
282
+ if bdd_token is not None and token is not None:
283
+ keyword_token = token
192
284
 
193
- if not text:
194
- continue
285
+ lib_entry, kw_namespace = self.get_namespace_info_from_keyword_token(namespace, keyword_token)
195
286
 
196
- result.append(
197
- CodeAction(
198
- f"Create Keyword `{text}`",
199
- kind=CodeActionKind.QUICK_FIX,
200
- data=as_dict(
201
- CodeActionData(
202
- "quickfix",
203
- "create_keyword",
204
- document.document_uri,
205
- diagnostic.range,
206
- )
207
- ),
208
- diagnostics=[diagnostic],
209
- disabled=disabled,
210
- is_preferred=True,
211
- )
212
- )
287
+ text = keyword_token.value
288
+ if lib_entry and kw_namespace:
289
+ text = text[len(kw_namespace) + 1 :].strip()
290
+ if not text:
291
+ return None
213
292
 
214
- return result if result else None
293
+ return text, lib_entry
215
294
 
216
295
  def resolve_code_action_create_keyword(self, code_action: CodeAction, data: CodeActionData) -> Optional[CodeAction]:
217
296
  document = self.parent.documents.get(data.document_uri)
218
297
  if document is None:
219
298
  return None
220
299
 
221
- model = self.parent.documents_cache.get_model(document)
222
- node = get_node_at_position(model, data.range.start)
223
-
224
- if isinstance(node, (KeywordCall, Fixture, TestTemplate, Template)):
225
- tokens = get_tokens_at_position(node, data.range.start)
226
- if not tokens:
227
- return None
228
-
229
- keyword_token = tokens[-1]
230
-
231
- namespace = self.parent.documents_cache.get_namespace(document)
232
-
233
- bdd_token, token = self.split_bdd_prefix(namespace, keyword_token)
234
- if bdd_token is not None and token is not None:
235
- keyword_token = token
236
-
237
- (
238
- lib_entry,
239
- kw_namespace,
240
- ) = self.get_namespace_info_from_keyword_token(namespace, keyword_token)
241
-
242
- if lib_entry is not None and lib_entry.library_doc.type == "LIBRARY":
243
- return None
244
-
245
- text = keyword_token.value
246
-
247
- if lib_entry and kw_namespace:
248
- text = text[len(kw_namespace) + 1 :].strip()
300
+ namespace = self.parent.documents_cache.get_namespace(document)
249
301
 
250
- if not text:
251
- return None
302
+ resolved = self._resolve_create_keyword_target(document, data.range.start, namespace)
303
+ if resolved is None:
304
+ return None
305
+ text, lib_entry = resolved
252
306
 
253
- arguments = []
307
+ # Library keywords can't be auto-created; legacy returns None here.
308
+ if lib_entry is not None and lib_entry.library_doc.type == "LIBRARY":
309
+ return None
254
310
 
255
- for t in node.get_tokens(Token.ARGUMENT):
256
- name, value = split_from_equals(cast(Token, t).value)
257
- if value is not None and not contains_variable(name, "$@&%"):
258
- arguments.append(f"${{{name}}}")
259
- else:
260
- arguments.append(f"${{arg{len(arguments) + 1}}}")
311
+ arguments = self._collect_create_keyword_arguments(document, data.range.start, namespace)
261
312
 
262
- insert_text = (
263
- KEYWORD_WITH_ARGS_TEMPLATE.substitute(name=text, args=" ".join(arguments))
264
- if arguments
265
- else KEYWORD_TEMPLATE.substitute(name=text)
266
- )
313
+ insert_text = (
314
+ KEYWORD_WITH_ARGS_TEMPLATE.substitute(name=text, args=" ".join(arguments))
315
+ if arguments
316
+ else KEYWORD_TEMPLATE.substitute(name=text)
317
+ )
267
318
 
268
- if lib_entry is not None and lib_entry.library_doc.type == "RESOURCE" and lib_entry.library_doc.source:
269
- dest_document = self.parent.documents.get_or_open_document(lib_entry.library_doc.source)
270
- else:
271
- dest_document = document
319
+ if lib_entry is not None and lib_entry.library_doc.type == "RESOURCE" and lib_entry.library_doc.source:
320
+ dest_document = self.parent.documents.get_or_open_document(lib_entry.library_doc.source)
321
+ else:
322
+ dest_document = document
272
323
 
273
- code_action.edit, select_range = self._apply_create_keyword(dest_document, insert_text)
324
+ code_action.edit, select_range = self._apply_create_keyword(dest_document, insert_text)
274
325
 
275
- code_action.command = Command(
276
- SHOW_DOCUMENT_SELECT_AND_RENAME_COMMAND,
277
- SHOW_DOCUMENT_SELECT_AND_RENAME_COMMAND,
278
- [dest_document.document_uri, select_range, False],
279
- )
280
- return code_action
326
+ code_action.command = Command(
327
+ SHOW_DOCUMENT_SELECT_AND_RENAME_COMMAND,
328
+ SHOW_DOCUMENT_SELECT_AND_RENAME_COMMAND,
329
+ [dest_document.document_uri, select_range, False],
330
+ )
331
+ return code_action
281
332
 
282
- return None
333
+ def _collect_create_keyword_arguments(
334
+ self,
335
+ document: TextDocument,
336
+ position: Position,
337
+ namespace: Namespace,
338
+ ) -> List[str]:
339
+ """Build the `${name}` / `${argN}` placeholder list for the new
340
+ keyword's `[Arguments]` line, derived from the offending call's
341
+ ARGUMENT tokens.
342
+
343
+ SemanticModel-based when available (iterates `stmt.tokens`),
344
+ falls back to the AST `node.get_tokens(ARGUMENT)` walk otherwise.
345
+ Both paths apply the same `name=value` heuristic via
346
+ `split_from_equals`.
347
+ """
348
+ semantic_model = namespace.semantic_model
349
+ if semantic_model is not None:
350
+ stmt = semantic_model.statement_at(position.line + 1)
351
+ if isinstance(stmt, KeywordCallStatement):
352
+ arg_tokens = [t for t in stmt.tokens if t.kind is TokenKind.ARGUMENT]
353
+ return _format_create_keyword_args(t.value for t in arg_tokens)
354
+ return []
355
+
356
+ ast_model = self.parent.documents_cache.get_model(document)
357
+ node = get_node_at_position(ast_model, position)
358
+ if not isinstance(node, (KeywordCall, Fixture, TestTemplate, Template)):
359
+ return []
360
+ return _format_create_keyword_args(cast(Token, t).value for t in node.get_tokens(Token.ARGUMENT))
283
361
 
284
362
  def _apply_create_keyword(self, document: TextDocument, insert_text: str) -> Tuple[WorkspaceEdit, Range]:
285
363
  model = self.parent.documents_cache.get_model(document)