robotcode-language-server 2.6.2__py3-none-any.whl → 2.8.0.dev548__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.
Files changed (31) hide show
  1. robotcode/language_server/__version__.py +1 -1
  2. robotcode/language_server/common/parts/diagnostics.py +50 -17
  3. robotcode/language_server/common/parts/documents.py +34 -31
  4. robotcode/language_server/common/parts/workspace.py +57 -55
  5. robotcode/language_server/common/protocol.py +3 -1
  6. robotcode/language_server/robotframework/configuration.py +3 -0
  7. robotcode/language_server/robotframework/parts/code_action_documentation.py +440 -129
  8. robotcode/language_server/robotframework/parts/code_action_helper_mixin.py +17 -2
  9. robotcode/language_server/robotframework/parts/code_action_quick_fixes.py +6 -25
  10. robotcode/language_server/robotframework/parts/code_action_refactor.py +8 -6
  11. robotcode/language_server/robotframework/parts/code_lens.py +3 -4
  12. robotcode/language_server/robotframework/parts/completion.py +193 -21
  13. robotcode/language_server/robotframework/parts/debugging_utils.py +75 -6
  14. robotcode/language_server/robotframework/parts/diagnostics.py +15 -3
  15. robotcode/language_server/robotframework/parts/documents_cache.py +5 -1
  16. robotcode/language_server/robotframework/parts/hover.py +103 -21
  17. robotcode/language_server/robotframework/parts/http_server.py +17 -7
  18. robotcode/language_server/robotframework/parts/inline_value.py +69 -7
  19. robotcode/language_server/robotframework/parts/keywords_treeview.py +151 -53
  20. robotcode/language_server/robotframework/parts/model_variables.py +331 -0
  21. robotcode/language_server/robotframework/parts/references.py +47 -31
  22. robotcode/language_server/robotframework/parts/rename.py +1 -2
  23. robotcode/language_server/robotframework/parts/robot_workspace.py +229 -25
  24. robotcode/language_server/robotframework/parts/selection_range.py +26 -13
  25. robotcode/language_server/robotframework/parts/semantic_tokens.py +414 -113
  26. robotcode/language_server/robotframework/parts/signature_help.py +162 -70
  27. robotcode/language_server/robotframework/protocol.py +1 -0
  28. {robotcode_language_server-2.6.2.dist-info → robotcode_language_server-2.8.0.dev548.dist-info}/METADATA +7 -6
  29. {robotcode_language_server-2.6.2.dist-info → robotcode_language_server-2.8.0.dev548.dist-info}/RECORD +31 -30
  30. {robotcode_language_server-2.6.2.dist-info → robotcode_language_server-2.8.0.dev548.dist-info}/WHEEL +1 -1
  31. {robotcode_language_server-2.6.2.dist-info → robotcode_language_server-2.8.0.dev548.dist-info}/entry_points.txt +0 -0
@@ -39,7 +39,6 @@ from robot.parsing.model.statements import (
39
39
  TestTemplate,
40
40
  Variable,
41
41
  VariablesImport,
42
- WhileHeader,
43
42
  )
44
43
  from robot.utils.escaping import unescape
45
44
 
@@ -67,8 +66,14 @@ from robotcode.robot.diagnostics.library_doc import (
67
66
  )
68
67
  from robotcode.robot.diagnostics.model_helper import ModelHelper
69
68
  from robotcode.robot.diagnostics.namespace import Namespace
70
- from robotcode.robot.diagnostics.semantic_analyzer.enums import TokenKind
69
+ from robotcode.robot.diagnostics.semantic_analyzer.enums import NodeKind, TokenKind, TokenModifier
71
70
  from robotcode.robot.diagnostics.semantic_analyzer.model import SemanticModel
71
+ from robotcode.robot.diagnostics.semantic_analyzer.nodes import (
72
+ RunKeywordCallStatement,
73
+ SemanticStatement,
74
+ SemanticToken,
75
+ )
76
+ from robotcode.robot.diagnostics.semantic_analyzer.variable_tokenizer import VariableSite, build_variable_sub_tokens
72
77
  from robotcode.robot.utils import RF_VERSION
