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 +1 -1
- delphi_lsp/agent_cache.py +64 -6
- delphi_lsp/agent_context.py +721 -161
- delphi_lsp/agent_metrics.py +124 -27
- delphi_lsp/agent_protocol.py +4 -3
- delphi_lsp/agent_relations.py +300 -29
- delphi_lsp/agent_workspace.py +51 -3
- delphi_lsp/delphiast_lexer.py +284 -0
- delphi_lsp/delphiast_parser.py +1498 -0
- delphi_lsp/delphiast_tokens.py +41 -0
- delphi_lsp/metrics.py +80 -8
- delphi_lsp/navigation_cache.py +134 -0
- delphi_lsp/parallel_outline.py +49 -10
- delphi_lsp/parser.py +54 -4
- delphi_lsp/parser_backend.py +57 -0
- delphi_lsp/preprocessor.py +56 -20
- delphi_lsp/project_indexer.py +32 -1
- {python_delphi_lsp-2.1.0.dist-info → python_delphi_lsp-2.3.0.dist-info}/METADATA +30 -12
- python_delphi_lsp-2.3.0.dist-info/RECORD +42 -0
- python_delphi_lsp-2.1.0.dist-info/RECORD +0 -37
- {python_delphi_lsp-2.1.0.dist-info → python_delphi_lsp-2.3.0.dist-info}/WHEEL +0 -0
- {python_delphi_lsp-2.1.0.dist-info → python_delphi_lsp-2.3.0.dist-info}/entry_points.txt +0 -0
- {python_delphi_lsp-2.1.0.dist-info → python_delphi_lsp-2.3.0.dist-info}/licenses/LICENSE +0 -0
- {python_delphi_lsp-2.1.0.dist-info → python_delphi_lsp-2.3.0.dist-info}/top_level.txt +0 -0
delphi_lsp/_version.py
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
__version__ = "2.
|
|
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(
|
|
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.
|
|
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:
|
|
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:
|
|
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.
|
|
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
|
|