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