73
78
  from robotcode.robot.utils.ast import (
74
79
  cached_isinstance,
@@ -1034,46 +1039,51 @@ class SemanticTokenGenerator:
1034
1039
  creating and managing its own token mapper and keyword analyzer.
1035
1040
  """
1036
1041
 
1037
- # Mapping from SemanticModel TokenKind to LSP semantic token types.
1038
- # Used by collect_tokens_from_model() for the new model-based path.
1039
- _TOKEN_KIND_TO_SEM_TOKEN: Dict[TokenKind, Tuple[AnyTokenType, Optional[Set[AnyTokenModifier]]]] = {
1042
+ # ------------------------------------------------------------------
1043
+ # Model-based path (Tier 1): declarative rendering of the SemanticModel.
1044
+ #
1045
+ # After analysis every token carries its final render semantics (kind +
1046
+ # modifiers), so rendering is: leaf descent -> static table lookup ->
1047
+ # delta encoding, plus a small legacy-compat emission policy keyed only
1048
+ # on TokenKind / NodeKind / TokenModifier (design D6/D7). No RF version
1049
+ # checks, no tokenization, no statement-type semantics here.
1050
+ # ------------------------------------------------------------------
1051
+
1052
+ _TOKEN_KIND_TO_SEM_TOKEN: ClassVar[Dict[TokenKind, Tuple[AnyTokenType, Optional[Set[AnyTokenModifier]]]]] = {
1040
1053
  TokenKind.KEYWORD: (RobotSemTokenTypes.KEYWORD, None),
1054
+ TokenKind.KEYWORD_INNER: (RobotSemTokenTypes.KEYWORD_INNER, None),
1041
1055
  TokenKind.BDD_PREFIX: (RobotSemTokenTypes.BDD_PREFIX, None),
1042
1056
  TokenKind.NAMESPACE: (RobotSemTokenTypes.NAMESPACE, None),
1057
+ # Variables render their name only (see `_render_variable_part`).
1043
1058
  TokenKind.VARIABLE: (RobotSemTokenTypes.VARIABLE, None),
1044
1059
  TokenKind.VARIABLE_NOT_FOUND: (RobotSemTokenTypes.VARIABLE, None),
1045
- TokenKind.VARIABLE_PREFIX: (RobotSemTokenTypes.VARIABLE_BEGIN, None),
1046
- TokenKind.VARIABLE_OPEN_BRACE: (RobotSemTokenTypes.VARIABLE_BEGIN, None),
1047
- TokenKind.VARIABLE_CLOSE_BRACE: (RobotSemTokenTypes.VARIABLE_END, None),
1048
- TokenKind.VARIABLE_BASE: (RobotSemTokenTypes.VARIABLE, None),
1049
- TokenKind.VARIABLE_EXTENDED: (RobotSemTokenTypes.VARIABLE, None),
1050
- TokenKind.VARIABLE_TYPE_SEPARATOR: (SemanticTokenTypes.OPERATOR, None),
1051
- TokenKind.VARIABLE_TYPE_HINT: (SemanticTokenTypes.TYPE, None),
1052
- TokenKind.VARIABLE_DEFAULT_SEPARATOR: (SemanticTokenTypes.OPERATOR, None),
1060
+ TokenKind.VARIABLE_NAME: (RobotSemTokenTypes.VARIABLE, None),
1053
1061
  TokenKind.VARIABLE_DEFAULT_VALUE: (RobotSemTokenTypes.ARGUMENT, None),
1054
- TokenKind.VARIABLE_PATTERN_SEPARATOR: (SemanticTokenTypes.OPERATOR, None),
1055
1062
  TokenKind.VARIABLE_PATTERN: (RobotSemTokenTypes.ARGUMENT, None),
1056
- TokenKind.VARIABLE_ASSIGN_MARK: (RobotSemTokenTypes.VARIABLE, None),
1057
- TokenKind.VARIABLE_EXPRESSION_OPEN: (RobotSemTokenTypes.EXPRESSION_BEGIN, None),
1058
- TokenKind.VARIABLE_EXPRESSION_CLOSE: (RobotSemTokenTypes.EXPRESSION_END, None),
1059
- TokenKind.PYTHON_EXPRESSION: (RobotSemTokenTypes.VARIABLE_EXPRESSION, None),
1060
- TokenKind.PYTHON_VARIABLE_REF: (RobotSemTokenTypes.VARIABLE, None),
1061
- TokenKind.VARIABLE_INDEX: (RobotSemTokenTypes.VARIABLE, None),
1062
- TokenKind.VARIABLE_INDEX_OPEN: (RobotSemTokenTypes.VARIABLE_BEGIN, None),
1063
- TokenKind.VARIABLE_INDEX_CLOSE: (RobotSemTokenTypes.VARIABLE_END, None),
1064
- TokenKind.VARIABLE_INDEX_CONTENT: (RobotSemTokenTypes.VARIABLE, None),
1065
1063
  TokenKind.TEXT_FRAGMENT: (RobotSemTokenTypes.ARGUMENT, None),
1066
1064
  TokenKind.ARGUMENT: (RobotSemTokenTypes.ARGUMENT, None),
1067
1065
  TokenKind.NAMED_ARGUMENT_NAME: (RobotSemTokenTypes.NAMED_ARGUMENT, None),
1068
1066
  TokenKind.NAMED_ARGUMENT_VALUE: (RobotSemTokenTypes.ARGUMENT, None),
1067
+ TokenKind.PARAMETER: (SemanticTokenTypes.PARAMETER, None),
1069
1068
  TokenKind.CONTROL_FLOW: (RobotSemTokenTypes.CONTROL_FLOW, None),
1070
1069
  TokenKind.CONDITION: (RobotSemTokenTypes.ARGUMENT, None),
1070
+ TokenKind.FOR_SEPARATOR: (RobotSemTokenTypes.FOR_SEPARATOR, None),
1071
+ TokenKind.VAR_MARKER: (RobotSemTokenTypes.VAR, None),
1072
+ TokenKind.OPTION: (RobotSemTokenTypes.CONTROL_FLOW, None),
1073
+ TokenKind.OPTION_VALUE: (RobotSemTokenTypes.CONTROL_FLOW, None),
1071
1074
  TokenKind.TEST_NAME: (RobotSemTokenTypes.TESTCASE_NAME, {SemanticTokenModifiers.DECLARATION}),
1072
1075
  TokenKind.KEYWORD_NAME: (RobotSemTokenTypes.KEYWORD_NAME, {SemanticTokenModifiers.DECLARATION}),
1073
- TokenKind.VARIABLE_NAME: (RobotSemTokenTypes.VARIABLE, {SemanticTokenModifiers.DECLARATION}),
1074
1076
  TokenKind.SETTING_NAME: (RobotSemTokenTypes.SETTING, None),
1075
- TokenKind.IMPORT_NAME: (RobotSemTokenTypes.SETTING_IMPORT, None),
1077
+ TokenKind.SETTING_IMPORT: (RobotSemTokenTypes.SETTING_IMPORT, None),
1078
+ TokenKind.IMPORT_NAME: (RobotSemTokenTypes.NAMESPACE, None),
1079
+ TokenKind.OPERATOR: (SemanticTokenTypes.OPERATOR, None),
1076
1080
  TokenKind.HEADER: (RobotSemTokenTypes.HEADER, None),
1081
+ TokenKind.HEADER_SETTINGS: (RobotSemTokenTypes.HEADER_SETTINGS, None),
1082
+ TokenKind.HEADER_VARIABLE: (RobotSemTokenTypes.HEADER_VARIABLE, None),
1083
+ TokenKind.HEADER_TESTCASE: (RobotSemTokenTypes.HEADER_TESTCASE, None),
1084
+ TokenKind.HEADER_TASK: (RobotSemTokenTypes.HEADER_TASK, None),
1085
+ TokenKind.HEADER_KEYWORD: (RobotSemTokenTypes.HEADER_KEYWORD, None),
1086
+ TokenKind.HEADER_COMMENT: (RobotSemTokenTypes.HEADER_COMMENT, None),
1077
1087
  TokenKind.SEPARATOR: (RobotSemTokenTypes.SEPARATOR, None),
1078
1088
  TokenKind.CONTINUATION: (RobotSemTokenTypes.CONTINUATION, None),
1079
1089
  TokenKind.COMMENT: (SemanticTokenTypes.COMMENT, None),
@@ -1082,6 +1092,61 @@ class SemanticTokenGenerator:
1082
1092
  TokenKind.ERROR: (RobotSemTokenTypes.ERROR, None),
1083
1093
  }
1084
1094
 
1095
+ _TOKEN_MODIFIER_TO_SEM: ClassVar[Dict[TokenModifier, AnyTokenModifier]] = {
1096
+ TokenModifier.BUILTIN: RobotSemTokenModifiers.BUILTIN,
1097
+ TokenModifier.EMBEDDED: RobotSemTokenModifiers.EMBEDDED,
1098
+ TokenModifier.DECLARATION: SemanticTokenModifiers.DECLARATION,
1099
+ TokenModifier.DOCUMENTATION: SemanticTokenModifiers.DOCUMENTATION,
1100
+ }
1101
+
1102
+ # --- Legacy-compat emission policy (design D7) ---
1103
+
1104
+ # Whole options of control structures render atomically: one control-flow
1105
+ # token per option, no descent into their sub-structure.
1106
+ _ATOMIC_KINDS: ClassVar[FrozenSet[TokenKind]] = frozenset({TokenKind.OPTION, TokenKind.OPTION_VALUE})
1107
+ # Variables and parameters render only their name (and type hint); the
1108
+ # grammar shows prefix, braces, item access, `=`, patterns, defaults and
1109
+ # inline Python.
1110
+ _VARIABLE_KINDS: ClassVar[FrozenSet[TokenKind]] = frozenset(
1111
+ {TokenKind.VARIABLE, TokenKind.VARIABLE_NOT_FOUND, TokenKind.VARIABLE_NAME, TokenKind.PARAMETER}
1112
+ )
1113
+ # Model-only kinds: carried for model consumers (inline values, debug
1114
+ # variable extraction), never rendered — legacy highlights no bare-`$var`
1115
+ # expression references.
1116
+ _MODEL_ONLY_KINDS: ClassVar[FrozenSet[TokenKind]] = frozenset({TokenKind.PYTHON_VARIABLE_REF})
1117
+ # Plain argument text is suppressed by the legacy path...
1118
+ _ARGUMENT_TEXT_KINDS: ClassVar[FrozenSet[TokenKind]] = frozenset(
1119
+ {
1120
+ TokenKind.ARGUMENT,
1121
+ TokenKind.TEXT_FRAGMENT,
1122
+ TokenKind.TAG,
1123
+ TokenKind.CONDITION,
1124
+ TokenKind.NAMED_ARGUMENT_VALUE,
1125
+ TokenKind.VARIABLE_DEFAULT_VALUE,
1126
+ TokenKind.VARIABLE_PATTERN,
1127
+ }
1128
+ )
1129
+ # ...except in template argument rows and metadata values...
1130
+ # (TEMPLATE_KEYWORD: an unresolved template name keeps kind ARGUMENT and
1131
+ # must still render, matching legacy.)
1132
+ _YIELD_ARGUMENT_STMT_KINDS: ClassVar[FrozenSet[NodeKind]] = frozenset(
1133
+ {NodeKind.TEMPLATE_DATA, NodeKind.SETTING_METADATA, NodeKind.TEMPLATE_KEYWORD}
1134
+ )
1135
+ # Comments only render on keyword-call / import statements (and inside
1136
+ # invalid sections).
1137
+ _COMMENT_STMT_KINDS: ClassVar[FrozenSet[NodeKind]] = frozenset(
1138
+ {NodeKind.KEYWORD_CALL, NodeKind.SETUP, NodeKind.TEARDOWN, NodeKind.TEMPLATE_KEYWORD, NodeKind.IMPORT}
1139
+ )
1140
+ # Statements whose keyword name comes from a Token.NAME cell -- their
1141
+ # BDD-prefix gap token renders as `argument` (the NAME cell's initial
1142
+ # legacy mapping), not as a keyword.
1143
+ _NAME_KEYWORD_STMT_KINDS: ClassVar[FrozenSet[NodeKind]] = frozenset(
1144
+ {NodeKind.SETUP, NodeKind.TEARDOWN, NodeKind.TEMPLATE_KEYWORD}
1145
+ )
1146
+ _BDD_FOLLOW_KINDS: ClassVar[FrozenSet[TokenKind]] = frozenset(
1147
+ {TokenKind.KEYWORD, TokenKind.KEYWORD_INNER, TokenKind.NAMESPACE, TokenKind.ARGUMENT}
1148
+ )
1149
+
1085
1150
  def __init__(self) -> None:
1086
1151
  """Initialize the generator with its own dependencies."""
1087
1152
  self.token_mapper = SemanticTokenMapper()
@@ -1098,8 +1163,9 @@ class SemanticTokenGenerator:
1098
1163
  """Collect semantic tokens from the pre-built SemanticModel.
1099
1164
 
1100
1165
  This is the Tier 1 model-based path, used when the semantic model
1101
- feature flag is enabled. It maps SemanticModel TokenKind values
1102
- to LSP semantic token types via _TOKEN_KIND_TO_SEM_TOKEN.
1166
+ feature flag is enabled. The model carries final render semantics,
1167
+ so this is a declarative mapping (see the class-level tables) plus
1168
+ UTF-16 delta encoding.
1103
1169
  """
1104
1170
  data: List[int] = []
1105
1171
  last_line = 0
@@ -1109,30 +1175,21 @@ class SemanticTokenGenerator:
1109
1175
  for stmt in semantic_model.statements:
1110
1176
  check_current_task_canceled()
1111
1177
 
1112
- for token in stmt.tokens:
1113
- if token.length == 0:
1178
+ for info in self._iter_model_sem_tokens(stmt, semantic_model):
1179
+ if info.length == 0:
1114
1180
  continue
1115
1181
 
1182
+ token_line_0 = info.lineno - 1
1116
1183
  # Range filtering
1117
- token_line_0 = token.line - 1
1118
- if range is not None:
1119
- if token_line_0 < range.start.line:
1120
- continue
1121
- if token_line_0 > range.end.line:
1122
- break
1123
-
1124
- sem_info = self._TOKEN_KIND_TO_SEM_TOKEN.get(token.kind)
1125
- if sem_info is None:
1184
+ if range is not None and (token_line_0 < range.start.line or token_line_0 > range.end.line):
1126
1185
  continue
1127
1186
 
1128
- sem_type, sem_mods = sem_info
1129
-
1130
1187
  # Convert to UTF-16 positions
1131
1188
  token_range = range_to_utf16(
1132
1189
  lines,
1133
1190
  Range(
1134
- start=Position(line=token_line_0, character=token.col_offset),
1135
- end=Position(line=token_line_0, character=token.col_offset + token.length),
1191
+ start=Position(line=token_line_0, character=info.col_offset),
1192
+ end=Position(line=token_line_0, character=info.col_offset + info.length),
1136
1193
  ),
1137
1194
  )
1138
1195
 
@@ -1154,11 +1211,244 @@ class SemanticTokenGenerator:
1154
1211
  last_line = current_line
1155
1212
 
1156
1213
  data.append(token_length)
1157
- data.append(token_types.index(sem_type))
1158
- data.append(reduce(operator.or_, [2 ** token_modifiers.index(e) for e in sem_mods]) if sem_mods else 0)
1214
+ data.append(token_types.index(info.sem_token_type))
1215
+ data.append(
1216
+ reduce(operator.or_, [2 ** token_modifiers.index(e) for e in info.sem_modifiers])
1217
+ if info.sem_modifiers
1218
+ else 0
1219
+ )
1159
1220
 
1160
1221
  return SemanticTokens(data=data)
1161
1222
 
1223
+ def _emit_model_token(self, token: SemanticToken) -> Optional[SemTokenInfo]:
1224
+ """Map one model token through the static kind / modifier tables."""
1225
+ sem_info = self._TOKEN_KIND_TO_SEM_TOKEN.get(token.kind)
1226
+ if sem_info is None:
1227
+ return None
1228
+ sem_type, static_mods = sem_info
1229
+ sem_mods: Optional[Set[AnyTokenModifier]] = set(static_mods) if static_mods else None
1230
+ if token.modifiers:
1231
+ mapped = {self._TOKEN_MODIFIER_TO_SEM[m] for m in token.modifiers if m in self._TOKEN_MODIFIER_TO_SEM}
1232
+ if mapped:
1233
+ sem_mods = (sem_mods or set()) | mapped
1234
+ return SemTokenInfo(token.line, token.col_offset, token.length, sem_type, sem_mods)
1235
+
1236
+ def _iter_model_sem_tokens(
1237
+ self, stmt: SemanticStatement, model: SemanticModel, is_inner: bool = False
1238
+ ) -> Iterator[SemTokenInfo]:
1239
+ """Yield SemTokenInfo for one statement, matching the legacy output.
1240
+
1241
+ `is_inner` marks a Run Keyword inner call (its BDD gap token renders
1242
+ as keywordCallInner).
1243
+ """
1244
+ # Documentation statements emit only their continuation markers
1245
+ # (legacy renders neither the setting name nor the body).
1246
+ if stmt.kind is NodeKind.SETTING_DOCUMENTATION:
1247
+ for token in stmt.tokens:
1248
+ if token.kind is TokenKind.CONTINUATION:
1249
+ yield SemTokenInfo(token.line, token.col_offset, token.length, RobotSemTokenTypes.CONTINUATION)
1250
+ return
1251
+
1252
+ section = model.enclosing_section(stmt)
1253
+ emit_comments = stmt.kind in self._COMMENT_STMT_KINDS or (
1254
+ section is not None and section.kind is NodeKind.INVALID_SECTION
1255
+ )
1256
+
1257
+ if cached_isinstance(stmt, RunKeywordCallStatement):
1258
+ kw_doc = stmt.keyword_doc
1259
+ # Legacy only decomposes the hardcoded BuiltIn run keywords;
1260
+ # register-only run keywords render flat.
1261
+ if kw_doc is not None and kw_doc.is_any_run_keyword():
1262
+ yield from self._iter_run_keyword_sem_tokens(stmt, model, emit_comments, is_inner)
1263
+ return
1264
+
1265
+ yield_arguments = stmt.kind in self._YIELD_ARGUMENT_STMT_KINDS
1266
+
1267
+ prev: Optional[SemanticToken] = None
1268
+ for token in stmt.tokens:
1269
+ gap = self._bdd_gap_token(stmt, prev, token, is_inner)
1270
+ if gap is not None:
1271
+ yield gap
1272
+ yield from self._render_model_token(token, emit_comments, yield_arguments)
1273
+ prev = token
1274
+
1275
+ def _bdd_gap_token(
1276
+ self,
1277
+ stmt: SemanticStatement,
1278
+ prev: Optional[SemanticToken],
1279
+ token: SemanticToken,
1280
+ is_inner: bool,
1281
+ ) -> Optional[SemTokenInfo]:
1282
+ """Legacy quirk: a BDD prefix (`Given`/`When`/...) emits a length-1
1283
+ keyword-typed token for the separator character between the prefix
1284
+ word and the keyword name. The model folds that space into the prefix
1285
+ offset, so it is synthesized here from adjacent leaf kinds."""
1286
+ if (
1287
+ prev is None
1288
+ or prev.kind is not TokenKind.BDD_PREFIX
1289
+ or token.kind not in self._BDD_FOLLOW_KINDS
1290
+ or token.col_offset <= prev.col_offset + prev.length
1291
+ ):
1292
+ return None
1293
+ if is_inner:
1294
+ sem_type: AnyTokenType = RobotSemTokenTypes.KEYWORD_INNER
1295
+ elif stmt.kind in self._NAME_KEYWORD_STMT_KINDS:
1296
+ sem_type = RobotSemTokenTypes.ARGUMENT
1297
+ else:
1298
+ sem_type = RobotSemTokenTypes.KEYWORD
1299
+ gap_col = prev.col_offset + prev.length
1300
+ return SemTokenInfo(prev.line, gap_col, token.col_offset - gap_col, sem_type)
1301
+
1302
+ def _render_model_token(
1303
+ self,
1304
+ token: SemanticToken,
1305
+ emit_comments: bool,
1306
+ yield_arguments: bool,
1307
+ ) -> Iterator[SemTokenInfo]:
1308
+ """Render one SemanticToken (recursively descending into sub-tokens)
1309
+ through the static tables and the declarative emission policy."""
1310
+ kind = token.kind
1311
+ mods = token.modifiers
1312
+
1313
+ if kind in self._MODEL_ONLY_KINDS:
1314
+ return
1315
+
1316
+ if kind in self._VARIABLE_KINDS and token.sub_tokens:
1317
+ for sub in token.sub_tokens:
1318
+ yield from self._render_variable_part(sub, token)
1319
+ return
1320
+
1321
+ # Atomic kinds render whole; their sub-structure stays model-only.
1322
+ if kind in self._ATOMIC_KINDS:
1323
+ info = self._emit_model_token(token)
1324
+ if info is not None:
1325
+ yield info
1326
+ return
1327
+
1328
+ # Legacy quirk: `[Documentation]` / `Metadata` setting names render
1329
+ # as nothing.
1330
+ if kind is TokenKind.SETTING_NAME and mods and TokenModifier.DOCUMENTATION in mods:
1331
+ return
1332
+
1333
+ # Legacy quirk: an embedded keyword name that does not match its own
1334
+ # pattern (parent carries EMBEDDED, no sub-tokens) renders as nothing.
1335
+ if (
1336
+ kind in (TokenKind.KEYWORD, TokenKind.KEYWORD_INNER)
1337
+ and mods
1338
+ and TokenModifier.EMBEDDED in mods
1339
+ and not token.sub_tokens
1340
+ ):
1341
+ return
1342
+
1343
+ # Leaf descent: emit the leaves instead of the parent.
1344
+ if token.sub_tokens:
1345
+ for sub in token.sub_tokens:
1346
+ yield from self._render_model_token(sub, emit_comments, yield_arguments)
1347
+ return
1348
+
1349
+ # Whitespace / line-structure tokens are never rendered.
1350
+ if kind is TokenKind.SEPARATOR or kind is TokenKind.EOL:
1351
+ return
1352
+
1353
+ if kind is TokenKind.COMMENT:
1354
+ if emit_comments:
1355
+ yield SemTokenInfo(token.line, token.col_offset, token.length, SemanticTokenTypes.COMMENT)
1356
+ return
1357
+
1358
+ # Plain argument text renders only in template rows / metadata values
1359
+ # and for embedded-argument fragments.
1360
+ if kind in self._ARGUMENT_TEXT_KINDS and not yield_arguments and not (mods and TokenModifier.EMBEDDED in mods):
1361
+ return
1362
+
1363
+ info = self._emit_model_token(token)
1364
+ if info is not None:
1365
+ yield info
1366
+
1367
+ def _render_variable_part(self, token: SemanticToken, variable: SemanticToken) -> Iterator[SemTokenInfo]:
1368
+ """Render one sub-token of `variable`: its name with the type and
1369
+ modifiers of the variable, its type hint as `type`, nested variables as
1370
+ variables of their own, and nothing else."""
1371
+ kind = token.kind
1372
+ if kind in self._VARIABLE_KINDS:
1373
+ yield from self._render_model_token(token, emit_comments=False, yield_arguments=False)
1374
+ return
1375
+ if kind is TokenKind.PYTHON_EXPRESSION:
1376
+ return
1377
+ if token.sub_tokens:
1378
+ for sub in token.sub_tokens:
1379
+ yield from self._render_variable_part(sub, variable)
1380
+ return
1381
+ if kind is TokenKind.VARIABLE_BASE:
1382
+ info = self._emit_model_token(variable)
1383
+ if info is not None:
1384
+ yield SemTokenInfo(token.line, token.col_offset, token.length, info.sem_token_type, info.sem_modifiers)
1385
+ elif kind is TokenKind.VARIABLE_TYPE_HINT:
1386
+ yield SemTokenInfo(token.line, token.col_offset, token.length, SemanticTokenTypes.TYPE)
1387
+
1388
+ def _iter_run_keyword_sem_tokens(
1389
+ self,
1390
+ stmt: "RunKeywordCallStatement",
1391
+ model: SemanticModel,
1392
+ emit_comments: bool,
1393
+ is_inner: bool,
1394
+ ) -> Iterator[SemTokenInfo]:
1395
+ """Render a Run Keyword variant, position-merging inner calls with the
1396
+ outer token stream. Cells covered by an inner call render from the
1397
+ inner (KEYWORD_INNER) decomposition; the remaining outer tokens (outer
1398
+ keyword, condition, ELSE/AND control-flow cells) render from the outer
1399
+ list. The merge is purely structural."""
1400
+ inner_infos: List[SemTokenInfo] = []
1401
+ covered: List[Tuple[Tuple[int, int], Tuple[int, int]]] = []
1402
+ for inner in stmt.inner_calls:
1403
+ span = self._statement_span(inner)
1404
+ if span is not None:
1405
+ covered.append(span)
1406
+ inner_infos.extend(self._iter_model_sem_tokens(inner, model, is_inner=True))
1407
+
1408
+ outer_infos: List[SemTokenInfo] = []
1409
+ prev: Optional[SemanticToken] = None
1410
+ for token in stmt.tokens:
1411
+ pos = (token.line, token.col_offset)
1412
+ if any(start <= pos < end for start, end in covered):
1413
+ prev = token
1414
+ continue
1415
+ gap = self._bdd_gap_token(stmt, prev, token, is_inner)
1416
+ if gap is not None:
1417
+ outer_infos.append(gap)
1418
+ outer_infos.extend(self._render_model_token(token, emit_comments, False))
1419
+ prev = token
1420
+
1421
+ merged = inner_infos + outer_infos
1422
+ merged.sort(key=lambda i: (i.lineno, i.col_offset))
1423
+ yield from merged
1424
+
1425
+ @staticmethod
1426
+ def _statement_span(
1427
+ stmt: SemanticStatement,
1428
+ ) -> Optional[Tuple[Tuple[int, int], Tuple[int, int]]]:
1429
+ """Source span (start, end) as (line, col) tuples over all tokens of a
1430
+ statement, recursing into nested Run Keyword inner calls."""
1431
+ start: Optional[Tuple[int, int]] = None
1432
+ end: Optional[Tuple[int, int]] = None
1433
+
1434
+ def visit(s: SemanticStatement) -> None:
1435
+ nonlocal start, end
1436
+ for token in s.tokens:
1437
+ lo = (token.line, token.col_offset)
1438
+ hi = (token.line, token.col_offset + token.length)
1439
+ if start is None or lo < start:
1440
+ start = lo
1441
+ if end is None or hi > end:
1442
+ end = hi
1443
+ if cached_isinstance(s, RunKeywordCallStatement):
1444
+ for inner in s.inner_calls:
1445
+ visit(inner)
1446
+
1447
+ visit(stmt)
1448
+ if start is None or end is None:
1449
+ return None
1450
+ return start, end
1451
+
1162
1452
  def _get_tokens_after(self, tokens: List[Token], target_token: Token) -> List[Token]:
1163
1453
  """Get all tokens after target token efficiently.
1164
1454
 
@@ -1194,6 +1484,7 @@ class SemanticTokenGenerator:
1194
1484
  col_offset: Optional[int] = None,
1195
1485
  length: Optional[int] = None,
1196
1486
  yield_arguments: bool = False,
1487
+ site: VariableSite = VariableSite.USAGE,
1197
1488
  ) -> Iterator[SemTokenInfo]:
