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
@@ -26,6 +26,7 @@ from robotcode.robot.diagnostics.entities import (
26
26
  ResourceEntry,
27
27
  VariableDefinition,
28
28
  VariableDefinitionType,
29
+ VariablesEntry,
29
30
  )
30
31
  from robotcode.robot.diagnostics.library_doc import (
31
32
  RESOURCE_FILE_EXTENSION,
@@ -33,7 +34,6 @@ from robotcode.robot.diagnostics.library_doc import (
33
34
  KeywordDoc,
34
35
  LibraryDoc,
35
36
  )
36
- from robotcode.robot.diagnostics.model_helper import ModelHelper
37
37
  from robotcode.robot.diagnostics.namespace import Namespace
38
38
  from robotcode.robot.utils import RF_VERSION
39
39
  from robotcode.robot.utils.ast import (
@@ -55,7 +55,7 @@ _ReferencesMethod = Callable[
55
55
  ]
56
56
 
57
57
 
58
- class RobotReferencesProtocolPart(RobotLanguageServerProtocolPart, ModelHelper):
58
+ class RobotReferencesProtocolPart(RobotLanguageServerProtocolPart):
59
59
  _logger = LoggingDescriptor()
60
60
 
61
61
  def __init__(self, parent: "RobotLanguageServerProtocol") -> None:
@@ -68,7 +68,7 @@ class RobotReferencesProtocolPart(RobotLanguageServerProtocolPart, ModelHelper):
68
68
 
69
69
  parent.references.collect.add(self.collect)
70
70
  parent.documents.did_change.add(self.document_did_change)
71
- parent.documents.on_document_cache_invalidated(self.document_did_change)
71
+ parent.documents.on_document_cache_invalidated.add(self.document_did_change)
72
72
  parent.diagnostics.on_workspace_diagnostics_break.add(self.on_workspace_diagnostics_break)
73
73
 
74
74
  @event
@@ -80,7 +80,8 @@ class RobotReferencesProtocolPart(RobotLanguageServerProtocolPart, ModelHelper):
80
80
  f"**/*.{{{ROBOT_FILE_EXTENSION[1:]},{RESOURCE_FILE_EXTENSION[1:]}}}",
81
81
  WatchKind.CREATE | WatchKind.DELETE,
82
82
  )
83
- self.parent.documents_cache.namespace_invalidated(self.namespace_invalidated)
83
+ self.parent.documents_cache.namespace_invalidated.add(self.namespace_invalidated)
84
+ self.parent.documents_cache.namespace_initialized.add(self.namespace_initialized)
84
85
 
85
86
  def do_on_file_changed(self, sender: Any, files: List[FileEvent]) -> None:
86
87
  self.clear_cache()
@@ -93,6 +94,11 @@ class RobotReferencesProtocolPart(RobotLanguageServerProtocolPart, ModelHelper):
93
94
  def namespace_invalidated(self, sender: Any, namespace: Namespace) -> None:
94
95
  self.clear_cache()
95
96
 
97
+ @language_id("robotframework")
98
+ def namespace_initialized(self, sender: Any, namespace: Namespace) -> None:
99
+ # the new analysis changed the index, so a result found before it can miss its references
100
+ self.clear_cache()
101
+
96
102
  def on_workspace_diagnostics_break(self, sender: Any) -> None:
97
103
  self.clear_cache()
98
104
 
@@ -330,18 +336,21 @@ class RobotReferencesProtocolPart(RobotLanguageServerProtocolPart, ModelHelper):
330
336
  def _find_library_import_references_in_file(self, doc: TextDocument, library_doc: LibraryDoc) -> List[Location]:
331
337
  namespace = self.parent.documents_cache.get_namespace(doc)
332
338
 
333
- result: List[Location] = []
334
- for lib_entry in namespace.libraries.values():
335
- if (
336
- lib_entry.import_source == str(doc.uri.to_path())
337
- and lib_entry.library_doc.source_or_origin == library_doc.source_or_origin
338
- ):
339
- result.append(Location(str(doc.uri), lib_entry.import_range))
340
-
339
+ # every import statement, also one of a library that is already imported
340
+ result: List[Location] = [
341
+ Location(str(doc.uri), lib_entry.import_range)
342
+ for lib_entry in namespace.import_entries.values()
343
+ if not isinstance(lib_entry, (ResourceEntry, VariablesEntry))
344
+ and lib_entry.import_source == str(doc.uri.to_path())
345
+ and lib_entry.library_doc.source_or_origin == library_doc.source_or_origin
346
+ and lib_entry.library_doc.name == library_doc.name
347
+ ]
348
+
349
+ # the analysis also keeps a repeated import as a reference of the first one
341
350
  references = namespace.namespace_references
342
351
  for k, v in references.items():
343
352
  if not k.alias and k.library_doc == library_doc:
344
- result.extend(v)
353
+ result.extend(location for location in v if location not in result)
345
354
 
346
355
  return result
347
356
 
@@ -415,17 +424,20 @@ class RobotReferencesProtocolPart(RobotLanguageServerProtocolPart, ModelHelper):
415
424
  def _find_resource_import_references_in_file(self, doc: TextDocument, entry: ResourceEntry) -> List[Location]:
416
425
  namespace = self.parent.documents_cache.get_namespace(doc)
417
426
 
418
- result: List[Location] = []
419
- for lib_entry in namespace.resources.values():
420
- if (
421
- lib_entry.import_source == str(doc.uri.to_path())
422
- and lib_entry.library_doc.source == entry.library_doc.source
423
- ):
424
- result.append(Location(str(doc.uri), lib_entry.import_range))
427
+ # every import statement, also one of a resource file that is already imported
428
+ result: List[Location] = [
429
+ Location(str(doc.uri), lib_entry.import_range)
430
+ for lib_entry in namespace.import_entries.values()
431
+ if isinstance(lib_entry, ResourceEntry)
432
+ and lib_entry.import_source == str(doc.uri.to_path())
433
+ and lib_entry.library_doc.source == entry.library_doc.source
434
+ ]
425
435
 
436
+ # the calls with the resource's name as prefix, in every file that imports the resource
426
437
  references = namespace.namespace_references
427
- if entry in references:
428
- result.extend(references[entry])
438
+ for k, v in references.items():
439
+ if isinstance(k, ResourceEntry) and k.library_doc.source == entry.library_doc.source:
440
+ result.extend(location for location in v if location not in result)
429
441
 
430
442
  return result
431
443
 
@@ -448,12 +460,14 @@ class RobotReferencesProtocolPart(RobotLanguageServerProtocolPart, ModelHelper):
448
460
  if not name_token:
449
461
  return None
450
462
 
451
- entries = namespace.resources
463
+ # the entry of this statement, also of a repeated import of the resource file
452
464
  entry = next(
453
465
  (
454
466
  v
455
- for v in entries.values()
456
- if v.import_source == namespace.source and v.import_range == range_from_token(name_token)
467
+ for v in namespace.import_entries.values()
468
+ if isinstance(v, ResourceEntry)
469
+ and v.import_source == namespace.source
470
+ and v.import_range == range_from_token(name_token)
457
471
  ),
458
472
  None,
459
473
  )
@@ -484,12 +498,14 @@ class RobotReferencesProtocolPart(RobotLanguageServerProtocolPart, ModelHelper):
484
498
  def _find_variables_import_references_in_file(self, doc: TextDocument, library_doc: LibraryDoc) -> List[Location]:
485
499
  namespace = self.parent.documents_cache.get_namespace(doc)
486
500
 
487
- result: List[Location] = []
488
- for lib_entry in namespace.variables_imports.values():
489
- if lib_entry.import_source == str(doc.uri.to_path()) and lib_entry.library_doc.source == library_doc.source:
490
- result.append(Location(str(doc.uri), lib_entry.import_range))
491
-
492
- return result
501
+ # every import statement, also one of a variable file that is already imported
502
+ return [
503
+ Location(str(doc.uri), lib_entry.import_range)
504
+ for lib_entry in namespace.import_entries.values()
505
+ if isinstance(lib_entry, VariablesEntry)
506
+ and lib_entry.import_source == str(doc.uri.to_path())
507
+ and lib_entry.library_doc.source == library_doc.source
508
+ ]
493
509
 
494
510
  def references_VariablesImport( # noqa: N802
495
511
  self,
@@ -33,7 +33,6 @@ from robotcode.robot.diagnostics.entities import (
33
33
  VariableDefinitionType,
34
34
  )
35
35
  from robotcode.robot.diagnostics.library_doc import KeywordDoc
36
- from robotcode.robot.diagnostics.model_helper import ModelHelper
37
36
 
38
37
  from ...common.parts.rename import CantRenameError
39
38
  from .protocol_part import RobotLanguageServerProtocolPart
@@ -48,7 +47,7 @@ _PrepareRenameMethod = Callable[[ast.AST, TextDocument, Position], Optional[Prep
48
47
  _T = TypeVar("_T", bound=Callable[..., Any])
49
48
 
50
49
 
51
- class RobotRenameProtocolPart(RobotLanguageServerProtocolPart, ModelHelper):
50
+ class RobotRenameProtocolPart(RobotLanguageServerProtocolPart):
52
51
  _logger = LoggingDescriptor()
53
52
 
54
53
  def __init__(self, parent: "RobotLanguageServerProtocol") -> None:
@@ -1,18 +1,35 @@
1
+ import importlib
2
+ import threading
1
3
  from concurrent.futures import CancelledError
2
4
  from logging import CRITICAL
5
+ from pathlib import Path
3
6
  from threading import Event
4
- from typing import TYPE_CHECKING, Any, Optional
7
+ from typing import TYPE_CHECKING, Any, Dict, List, Optional
5
8
 
6
- from robotcode.core.ignore_spec import DEFAULT_SPEC_RULES, GIT_IGNORE_FILE, ROBOT_IGNORE_FILE, IgnoreSpec, iter_files
9
+ from robotcode.core.concurrent import run_as_task
10
+ from robotcode.core.documents_manager import CantReadDocumentError
11
+ from robotcode.core.ignore_spec import (
12
+ DEFAULT_SPEC_RULES,
13
+ GIT_IGNORE_FILE,
14
+ ROBOT_IGNORE_FILE,
15
+ IgnoreSpec,
16
+ iter_files,
17
+ iter_files_includes,
18
+ )
7
19
  from robotcode.core.language import language_id
20
+ from robotcode.core.lsp.types import DocumentUri, FileChangeType, FileEvent, WatchKind
21
+ from robotcode.core.text_document import TextDocument
8
22
  from robotcode.core.uri import Uri
9
23
  from robotcode.core.utils.logging import LoggingDescriptor
24
+ from robotcode.core.utils.path import probe_disk_info
25
+ from robotcode.core.workspace import WorkspaceFolder
10
26
  from robotcode.jsonrpc2.protocol import rpc_method
11
27
  from robotcode.language_server.common.parts.diagnostics import (
12
28
  AnalysisProgressMode,
13
29
  DiagnosticsMode,
14
30
  )
15
31
  from robotcode.language_server.robotframework.configuration import AnalysisConfig
32
+ from robotcode.robot.diagnostics.imports_manager import ImportsManager
16
33
  from robotcode.robot.diagnostics.library_doc import (
17
34
  RESOURCE_FILE_EXTENSION,
18
35
  ROBOT_FILE_EXTENSION,
@@ -25,6 +42,9 @@ if TYPE_CHECKING:
25
42
 
26
43
  from .protocol_part import RobotLanguageServerProtocolPart
27
44
 
45
+ WORKSPACE_FILE_EXTENSIONS = [ROBOT_FILE_EXTENSION, RESOURCE_FILE_EXTENSION]
46
+ WORKSPACE_IGNORE_FILES = [ROBOT_IGNORE_FILE, GIT_IGNORE_FILE]
47
+
28
48
 
29
49
  class RobotWorkspaceProtocolPart(RobotLanguageServerProtocolPart):
30
50
  _logger = LoggingDescriptor()
@@ -32,11 +52,43 @@ class RobotWorkspaceProtocolPart(RobotLanguageServerProtocolPart):
32
52
  def __init__(self, parent: "RobotLanguageServerProtocol") -> None:
33
53
  super().__init__(parent)
34
54
  self.parent.documents.on_read_document_text.add(self.on_read_document_text)
55
+ self.parent.documents.did_create_uri.add(self._on_did_create_uri)
56
+ self.parent.on_initialized.add(self._on_initialized)
57
+ self.parent.diagnostics.on_workspace_loaded.add(self._on_workspace_loaded)
58
+ self.parent.on_shutdown.add(self._stop_file_checks)
35
59
  self.parent.diagnostics.load_workspace_documents.add(self.load_workspace_documents)
36
60
  self.parent.diagnostics.on_get_diagnostics_mode.add(self.on_get_diagnostics_mode)
37
61
  self.parent.diagnostics.on_get_analysis_progress_mode.add(self.on_get_analysis_progress_mode)
38
62
  self.documents_loaded = Event()
39
63
 
64
+ # the periodic check of the files of each workspace folder, by the folder's URI
65
+ self._file_check_lock = threading.RLock()
66
+ self._file_check_timers: Dict[str, threading.Timer] = {}
67
+ self._file_checks_stopped = False
68
+
69
+ def _on_initialized(self, sender: Any) -> None:
70
+ # the workspace exists only after the initialize request, the parts of the protocol already before it
71
+ self.parent.workspace.did_change_configuration.add(self._on_did_change_configuration)
72
+ self.parent.workspace.add_file_watcher(
73
+ self._on_files_deleted,
74
+ f"**/*.{{{ROBOT_FILE_EXTENSION[1:]},{RESOURCE_FILE_EXTENSION[1:]}}}",
75
+ WatchKind.DELETE,
76
+ )
77
+
78
+ def _on_files_deleted(self, sender: Any, changes: List[FileEvent]) -> None:
79
+ for change in changes:
80
+ if change.type != FileChangeType.DELETED:
81
+ continue
82
+
83
+ uri = Uri(change.uri).normalized()
84
+ if uri.scheme != "file":
85
+ continue
86
+
87
+ path = uri.to_path()
88
+ # the cache keeps the entries of a file that is there again
89
+ if path.suffix in WORKSPACE_FILE_EXTENSIONS and not path.exists():
90
+ self.parent.documents_cache.remove_cached_analysis(path)
91
+
40
92
  @language_id("robotframework")
41
93
  def on_read_document_text(self, sender: Any, uri: Uri) -> Optional[str]:
42
94
  from robot.utils import FileReader
@@ -52,36 +104,67 @@ class RobotWorkspaceProtocolPart(RobotLanguageServerProtocolPart):
52
104
  config = self.parent.workspace.get_configuration(AnalysisConfig, uri)
53
105
  return config.progress_mode
54
106
 
107
+ def _exclude_spec(self, folder: WorkspaceFolder) -> IgnoreSpec:
108
+ config = self.parent.workspace.get_configuration(RobotCodeConfig, folder.uri)
109
+ exclude_patterns = [
110
+ *self.parent.analysis_config.exclude_patterns,
111
+ *(config.workspace.exclude_patterns or []),
112
+ ]
113
+ return IgnoreSpec.from_list([*DEFAULT_SPEC_RULES, *exclude_patterns], folder.uri.to_path())
114
+
115
+ def collect_workspace_files(self, folder: WorkspaceFolder) -> List[Path]:
116
+ """The Robot Framework files of a workspace folder, without the excluded and ignored ones."""
117
+ return [
118
+ f
119
+ for f in iter_files(
120
+ folder.uri.to_path(),
121
+ ignore_files=WORKSPACE_IGNORE_FILES,
122
+ include_hidden=False,
123
+ parent_spec=self._exclude_spec(folder),
124
+ verbose_callback=self._logger.debug,
125
+ verbose_trace=False,
126
+ )
127
+ if f.suffix in WORKSPACE_FILE_EXTENSIONS
128
+ ]
129
+
130
+ def is_workspace_file(self, folder: WorkspaceFolder, path: Path) -> bool:
131
+ """Whether `collect_workspace_files` would return `path`."""
132
+ return path.suffix in WORKSPACE_FILE_EXTENSIONS and iter_files_includes(
133
+ path,
134
+ folder.uri.to_path(),
135
+ ignore_files=WORKSPACE_IGNORE_FILES,
136
+ include_hidden=False,
137
+ parent_spec=self._exclude_spec(folder),
138
+ )
139
+
140
+ def _on_did_create_uri(self, sender: Any, uri: DocumentUri) -> None:
141
+ # a created file is analyzed like the files found at startup
142
+ file_uri = Uri(uri)
143
+ if file_uri.scheme != "file":
144
+ return
145
+
146
+ path = file_uri.to_path()
147
+ folder = self.parent.workspace.get_workspace_folder(file_uri)
148
+ if folder is None or not self.is_workspace_file(folder, path):
149
+ return
150
+
151
+ try:
152
+ document = self.parent.documents.get_or_open_document(path)
153
+ except CantReadDocumentError as e:
154
+ ex = e
155
+ self._logger.debug(lambda: f"Can't load created document {path}: {ex}")
156
+ return
157
+
158
+ self.parent.diagnostics.force_refresh_document(document)
159
+
55
160
  def load_workspace_documents(self, sender: Any) -> None:
56
161
  with self._logger.measure_time(lambda: "loading workspace documents", context_name="load_workspace_documents"):
57
162
  try:
58
163
  for folder in self.parent.workspace.workspace_folders:
59
164
  config = self.parent.workspace.get_configuration(RobotCodeConfig, folder.uri)
60
165
 
61
- extensions = [ROBOT_FILE_EXTENSION, RESOURCE_FILE_EXTENSION]
62
-
63
- exclude_patterns = [
64
- *self.parent.analysis_config.exclude_patterns,
65
- *(config.workspace.exclude_patterns or []),
66
- ]
67
-
68
166
  with self.parent.window.progress("Collect sources", cancellable=False):
69
- files = list(
70
- filter(
71
- lambda f: f.suffix in extensions,
72
- iter_files(
73
- folder.uri.to_path(),
74
- ignore_files=[ROBOT_IGNORE_FILE, GIT_IGNORE_FILE],
75
- include_hidden=False,
76
- parent_spec=IgnoreSpec.from_list(
77
- [*DEFAULT_SPEC_RULES, *exclude_patterns],
78
- folder.uri.to_path(),
79
- ),
80
- verbose_callback=self._logger.debug,
81
- verbose_trace=False,
82
- ),
83
- )
84
- )
167
+ files = self.collect_workspace_files(folder)
85
168
 
86
169
  canceled = False
87
170
  self._logger.debug(
@@ -122,6 +205,127 @@ class RobotWorkspaceProtocolPart(RobotLanguageServerProtocolPart):
122
205
  if canceled:
123
206
  self._logger.info(lambda: "Workspace loading canceled")
124
207
 
208
+ def _documents_of(self, folder: WorkspaceFolder) -> List[TextDocument]:
209
+ result = []
210
+ for document in list(self.parent.documents.documents):
211
+ document_folder = self.parent.workspace.get_workspace_folder(document.uri)
212
+ if document_folder is not None and document_folder.uri == folder.uri:
213
+ result.append(document)
214
+ return result
215
+
216
+ def check_workspace_folder(self, folder: WorkspaceFolder) -> None:
217
+ """Find the changes of the files of a workspace folder, and handle them like the file events of the client."""
218
+ changes: Dict[str, FileEvent] = {}
219
+
220
+ def report(path: Path, change: FileChangeType) -> None:
221
+ uri = str(Uri.from_path(path))
222
+ if uri not in changes:
223
+ changes[uri] = FileEvent(uri, change)
224
+
225
+ for path in self.collect_workspace_files(folder):
226
+ if self.parent.documents.get(Uri.from_path(path).normalized()) is None:
227
+ report(path, FileChangeType.CREATED)
228
+
229
+ # the documents open in the editor are the editor's
230
+ for document in self._documents_of(folder):
231
+ if document.opened_in_editor or document.uri.scheme != "file":
232
+ continue
233
+
234
+ path = document.uri.to_path()
235
+ disk_info = probe_disk_info(path)
236
+ if disk_info is None:
237
+ report(path, FileChangeType.DELETED)
238
+ elif disk_info != document.disk_info:
239
+ report(path, FileChangeType.CHANGED)
240
+
241
+ imports_manager = self.parent.documents_cache.get_imports_manager_for_workspace_folder(folder)
242
+ for file in imports_manager.changed_import_files():
243
+ report(Path(file), FileChangeType.CHANGED if Path(file).exists() else FileChangeType.DELETED)
244
+
245
+ for file in self._found_failed_imports(folder, imports_manager):
246
+ report(Path(file), FileChangeType.CREATED)
247
+
248
+ if changes:
249
+ self.parent.workspace.did_change_watched_files(self.parent.workspace, list(changes.values()))
250
+
251
+ def _found_failed_imports(self, folder: WorkspaceFolder, imports_manager: ImportsManager) -> List[str]:
252
+ """The files of the failed imports whose file or module was not found and is there now."""
253
+ # a module created since the import failed is found only with fresh finder caches
254
+ importlib.invalidate_caches()
255
+
256
+ result: List[str] = []
257
+ for document in self._documents_of(folder):
258
+ namespace = self.parent.documents_cache.get_only_initialized_namespace(document)
259
+ if namespace is None:
260
+ continue
261
+
262
+ for failed in namespace.failed_imports:
263
+ if failed.found:
264
+ continue
265
+ file = imports_manager.find_import_source(failed.imp, failed.base_dir, failed.variables)
266
+ if file is not None:
267
+ result.append(file)
268
+ return result
269
+
270
+ def _on_workspace_loaded(self, sender: Any) -> None:
271
+ self._schedule_file_checks()
272
+
273
+ def _on_did_change_configuration(self, sender: Any, settings: Dict[str, Any]) -> None:
274
+ # the interval of a folder may have changed; the configuration is read off the thread of the notification
275
+ run_as_task(self._schedule_file_checks)
276
+
277
+ def _schedule_file_checks(self) -> None:
278
+ if self._file_checks_stopped or not self.parent.diagnostics.workspace_loaded_event.is_set():
279
+ return
280
+
281
+ for folder in self.parent.workspace.workspace_folders:
282
+ self._schedule_file_check(folder)
283
+
284
+ def _file_check_interval(self, folder: WorkspaceFolder) -> int:
285
+ try:
286
+ interval = self.parent.workspace.get_configuration(AnalysisConfig, folder.uri).file_check_interval
287
+ except (SystemExit, KeyboardInterrupt):
288
+ raise
289
+ except BaseException as e:
290
+ # without a timer the folder would not be checked again
291
+ self._logger.exception(e)
292
+ interval = AnalysisConfig().file_check_interval
293
+ return min(max(interval, 1), 1440)
294
+
295
+ def _schedule_file_check(self, folder: WorkspaceFolder) -> None:
296
+ # reading the configuration can wait for the client, so it happens without the lock that the shutdown takes
297
+ interval = self._file_check_interval(folder)
298
+
299
+ with self._file_check_lock:
300
+ if self._file_checks_stopped:
301
+ return
302
+
303
+ timer = self._file_check_timers.pop(str(folder.uri), None)
304
+ if timer is not None:
305
+ timer.cancel()
306
+
307
+ timer = threading.Timer(interval * 60, self._run_file_check, (folder,))
308
+ timer.daemon = True
309
+ self._file_check_timers[str(folder.uri)] = timer
310
+ timer.start()
311
+
312
+ def _run_file_check(self, folder: WorkspaceFolder) -> None:
313
+ try:
314
+ self.check_workspace_folder(folder)
315
+ except (SystemExit, KeyboardInterrupt):
316
+ raise
317
+ except BaseException as e:
318
+ self._logger.exception(e)
319
+ finally:
320
+ self._schedule_file_check(folder)
321
+
322
+ def _stop_file_checks(self, sender: Any) -> None:
323
+ with self._file_check_lock:
324
+ self._file_checks_stopped = True
325
+ for timer in self._file_check_timers.values():
326
+ timer.cancel()
327
+ self._file_check_timers.clear()
328
+
125
329
  @rpc_method(name="robot/cache/clear", threaded=True)
126
330
  def robot_cache_clear(self) -> None:
127
331
  for folder in self.parent.workspace.workspace_folders:
@@ -14,13 +14,14 @@ from robotcode.robot.utils.ast import (
14
14
  range_from_token,
15
15
  )
16
16
 
17
+ from .model_variables import find_model_variable_range_at
17
18
  from .protocol_part import RobotLanguageServerProtocolPart
18
19
 
19
20
  if TYPE_CHECKING:
20
21
  from ..protocol import RobotLanguageServerProtocol
21
22
 
22
23
 
23
- class RobotSelectionRangeProtocolPart(RobotLanguageServerProtocolPart, ModelHelper):
24
+ class RobotSelectionRangeProtocolPart(RobotLanguageServerProtocolPart):
24
25
  _logger = LoggingDescriptor()
25
26
 
26
27
  def __init__(self, parent: "RobotLanguageServerProtocol") -> None:
@@ -32,10 +33,12 @@ class RobotSelectionRangeProtocolPart(RobotLanguageServerProtocolPart, ModelHelp
32
33
  @_logger.call
33
34
  def collect(self, sender: Any, document: TextDocument, positions: List[Position]) -> Optional[List[SelectionRange]]:
34
35
  namespace = self.parent.documents_cache.get_namespace(document)
36
+ semantic_model = namespace.semantic_model
37
+ model = self.parent.documents_cache.get_model(document)
35
38
 
36
39
  results: List[SelectionRange] = []
37
40
  for position in positions:
38
- nodes = get_nodes_at_position(self.parent.documents_cache.get_model(document), position)
41
+ nodes = get_nodes_at_position(model, position)
39
42
 
40
43
  if not nodes:
41
44
  break
@@ -52,17 +55,27 @@ class RobotSelectionRangeProtocolPart(RobotLanguageServerProtocolPart, ModelHelp
52
55
  token = tokens[-1]
53
56
  if token is not None:
54
57
  current_range = SelectionRange(range_from_token(token), current_range)
55
- for var_token, _ in self.iter_variables_from_token(
56
- token,
57
- namespace,
58
- position,
59
- return_not_found=True,
60
- ):
61
- var_token_range = range_from_token(var_token)
62
-
63
- if position in var_token_range:
64
- current_range = SelectionRange(var_token_range, current_range)
65
- break
58
+ if semantic_model is not None:
59
+ # Model path: variable step from the model's
60
+ # statement tokens; the structural node/token
61
+ # walk above stays on the AST.
62
+ var_range = find_model_variable_range_at(
63
+ semantic_model, position, range_from_token(token)
64
+ )
65
+ if var_range is not None:
66
+ current_range = SelectionRange(var_range, current_range)
67
+ else:
68
+ for var_token, _ in ModelHelper.iter_variables_from_token(
69
+ token,
70
+ namespace,
71
+ position,
72
+ return_not_found=True,
73
+ ):
74
+ var_token_range = range_from_token(var_token)
75
+
76
+ if position in var_token_range:
77
+ current_range = SelectionRange(var_token_range, current_range)
78
+ break
66
79
 
67
80
  results.append(current_range)
68
81