python-delphi-lsp 2.1.0__py3-none-any.whl → 2.3.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
delphi_lsp/_version.py CHANGED
@@ -1 +1 @@
1
- __version__ = "2.1.0"
1
+ __version__ = "2.3.0"
delphi_lsp/agent_cache.py CHANGED
@@ -20,6 +20,8 @@ import threading
20
20
  import time
21
21
  from types import ModuleType
22
22
 
23
+ from watchfiles import watch
24
+
23
25
  from ._version import __version__
24
26
  from .agent_context import AgentContext
25
27
  from .agent_protocol import AgentProtocolError
@@ -32,10 +34,15 @@ DEFAULT_IDLE_TIMEOUT = 1800
32
34
  DEFAULT_STARTUP_TIMEOUT = 120.0
33
35
  _MAX_MESSAGE_BYTES = 1024 * 1024
34
36
  _CONNECTION_TIMEOUT = 2.0
37
+ _CLIENT_RESPONSE_TIMEOUT = 120.0
35
38
  _MEMORY_SIZE = re.compile(r"^(?P<count>[1-9][0-9]*)(?P<suffix>[KMG]?)$", re.IGNORECASE)
36
39
  _STARTUP_DIAGNOSTIC_BYTES = 16 * 1024
37
40
  _STARTUP_TOKEN_RE = re.compile(r"(?i)(token\b[^\n\r]*?:?\s*['\"]?[A-Za-z0-9_-]+['\"]?|\b[a-zA-Z0-9_-]{32,})")
38
41
  _START_LOCK_INCOMPLETE_GRACE_SECONDS = 1.0
42
+ _CACHE_REVISION_CHECK_INTERVAL_SECONDS = 3600.0
43
+ _WATCHED_SUFFIXES = frozenset(
44
+ {".pas", ".pp", ".inc", ".dpr", ".dpk", ".dproj", ".cfg"}
45
+ )
39
46
 
40
47
 
41
48
  def estimate_deep_size(value: object) -> int:
@@ -273,6 +280,18 @@ def _safe_metadata_path(root: str | Path, *, create: bool = False) -> Path:
273
280
  return result
274
281
 
275
282
 
283
+ def _safe_navigation_cache_path(root: str | Path, *, create: bool = False) -> Path:
284
+ parent = _safe_metadata_path(root, create=create).parent
285
+ result = parent / "navigation-v1"
286
+ if result.exists() and result.is_symlink():
287
+ raise CacheClientError("unsafe_metadata", "Navigation cache path is unsafe.")
288
+ if create:
289
+ result.mkdir(mode=0o700, exist_ok=True)
290
+ if os.name != "nt":
291
+ os.chmod(result, 0o700)
292
+ return result
293
+
294
+
276
295
  def _metadata_mapping(metadata: CacheMetadata) -> dict[str, object]:
277
296
  return {field.name: getattr(metadata, field.name) for field in fields(metadata)}
278
297
 
@@ -475,7 +494,7 @@ def _start_lock(root: str | Path, timeout: float):
475
494
  def _client_exchange(metadata: CacheMetadata, request: dict[str, object]) -> CacheClientResponse:
476
495
  try:
477
496
  with socket.create_connection(("127.0.0.1", metadata.port), timeout=2) as connection:
478
- connection.settimeout(3)
497
+ connection.settimeout(_CLIENT_RESPONSE_TIMEOUT)
479
498
  request_without_token = {key: value for key, value in request.items() if key != "token"}
480
499
  connection.sendall(json.dumps({"token": metadata.token, **request_without_token}, separators=(",", ":")).encode("utf-8") + b"\n")
481
500
  response = _read_line(connection)
@@ -541,6 +560,11 @@ class _CacheService:
541
560
  metadata.project_file or None,
542
561
  workers=metadata.workers,
543
562
  worker_memory_budget_bytes=metadata.max_memory_bytes,
563
+ revision_check_interval_seconds=_CACHE_REVISION_CHECK_INTERVAL_SECONDS,
564
+ navigation_cache_dir=_safe_navigation_cache_path(
565
+ metadata.root,
566
+ create=True,
567
+ ),
544
568
  )
545
569
  self.budget = CacheBudget(metadata.max_memory_bytes)