1198
1489
  """Generate semantic token information for Robot Framework tokens.
1199
1490
 
@@ -1205,6 +1496,7 @@ class SemanticTokenGenerator:
1205
1496
  col_offset: Optional column offset override
1206
1497
  length: Optional length override
1207
1498
  yield_arguments: Whether to yield argument tokens
1499
+ site: Where a variable token stands
1208
1500
 
1209
1501
  Yields:
1210
1502
  SemTokenInfo: Semantic token information for LSP client
@@ -1393,37 +1685,8 @@ class SemanticTokenGenerator:
1393
1685
  col_offset,
1394
1686
  length,
1395
1687
  )
1396
- elif token.type == Token.OPTION:
1397
- if (
1398
- cached_isinstance(node, ExceptHeader) or cached_isinstance(node, WhileHeader)
1399
- ) and "=" in token.value:
1400
- if col_offset is None:
1401
- col_offset = token.col_offset
1402
-
1403
- name, value = token.value.split("=", 1)
1404
- yield SemTokenInfo.from_token(
1405
- token,
1406
- RobotSemTokenTypes.VARIABLE,
1407
- sem_mod,
1408
- col_offset,
1409
- len(name),
1410
- )
1411
- yield SemTokenInfo.from_token(
1412
- token,
1413
- SemanticTokenTypes.OPERATOR,
1414
- sem_mod,
1415
- col_offset + len(name),
1416
- 1,
1417
- )
1418
- yield SemTokenInfo.from_token(
1419
- token,
1420
- sem_type,
1421
- sem_mod,
1422
- col_offset + len(name) + 1,
1423
- len(value),
1424
- )
1425
- else:
1426
- yield SemTokenInfo.from_token(token, sem_type, sem_mod, col_offset, length)
1688
+ elif token.type in (Token.VARIABLE, Token.ASSIGN):
1689
+ yield from self._generate_variable_sem_tokens(namespace, token, sem_type, sem_mod, site)
1427
1690
  elif (
1428
1691
  token.type in Token.SETTING_TOKENS and token.value and token.value[0] == "[" and token.value[-1] == "]"
1429
1692
  ):
@@ -1471,33 +1734,37 @@ class SemanticTokenGenerator:
1471
1734
  if token.type in {Token.ARGUMENT, Token.TESTCASE_NAME, Token.KEYWORD_NAME} or (
1472
1735
  token.type == Token.NAME and cached_isinstance(node, VariablesImport, LibraryImport, ResourceImport)
1473
1736
  ):
1474
- if (
1737
+ if cached_isinstance(node, Arguments):
1738
+ # The name of an argument is a parameter; a default value continues as argument.
1739
+ name, value = split_from_equals(token.value)
1740
+ if name:
1741
+ yield from self._generate_variable_sem_tokens(
1742
+ namespace,
1743
+ Token(Token.VARIABLE, name, token.lineno, token.col_offset),
1744
+ SemanticTokenTypes.PARAMETER,
1745
+ None,
1746
+ VariableSite.DECLARATION,
1747
+ )
1748
+ token = Token(
1749
+ token.type,
1750
+ value or "",
1751
+ token.lineno,
1752
+ token.col_offset + len(name) + (1 if value is not None else 0),
1753
+ token.error,
1754
+ )
1755
+ elif (
1475
1756
  cached_isinstance(node, Variable) and token.type == Token.ARGUMENT and node.name and node.name[0] == "&"
1476
- ) or (cached_isinstance(node, Arguments)):
1757
+ ):
1477
1758
  name, value = split_from_equals(token.value)
1478
1759
  if value is not None:
1479
1760
  length = len(name)
1480
1761
 
1481
1762
  yield SemTokenInfo.from_token(
1482
- Token(
1483
- ROBOT_NAMED_ARGUMENT if cached_isinstance(node, Variable) else SemanticTokenTypes.PARAMETER,
1484
- name,
1485
- token.lineno,
1486
- token.col_offset,
1487
- ),
1488
- (
1489
- RobotSemTokenTypes.NAMED_ARGUMENT
1490
- if cached_isinstance(node, Variable)
1491
- else SemanticTokenTypes.PARAMETER
1492
- ),
1763
+ Token(ROBOT_NAMED_ARGUMENT, name, token.lineno, token.col_offset),
1764
+ RobotSemTokenTypes.NAMED_ARGUMENT,
1493
1765
  )
1494
1766
  yield SemTokenInfo.from_token(
1495
- Token(
1496
- ROBOT_OPERATOR,
1497
- "=",
1498
- token.lineno,
1499
- token.col_offset + length,
1500
- ),
1767
+ Token(ROBOT_OPERATOR, "=", token.lineno, token.col_offset + length),
1501
1768
  SemanticTokenTypes.OPERATOR,
1502
1769
  )
1503
1770
  token = Token(
@@ -1507,36 +1774,70 @@ class SemanticTokenGenerator:
1507
1774
  token.col_offset + length + 1,
1508
1775
  token.error,
1509
1776
  )
1510
- elif cached_isinstance(node, Arguments) and name:
1511
- yield SemTokenInfo.from_token(
1512
- Token(
1513
- ROBOT_NAMED_ARGUMENT,
1514
- name,
1515
- token.lineno,
1516
- token.col_offset,
1517
- ),
1518
- RobotSemTokenTypes.NAMED_ARGUMENT,
1519
- )
1520
- token = Token(
1521
- token.type,
1522
- "",
1523
- token.lineno,
1524
- token.col_offset + len(name),
1525
- token.error,
1526
- )
1527
1777
 
1778
+ site = VariableSite.KEYWORD_NAME if token.type == Token.KEYWORD_NAME else VariableSite.USAGE
1528
1779
  for sub_token in ModelHelper.tokenize_variables(
1529
1780
  token,
1530
1781
  ignore_errors=True,
1531
1782
  identifiers="$" if token.type == Token.KEYWORD_NAME else "$@&%",
1532
1783
  ):
1533
- for e in self.generate_sem_sub_tokens(namespace, builtin_library_doc, sub_token, node):
1784
+ for e in self.generate_sem_sub_tokens(namespace, builtin_library_doc, sub_token, node, site=site):
1534
1785
  yield e
1535
1786
 
1536
1787
  else:
1537
- for e in self.generate_sem_sub_tokens(namespace, builtin_library_doc, token, node):
1788
+ # A whole VARIABLE or ASSIGN cell defines a variable.
1789
+ site = (
1790
+ VariableSite.UNTYPED_DECLARATION if cached_isinstance(node, ExceptHeader) else VariableSite.DECLARATION
1791
+ )
1792
+ for e in self.generate_sem_sub_tokens(namespace, builtin_library_doc, token, node, site=site):
1538
1793
  yield e
1539
1794
 
1795
+ def _generate_variable_sem_tokens(
1796
+ self,
1797
+ namespace: Namespace,
1798
+ token: Token,
1799
+ sem_type: AnyTokenType,
1800
+ sem_mod: Optional[Set[AnyTokenModifier]],
1801
+ site: VariableSite,
1802
+ ) -> Iterator[SemTokenInfo]:
1803
+ """A variable sends its name with `sem_type` and its type hint as `type`;
1804
+ prefix, braces, item access, `=`, patterns, defaults and inline Python
1805
+ are left to the grammar. The parts are split like the semantic model
1806
+ splits them."""
1807
+ position = Position(line=token.lineno - 1, character=token.col_offset)
1808
+
1809
+ def is_defined(name: str) -> bool:
1810
+ return namespace.find_variable(name, position=position) is not None
1811
+
1812
+ sub_tokens = build_variable_sub_tokens(
1813
+ token.value, token.lineno, token.col_offset, site=site, is_defined=is_defined
1814
+ )
1815
+ if not sub_tokens:
1816
+ yield SemTokenInfo.from_token(token, sem_type, sem_mod)
1817
+ return
1818
+ yield from self._iter_variable_name_sem_tokens(sub_tokens, sem_type, sem_mod)
1819
+
1820
+ def _iter_variable_name_sem_tokens(
1821
+ self,
1822
+ sub_tokens: Sequence[SemanticToken],
1823
+ sem_type: AnyTokenType,
1824
+ sem_mod: Optional[Set[AnyTokenModifier]],
1825
+ ) -> Iterator[SemTokenInfo]:
1826
+ for sub in sub_tokens:
1827
+ if sub.kind is TokenKind.VARIABLE:
1828
+ if sub.sub_tokens:
1829
+ yield from self._iter_variable_name_sem_tokens(sub.sub_tokens, RobotSemTokenTypes.VARIABLE, None)
1830
+ else:
1831
+ yield SemTokenInfo(sub.line, sub.col_offset, sub.length, RobotSemTokenTypes.VARIABLE)
1832
+ elif sub.kind is TokenKind.PYTHON_EXPRESSION:
1833
+ continue
1834
+ elif sub.sub_tokens:
1835
+ yield from self._iter_variable_name_sem_tokens(sub.sub_tokens, sem_type, sem_mod)
1836
+ elif sub.kind is TokenKind.VARIABLE_BASE:
1837
+ yield SemTokenInfo(sub.line, sub.col_offset, sub.length, sem_type, sem_mod)
1838
+ elif sub.kind is TokenKind.VARIABLE_TYPE_HINT:
1839
+ yield SemTokenInfo(sub.line, sub.col_offset, sub.length, SemanticTokenTypes.TYPE)
1840
+
1540
1841
  def collect_tokens(
1541
1842
  self,
1542
1843
  document: TextDocument,