546
570
  self.stats = CacheStats()
@@ -556,15 +580,14 @@ class _CacheService:
556
580
  def prewarm(self) -> None:
557
581
  started = time.monotonic()
558
582
  try:
559
- self.context.handle({"action": "find", "query": "", "max_items": 1, "max_chars": 256})
560
- self.last_revision = self.context.workspace.workspace_revision
583
+ self.last_revision = self.context.prewarm_navigation()
561
584
  self.cache_state = "warm"
562
585
  except AgentProtocolError as error:
563
586
  if error.code != "project_required":
564
587
  raise
565
588
  self.cache_state = "ready"
566
589
  self.last_budget = self.budget.enforce(
567
- measure=lambda: estimate_deep_size(self.context.cache_roots()),
590
+ measure=lambda: self.context.estimated_cache_bytes,
568
591
  evict_auxiliary=self.context.evict_auxiliary_caches,
569
592
  evict_navigation=self.context.evict_navigation_caches,
570
593
  )
@@ -605,7 +628,7 @@ class _CacheService:
605
628
  if before != after:
606
629
  self.stats.invalidations += 1
607
630
  self.last_budget = self.budget.enforce(
608
- measure=lambda: estimate_deep_size(self.context.cache_roots()),
631
+ measure=lambda: self.context.estimated_cache_bytes,
609
632
  evict_auxiliary=self.context.evict_auxiliary_caches,
610
633
  evict_navigation=self.context.evict_navigation_caches,
611
634
  )
@@ -641,11 +664,34 @@ class _CacheService:
641
664
  "prewarm_seconds": self.prewarm_seconds,
642
665
  "parallel_seconds": self.context.parallel_stats.elapsed_seconds,
643
666
  "parallel_fallbacks": self.stats.parallel_fallbacks,
667
+ "navigation_disk_hits": self.context.navigation_disk_hits,
668
+ "navigation_disk_misses": self.context.navigation_disk_misses,
644
669
  "idle_timeout": self.metadata.idle_timeout, "idle_remaining": max(0.0, self.metadata.idle_timeout - idle),
645
- "workspace_revision": self.context.workspace.workspace_revision,
670
+ "workspace_revision": self.last_revision,
646
671
  }
647
672
 
648
673
 
674
+ def _watch_filter(_change: object, path: str) -> bool:
675
+ return Path(path).suffix.casefold() in _WATCHED_SUFFIXES
676
+
677
+
678
+ def _watch_workspace(service: _CacheService) -> None:
679
+ try:
680
+ for changes in watch(
681
+ service.metadata.root,
682
+ watch_filter=_watch_filter,
683
+ stop_event=service.shutdown,
684
+ debounce=50,
685
+ step=20,
686
+ recursive=True,
687
+ raise_interrupt=False,
688
+ ):
689
+ if changes:
690
+ service.context.invalidate_revision_cache()
691
+ except (OSError, RuntimeError):
692
+ service.context.invalidate_revision_cache()
693
+
694
+
649
695
  def _serve_connection(connection: socket.socket, service: _CacheService) -> None:
650
696
  try:
651
697
  line = _read_line(connection)
@@ -691,9 +737,17 @@ def run_cache_daemon(
691
737
  idle_timeout,
692
738
  time.time(),
693
739
  )
740
+ watcher: threading.Thread | None = None
694
741
  try:
695
742
  service = _CacheService(metadata)
696
743
  service.prewarm()
744
+ watcher = threading.Thread(
745
+ target=_watch_workspace,
746
+ args=(service,),
747
+ name="delphi-cache-watcher",
748
+ daemon=True,
749
+ )
750
+ watcher.start()
697
751
  _write_metadata(metadata)
698
752
  while not service.shutdown.is_set() and time.monotonic() - service.last_activity < idle_timeout:
699
753
  try:
@@ -704,6 +758,10 @@ def run_cache_daemon(
704
758
  connection.settimeout(_CONNECTION_TIMEOUT)
705
759
  _serve_connection(connection, service)
706
760
  finally:
761
+ if "service" in locals():
762
+ service.shutdown.set()
763
+ if watcher is not None:
764
+ watcher.join(timeout=2.0)
707
765
  listener.close()
708
766
  _remove_metadata_if_owned(metadata)
709
767