python-delphi-lsp 2.0.5__py3-none-any.whl → 2.1.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 +211 -32
- delphi_lsp/agent_cli.py +44 -5
- delphi_lsp/agent_context.py +74 -29
- delphi_lsp/agent_layers.py +32 -21
- delphi_lsp/parallel_outline.py +278 -0
- {python_delphi_lsp-2.0.5.dist-info → python_delphi_lsp-2.1.0.dist-info}/METADATA +30 -4
- {python_delphi_lsp-2.0.5.dist-info → python_delphi_lsp-2.1.0.dist-info}/RECORD +12 -11
- {python_delphi_lsp-2.0.5.dist-info → python_delphi_lsp-2.1.0.dist-info}/WHEEL +0 -0
- {python_delphi_lsp-2.0.5.dist-info → python_delphi_lsp-2.1.0.dist-info}/entry_points.txt +0 -0
- {python_delphi_lsp-2.0.5.dist-info → python_delphi_lsp-2.1.0.dist-info}/licenses/LICENSE +0 -0
- {python_delphi_lsp-2.0.5.dist-info → python_delphi_lsp-2.1.0.dist-info}/top_level.txt +0 -0
delphi_lsp/_version.py
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
__version__ = "2.0
|
|
1
|
+
__version__ = "2.1.0"
|
delphi_lsp/agent_cache.py
CHANGED
|
@@ -6,6 +6,7 @@ import argparse
|
|
|
6
6
|
import contextlib
|
|
7
7
|
import hmac
|
|
8
8
|
import json
|
|
9
|
+
import math
|
|
9
10
|
import os
|
|
10
11
|
from pathlib import Path
|
|
11
12
|
import secrets
|
|
@@ -26,13 +27,15 @@ from .agent_protocol import AgentProtocolError
|
|
|
26
27
|
|
|
27
28
|
DEFAULT_MAX_MEMORY_BYTES = 512 * 1024**2
|
|
28
29
|
WARNING_THRESHOLD_PERCENT = 80
|
|
29
|
-
DAEMON_SCHEMA =
|
|
30
|
+
DAEMON_SCHEMA = 2
|
|
30
31
|
DEFAULT_IDLE_TIMEOUT = 1800
|
|
32
|
+
DEFAULT_STARTUP_TIMEOUT = 120.0
|
|
31
33
|
_MAX_MESSAGE_BYTES = 1024 * 1024
|
|
32
34
|
_CONNECTION_TIMEOUT = 2.0
|
|
33
35
|
_MEMORY_SIZE = re.compile(r"^(?P<count>[1-9][0-9]*)(?P<suffix>[KMG]?)$", re.IGNORECASE)
|
|
34
36
|
_STARTUP_DIAGNOSTIC_BYTES = 16 * 1024
|
|
35
37
|
_STARTUP_TOKEN_RE = re.compile(r"(?i)(token\b[^\n\r]*?:?\s*['\"]?[A-Za-z0-9_-]+['\"]?|\b[a-zA-Z0-9_-]{32,})")
|
|
38
|
+
_START_LOCK_INCOMPLETE_GRACE_SECONDS = 1.0
|
|
36
39
|
|
|
37
40
|
|
|
38
41
|
def estimate_deep_size(value: object) -> int:
|
|
@@ -72,6 +75,7 @@ class CacheStats:
|
|
|
72
75
|
rebuilds: int = 0
|
|
73
76
|
invalidations: int = 0
|
|
74
77
|
evictions: int = 0
|
|
78
|
+
parallel_fallbacks: int = 0
|
|
75
79
|
|
|
76
80
|
|
|
77
81
|
@dataclass(frozen=True)
|
|
@@ -232,6 +236,7 @@ class CacheMetadata:
|
|
|
232
236
|
version: str
|
|
233
237
|
project_file: str
|
|
234
238
|
max_memory_bytes: int
|
|
239
|
+
workers: int
|
|
235
240
|
idle_timeout: int
|
|
236
241
|
started_at: float
|
|
237
242
|
|
|
@@ -291,7 +296,7 @@ def _write_metadata(metadata: CacheMetadata) -> None:
|
|
|
291
296
|
os.unlink(temporary)
|
|
292
297
|
|
|
293
298
|
|
|
294
|
-
def
|
|
299
|
+
def _read_metadata_record(root: str | Path) -> object | None:
|
|
295
300
|
path = _safe_metadata_path(root)
|
|
296
301
|
if not path.exists():
|
|
297
302
|
return None
|
|
@@ -309,15 +314,36 @@ def _read_metadata(root: str | Path) -> CacheMetadata | None:
|
|
|
309
314
|
raise
|
|
310
315
|
except (OSError, ValueError, UnicodeError):
|
|
311
316
|
return None
|
|
317
|
+
return raw
|
|
318
|
+
|
|
319
|
+
|
|
320
|
+
def _read_metadata(root: str | Path) -> CacheMetadata | None:
|
|
321
|
+
raw = _read_metadata_record(root)
|
|
312
322
|
required = {field.name for field in fields(CacheMetadata)}
|
|
313
323
|
if not isinstance(raw, dict) or set(raw) != required:
|
|
314
324
|
return None
|
|
315
|
-
values = tuple(
|
|
325
|
+
values = tuple(
|
|
326
|
+
raw[name]
|
|
327
|
+
for name in (
|
|
328
|
+
"schema",
|
|
329
|
+
"root",
|
|
330
|
+
"pid",
|
|
331
|
+
"port",
|
|
332
|
+
"token",
|
|
333
|
+
"version",
|
|
334
|
+
"project_file",
|
|
335
|
+
"max_memory_bytes",
|
|
336
|
+
"workers",
|
|
337
|
+
"idle_timeout",
|
|
338
|
+
"started_at",
|
|
339
|
+
)
|
|
340
|
+
)
|
|
316
341
|
if (type(values[0]) is not int or values[0] != DAEMON_SCHEMA or not isinstance(values[1], str)
|
|
317
342
|
or type(values[2]) is not int or values[2] <= 0 or type(values[3]) is not int or not 0 < values[3] < 65536
|
|
318
343
|
or not isinstance(values[4], str) or len(values[4]) < 32 or not isinstance(values[5], str)
|
|
319
344
|
or not isinstance(values[6], str) or type(values[7]) is not int or values[7] <= 0
|
|
320
|
-
or type(values[8]) is not int or
|
|
345
|
+
or type(values[8]) is not int or not 0 <= values[8] <= 32
|
|
346
|
+
or type(values[9]) is not int or values[9] <= 0 or type(values[10]) not in (int, float)):
|
|
321
347
|
return None
|
|
322
348
|
canonical = str(Path(root).resolve())
|
|
323
349
|
if values[1] != canonical:
|
|
@@ -325,15 +351,56 @@ def _read_metadata(root: str | Path) -> CacheMetadata | None:
|
|
|
325
351
|
return CacheMetadata(*values)
|
|
326
352
|
|
|
327
353
|
|
|
354
|
+
def _remove_incompatible_metadata_if_stale(root: str | Path) -> None:
|
|
355
|
+
path = _safe_metadata_path(root)
|
|
356
|
+
if not path.exists():
|
|
357
|
+
return
|
|
358
|
+
raw = _read_metadata_record(root)
|
|
359
|
+
canonical = str(Path(root).resolve())
|
|
360
|
+
pid = raw.get("pid") if isinstance(raw, dict) and raw.get("root") == canonical else None
|
|
361
|
+
if type(pid) is int and pid > 0 and _pid_alive(pid):
|
|
362
|
+
raise CacheClientError("unavailable", "Live cache daemon is unavailable.")
|
|
363
|
+
with contextlib.suppress(FileNotFoundError):
|
|
364
|
+
path.unlink()
|
|
365
|
+
|
|
366
|
+
|
|
367
|
+
def _metadata_is_owned(metadata: CacheMetadata) -> bool:
|
|
368
|
+
current = _read_metadata(metadata.root)
|
|
369
|
+
return current is not None and current.pid == metadata.pid and hmac.compare_digest(current.token, metadata.token)
|
|
370
|
+
|
|
371
|
+
|
|
328
372
|
def _remove_metadata_if_owned(metadata: CacheMetadata) -> None:
|
|
329
373
|
path = _safe_metadata_path(metadata.root)
|
|
330
|
-
|
|
331
|
-
if current is not None and current.pid == metadata.pid and hmac.compare_digest(current.token, metadata.token):
|
|
374
|
+
if _metadata_is_owned(metadata):
|
|
332
375
|
with contextlib.suppress(FileNotFoundError):
|
|
333
376
|
path.unlink()
|
|
334
377
|
|
|
335
378
|
|
|
379
|
+
def _windows_pid_alive(pid: int) -> bool:
|
|
380
|
+
import ctypes
|
|
381
|
+
from ctypes import wintypes
|
|
382
|
+
|
|
383
|
+
synchronize = 0x00100000
|
|
384
|
+
wait_timeout = 0x00000102
|
|
385
|
+
kernel32 = ctypes.windll.kernel32
|
|
386
|
+
kernel32.OpenProcess.argtypes = (wintypes.DWORD, wintypes.BOOL, wintypes.DWORD)
|
|
387
|
+
kernel32.OpenProcess.restype = wintypes.HANDLE
|
|
388
|
+
kernel32.WaitForSingleObject.argtypes = (wintypes.HANDLE, wintypes.DWORD)
|
|
389
|
+
kernel32.WaitForSingleObject.restype = wintypes.DWORD
|
|
390
|
+
kernel32.CloseHandle.argtypes = (wintypes.HANDLE,)
|
|
391
|
+
kernel32.CloseHandle.restype = wintypes.BOOL
|
|
392
|
+
handle = kernel32.OpenProcess(synchronize, False, pid)
|
|
393
|
+
if not handle:
|
|
394
|
+
return False
|
|
395
|
+
try:
|
|
396
|
+
return kernel32.WaitForSingleObject(handle, 0) == wait_timeout
|
|
397
|
+
finally:
|
|
398
|
+
kernel32.CloseHandle(handle)
|
|
399
|
+
|
|
400
|
+
|
|
336
401
|
def _pid_alive(pid: int) -> bool:
|
|
402
|
+
if os.name == "nt":
|
|
403
|
+
return _windows_pid_alive(pid)
|
|
337
404
|
try:
|
|
338
405
|
os.kill(pid, 0)
|
|
339
406
|
except OSError:
|
|
@@ -341,15 +408,41 @@ def _pid_alive(pid: int) -> bool:
|
|
|
341
408
|
return True
|
|
342
409
|
|
|
343
410
|
|
|
411
|
+
def _reap_child_if_exited(pid: int) -> bool:
|
|
412
|
+
waitpid = getattr(os, "waitpid", None)
|
|
413
|
+
nohang = getattr(os, "WNOHANG", None)
|
|
414
|
+
if not callable(waitpid) or nohang is None:
|
|
415
|
+
return False
|
|
416
|
+
try:
|
|
417
|
+
return waitpid(pid, nohang)[0] == pid
|
|
418
|
+
except (ChildProcessError, OSError):
|
|
419
|
+
return False
|
|
420
|
+
|
|
421
|
+
|
|
344
422
|
def _start_lock_path(root: str | Path) -> Path:
|
|
345
423
|
return _safe_metadata_path(root, create=True).with_name("start.lock")
|
|
346
424
|
|
|
347
425
|
|
|
426
|
+
def _start_lock_is_stale(path: Path) -> bool:
|
|
427
|
+
try:
|
|
428
|
+
age = max(0.0, time.time() - path.stat().st_mtime)
|
|
429
|
+
except OSError:
|
|
430
|
+
return True
|
|
431
|
+
try:
|
|
432
|
+
record = json.loads(path.read_text(encoding="utf-8"))
|
|
433
|
+
except (OSError, ValueError, UnicodeError):
|
|
434
|
+
return age >= _START_LOCK_INCOMPLETE_GRACE_SECONDS
|
|
435
|
+
owner = record.get("pid") if isinstance(record, dict) else None
|
|
436
|
+
if type(owner) is int and owner > 0:
|
|
437
|
+
return not _pid_alive(owner)
|
|
438
|
+
return age >= _START_LOCK_INCOMPLETE_GRACE_SECONDS
|
|
439
|
+
|
|
440
|
+
|
|
348
441
|
@contextlib.contextmanager
|
|
349
|
-
def _start_lock(root: str | Path):
|
|
442
|
+
def _start_lock(root: str | Path, timeout: float):
|
|
350
443
|
path = _start_lock_path(root)
|
|
351
444
|
token = secrets.token_urlsafe(24)
|
|
352
|
-
deadline = time.monotonic() +
|
|
445
|
+
deadline = time.monotonic() + timeout
|
|
353
446
|
while True:
|
|
354
447
|
try:
|
|
355
448
|
descriptor = os.open(path, os.O_WRONLY | os.O_CREAT | os.O_EXCL, 0o600)
|
|
@@ -361,14 +454,7 @@ def _start_lock(root: str | Path):
|
|
|
361
454
|
os.close(descriptor)
|
|
362
455
|
break
|
|
363
456
|
except FileExistsError:
|
|
364
|
-
|
|
365
|
-
record = json.loads(path.read_text(encoding="utf-8"))
|
|
366
|
-
owner = record.get("pid") if isinstance(record, dict) else None
|
|
367
|
-
started = record.get("started_at") if isinstance(record, dict) else None
|
|
368
|
-
stale = type(owner) is not int or not _pid_alive(owner) or type(started) not in (int, float) or time.time() - started > 30
|
|
369
|
-
except (OSError, ValueError, UnicodeError):
|
|
370
|
-
stale = True
|
|
371
|
-
if stale:
|
|
457
|
+
if _start_lock_is_stale(path):
|
|
372
458
|
with contextlib.suppress(FileNotFoundError):
|
|
373
459
|
path.unlink()
|
|
374
460
|
continue
|
|
@@ -450,7 +536,12 @@ def _read_startup_tail(diagnostics: object, *, max_bytes: int = _STARTUP_DIAGNOS
|
|
|
450
536
|
class _CacheService:
|
|
451
537
|
def __init__(self, metadata: CacheMetadata) -> None:
|
|
452
538
|
self.metadata = metadata
|
|
453
|
-
self.context = AgentContext.open(
|
|
539
|
+
self.context = AgentContext.open(
|
|
540
|
+
metadata.root,
|
|
541
|
+
metadata.project_file or None,
|
|
542
|
+
workers=metadata.workers,
|
|
543
|
+
worker_memory_budget_bytes=metadata.max_memory_bytes,
|
|
544
|
+
)
|
|
454
545
|
self.budget = CacheBudget(metadata.max_memory_bytes)
|
|
455
546
|
self.stats = CacheStats()
|
|
456
547
|
self.lock = threading.Lock()
|
|
@@ -459,9 +550,11 @@ class _CacheService:
|
|
|
459
550
|
self.cache_state = "warming"
|
|
460
551
|
self.last_revision = self.context.workspace.workspace_revision
|
|
461
552
|
self.last_budget = BudgetResult(0, 0.0, 0.0, False, False, False)
|
|
553
|
+
self.prewarm_seconds = 0.0
|
|
462
554
|
self.shutdown = threading.Event()
|
|
463
555
|
|
|
464
556
|
def prewarm(self) -> None:
|
|
557
|
+
started = time.monotonic()
|
|
465
558
|
try:
|
|
466
559
|
self.context.handle({"action": "find", "query": "", "max_items": 1, "max_chars": 256})
|
|
467
560
|
self.last_revision = self.context.workspace.workspace_revision
|
|
@@ -478,6 +571,8 @@ class _CacheService:
|
|
|
478
571
|
if self.last_budget.compacted:
|
|
479
572
|
self.stats.evictions += 1
|
|
480
573
|
self.cache_state = "compact"
|
|
574
|
+
self.prewarm_seconds = time.monotonic() - started
|
|
575
|
+
self.stats.parallel_fallbacks = self.context.parallel_stats.fallbacks
|
|
481
576
|
|
|
482
577
|
def request(self, request: dict[str, object]) -> CacheClientResponse:
|
|
483
578
|
with self.lock:
|
|
@@ -506,6 +601,7 @@ class _CacheService:
|
|
|
506
601
|
self.stats.warm_hits += 1
|
|
507
602
|
else:
|
|
508
603
|
self.stats.rebuilds += 1
|
|
604
|
+
self.stats.parallel_fallbacks += self.context.parallel_stats.fallbacks
|
|
509
605
|
if before != after:
|
|
510
606
|
self.stats.invalidations += 1
|
|
511
607
|
self.last_budget = self.budget.enforce(
|
|
@@ -539,6 +635,12 @@ class _CacheService:
|
|
|
539
635
|
"warning_active": self.last_budget.warning_active, "warning_threshold_percent": WARNING_THRESHOLD_PERCENT,
|
|
540
636
|
"cache_state": self.cache_state, "requests": self.stats.requests, "warm_hits": self.stats.warm_hits,
|
|
541
637
|
"rebuilds": self.stats.rebuilds, "invalidations": self.stats.invalidations, "evictions": self.stats.evictions,
|
|
638
|
+
"workers_configured": "auto" if self.metadata.workers == 0 else self.metadata.workers,
|
|
639
|
+
"workers_effective": self.context.parallel_stats.effective_workers,
|
|
640
|
+
"parallel_files_completed": self.context.parallel_stats.files_completed,
|
|
641
|
+
"prewarm_seconds": self.prewarm_seconds,
|
|
642
|
+
"parallel_seconds": self.context.parallel_stats.elapsed_seconds,
|
|
643
|
+
"parallel_fallbacks": self.stats.parallel_fallbacks,
|
|
542
644
|
"idle_timeout": self.metadata.idle_timeout, "idle_remaining": max(0.0, self.metadata.idle_timeout - idle),
|
|
543
645
|
"workspace_revision": self.context.workspace.workspace_revision,
|
|
544
646
|
}
|
|
@@ -562,14 +664,33 @@ def _serve_connection(connection: socket.socket, service: _CacheService) -> None
|
|
|
562
664
|
connection.sendall(json.dumps(response, separators=(",", ":")).encode("utf-8") + b"\n")
|
|
563
665
|
|
|
564
666
|
|
|
565
|
-
def run_cache_daemon(
|
|
667
|
+
def run_cache_daemon(
|
|
668
|
+
root: str | Path,
|
|
669
|
+
*,
|
|
670
|
+
project_file: str = "",
|
|
671
|
+
max_memory_bytes: int = DEFAULT_MAX_MEMORY_BYTES,
|
|
672
|
+
workers: int = 0,
|
|
673
|
+
idle_timeout: int = DEFAULT_IDLE_TIMEOUT,
|
|
674
|
+
) -> None:
|
|
566
675
|
canonical = str(Path(root).resolve())
|
|
567
676
|
listener = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
|
568
677
|
listener.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
|
|
569
678
|
listener.bind(("127.0.0.1", 0))
|
|
570
679
|
listener.listen(16)
|
|
571
680
|
listener.settimeout(0.25)
|
|
572
|
-
metadata = CacheMetadata(
|
|
681
|
+
metadata = CacheMetadata(
|
|
682
|
+
DAEMON_SCHEMA,
|
|
683
|
+
canonical,
|
|
684
|
+
os.getpid(),
|
|
685
|
+
listener.getsockname()[1],
|
|
686
|
+
secrets.token_urlsafe(32),
|
|
687
|
+
__version__,
|
|
688
|
+
project_file,
|
|
689
|
+
max_memory_bytes,
|
|
690
|
+
workers,
|
|
691
|
+
idle_timeout,
|
|
692
|
+
time.time(),
|
|
693
|
+
)
|
|
573
694
|
try:
|
|
574
695
|
service = _CacheService(metadata)
|
|
575
696
|
service.prewarm()
|
|
@@ -587,7 +708,15 @@ def run_cache_daemon(root: str | Path, *, project_file: str = "", max_memory_byt
|
|
|
587
708
|
_remove_metadata_if_owned(metadata)
|
|
588
709
|
|
|
589
710
|
|
|
590
|
-
def _start_cache_unlocked(
|
|
711
|
+
def _start_cache_unlocked(
|
|
712
|
+
root: str | Path,
|
|
713
|
+
*,
|
|
714
|
+
project_file: str | Path | None = None,
|
|
715
|
+
max_memory_bytes: int = DEFAULT_MAX_MEMORY_BYTES,
|
|
716
|
+
workers: int = 0,
|
|
717
|
+
idle_timeout: int = DEFAULT_IDLE_TIMEOUT,
|
|
718
|
+
startup_timeout: float = DEFAULT_STARTUP_TIMEOUT,
|
|
719
|
+
) -> CacheMetadata:
|
|
591
720
|
canonical = str(Path(root).resolve())
|
|
592
721
|
project = str((Path(canonical) / project_file).resolve()) if project_file and not Path(project_file).is_absolute() else (str(Path(project_file).resolve()) if project_file else "")
|
|
593
722
|
existing = _read_metadata(canonical)
|
|
@@ -596,12 +725,32 @@ def _start_cache_unlocked(root: str | Path, *, project_file: str | Path | None =
|
|
|
596
725
|
_client_exchange(existing, {"action": "status", "_startup_probe": True})
|
|
597
726
|
except CacheClientError as error:
|
|
598
727
|
raise CacheClientError("unavailable", "Live cache daemon is unavailable.") from error
|
|
599
|
-
if (existing.project_file, existing.max_memory_bytes, existing.idle_timeout) != (
|
|
728
|
+
if (existing.project_file, existing.max_memory_bytes, existing.workers, existing.idle_timeout) != (
|
|
729
|
+
project,
|
|
730
|
+
max_memory_bytes,
|
|
731
|
+
workers,
|
|
732
|
+
idle_timeout,
|
|
733
|
+
):
|
|
600
734
|
raise CacheClientError("configuration_conflict", "A live cache daemon has conflicting configuration.")
|
|
601
735
|
return existing
|
|
602
736
|
if existing:
|
|
603
737
|
_remove_metadata_if_owned(existing)
|
|
604
|
-
|
|
738
|
+
else:
|
|
739
|
+
_remove_incompatible_metadata_if_stale(canonical)
|
|
740
|
+
command = [
|
|
741
|
+
sys.executable,
|
|
742
|
+
"-m",
|
|
743
|
+
"delphi_lsp.agent_cache",
|
|
744
|
+
"serve",
|
|
745
|
+
"--root",
|
|
746
|
+
canonical,
|
|
747
|
+
"--max-memory",
|
|
748
|
+
str(max_memory_bytes),
|
|
749
|
+
"--workers",
|
|
750
|
+
str(workers),
|
|
751
|
+
"--idle-timeout",
|
|
752
|
+
str(idle_timeout),
|
|
753
|
+
]
|
|
605
754
|
if project:
|
|
606
755
|
command.extend(("--project-file", project))
|
|
607
756
|
options: dict[str, object] = {"stdin": subprocess.DEVNULL, "stdout": subprocess.DEVNULL}
|
|
@@ -612,7 +761,7 @@ def _start_cache_unlocked(root: str | Path, *, project_file: str | Path | None =
|
|
|
612
761
|
with tempfile.TemporaryFile() as diagnostics:
|
|
613
762
|
options["stderr"] = diagnostics
|
|
614
763
|
process = subprocess.Popen(command, **options)
|
|
615
|
-
deadline = time.monotonic() +
|
|
764
|
+
deadline = time.monotonic() + startup_timeout
|
|
616
765
|
startup_ready = False
|
|
617
766
|
try:
|
|
618
767
|
while time.monotonic() < deadline:
|
|
@@ -649,14 +798,34 @@ def _start_cache_unlocked(root: str | Path, *, project_file: str | Path | None =
|
|
|
649
798
|
process.wait()
|
|
650
799
|
|
|
651
800
|
|
|
652
|
-
def start_cache(
|
|
653
|
-
|
|
654
|
-
|
|
801
|
+
def start_cache(
|
|
802
|
+
root: str | Path,
|
|
803
|
+
*,
|
|
804
|
+
project_file: str | Path | None = None,
|
|
805
|
+
max_memory_bytes: int = DEFAULT_MAX_MEMORY_BYTES,
|
|
806
|
+
workers: int = 0,
|
|
807
|
+
idle_timeout: int = DEFAULT_IDLE_TIMEOUT,
|
|
808
|
+
startup_timeout: float = DEFAULT_STARTUP_TIMEOUT,
|
|
809
|
+
) -> CacheMetadata:
|
|
810
|
+
if type(workers) is not int or not 0 <= workers <= 32:
|
|
811
|
+
raise ValueError("workers must be auto or an integer from 1 through 32.")
|
|
812
|
+
if not math.isfinite(startup_timeout) or startup_timeout <= 0:
|
|
813
|
+
raise ValueError("startup_timeout must be greater than zero.")
|
|
814
|
+
with _start_lock(root, startup_timeout):
|
|
815
|
+
return _start_cache_unlocked(
|
|
816
|
+
root,
|
|
817
|
+
project_file=project_file,
|
|
818
|
+
max_memory_bytes=max_memory_bytes,
|
|
819
|
+
workers=workers,
|
|
820
|
+
idle_timeout=idle_timeout,
|
|
821
|
+
startup_timeout=startup_timeout,
|
|
822
|
+
)
|
|
655
823
|
|
|
656
824
|
|
|
657
825
|
def query_cache(root: str | Path, request: dict[str, object]) -> CacheClientResponse:
|
|
658
826
|
metadata = _read_metadata(root)
|
|
659
827
|
if metadata is None:
|
|
828
|
+
_remove_incompatible_metadata_if_stale(root)
|
|
660
829
|
raise CacheClientError("cache_not_running", "Cache daemon is not running.")
|
|
661
830
|
if not _pid_alive(metadata.pid):
|
|
662
831
|
_remove_metadata_if_owned(metadata)
|
|
@@ -671,6 +840,7 @@ def cache_status(root: str | Path) -> dict[str, object]:
|
|
|
671
840
|
def stop_cache(root: str | Path) -> None:
|
|
672
841
|
metadata = _read_metadata(root)
|
|
673
842
|
if metadata is None:
|
|
843
|
+
_remove_incompatible_metadata_if_stale(root)
|
|
674
844
|
return
|
|
675
845
|
if not _pid_alive(metadata.pid):
|
|
676
846
|
_remove_metadata_if_owned(metadata)
|
|
@@ -681,11 +851,13 @@ def stop_cache(root: str | Path) -> None:
|
|
|
681
851
|
pass
|
|
682
852
|
deadline = time.monotonic() + 3
|
|
683
853
|
stopped = False
|
|
684
|
-
while
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
854
|
+
while time.monotonic() < deadline:
|
|
855
|
+
if _reap_child_if_exited(metadata.pid):
|
|
856
|
+
stopped = True
|
|
857
|
+
break
|
|
858
|
+
if not _pid_alive(metadata.pid):
|
|
859
|
+
stopped = True
|
|
860
|
+
break
|
|
689
861
|
time.sleep(0.05)
|
|
690
862
|
if not stopped and _pid_alive(metadata.pid):
|
|
691
863
|
raise CacheClientError("stop_failed", "Cache daemon did not stop.")
|
|
@@ -699,10 +871,17 @@ def main(argv: list[str] | None = None) -> int:
|
|
|
699
871
|
serve.add_argument("--root", required=True)
|
|
700
872
|
serve.add_argument("--project-file", default="")
|
|
701
873
|
serve.add_argument("--max-memory", type=int, default=DEFAULT_MAX_MEMORY_BYTES)
|
|
874
|
+
serve.add_argument("--workers", type=int, default=0)
|
|
702
875
|
serve.add_argument("--idle-timeout", type=int, default=DEFAULT_IDLE_TIMEOUT)
|
|
703
876
|
args = parser.parse_args(argv)
|
|
704
877
|
if args.command == "serve":
|
|
705
|
-
run_cache_daemon(
|
|
878
|
+
run_cache_daemon(
|
|
879
|
+
args.root,
|
|
880
|
+
project_file=args.project_file,
|
|
881
|
+
max_memory_bytes=args.max_memory,
|
|
882
|
+
workers=args.workers,
|
|
883
|
+
idle_timeout=args.idle_timeout,
|
|
884
|
+
)
|
|
706
885
|
return 0
|
|
707
886
|
|
|
708
887
|
|
delphi_lsp/agent_cli.py
CHANGED
|
@@ -2,6 +2,7 @@ from __future__ import annotations
|
|
|
2
2
|
|
|
3
3
|
import argparse
|
|
4
4
|
import json
|
|
5
|
+
import math
|
|
5
6
|
import os
|
|
6
7
|
import sys
|
|
7
8
|
from pathlib import Path
|
|
@@ -12,6 +13,7 @@ from .agent_cache import (
|
|
|
12
13
|
CacheClientError,
|
|
13
14
|
DEFAULT_IDLE_TIMEOUT,
|
|
14
15
|
DEFAULT_MAX_MEMORY_BYTES,
|
|
16
|
+
DEFAULT_STARTUP_TIMEOUT,
|
|
15
17
|
parse_memory_size,
|
|
16
18
|
query_cache,
|
|
17
19
|
run_cache_daemon,
|
|
@@ -21,6 +23,7 @@ from .agent_cache import (
|
|
|
21
23
|
from .agent_layers import build_codebase_index, layer_payload, render_layer
|
|
22
24
|
from .agent_protocol import AgentProtocolError, SUPPORTED_ACTIONS, SUPPORTED_DETAILS, SUPPORTED_RELATIONS
|
|
23
25
|
from .agent_templates import install_opencode_support, install_skill
|
|
26
|
+
from .parallel_outline import parse_worker_setting
|
|
24
27
|
|
|
25
28
|
|
|
26
29
|
_MAX_WORKER_RECORD_BYTES = 1024 * 1024
|
|
@@ -60,12 +63,14 @@ def build_parser() -> argparse.ArgumentParser:
|
|
|
60
63
|
view.add_argument("--query", default="")
|
|
61
64
|
view.add_argument("--format", default="markdown", choices=["markdown", "json"])
|
|
62
65
|
view.add_argument("--deep-projects", action="store_true", help="Deep-parse project dependencies for the projects layer.")
|
|
66
|
+
view.add_argument("--workers", type=parse_worker_setting, default=0)
|
|
63
67
|
view.set_defaults(func=_view)
|
|
64
68
|
|
|
65
69
|
index = subcommands.add_parser("index", help="Materialize a JSON codebase index.")
|
|
66
70
|
index.add_argument("--root", type=Path, default=Path("."))
|
|
67
71
|
index.add_argument("--project-file", type=Path)
|
|
68
72
|
index.add_argument("--out", type=Path, default=Path(".delphi-lsp") / "agent-index" / "index.json")
|
|
73
|
+
index.add_argument("--workers", type=parse_worker_setting, default=0)
|
|
69
74
|
index.set_defaults(func=_index)
|
|
70
75
|
|
|
71
76
|
skill = subcommands.add_parser("skill", help="Install agent skill templates.")
|
|
@@ -95,6 +100,7 @@ def build_parser() -> argparse.ArgumentParser:
|
|
|
95
100
|
worker = subcommands.add_parser("worker", help="Serve Protocol v2 NDJSON requests.")
|
|
96
101
|
worker.add_argument("--root", type=Path, required=True)
|
|
97
102
|
worker.add_argument("--project-file", type=Path)
|
|
103
|
+
worker.add_argument("--workers", type=parse_worker_setting, default=0)
|
|
98
104
|
worker.set_defaults(func=_worker)
|
|
99
105
|
|
|
100
106
|
cache = subcommands.add_parser("cache", help="Manage the shared Protocol v2 cache daemon.")
|
|
@@ -113,6 +119,7 @@ def build_parser() -> argparse.ArgumentParser:
|
|
|
113
119
|
cache_serve.add_argument("--root", type=Path, required=True)
|
|
114
120
|
cache_serve.add_argument("--project-file", type=Path)
|
|
115
121
|
cache_serve.add_argument("--max-memory", type=parse_memory_size, required=True)
|
|
122
|
+
cache_serve.add_argument("--workers", type=parse_worker_setting, required=True)
|
|
116
123
|
cache_serve.add_argument("--idle-timeout", type=int, required=True)
|
|
117
124
|
cache_serve.set_defaults(func=_cache_serve)
|
|
118
125
|
|
|
@@ -135,7 +142,16 @@ def _add_cache_start_arguments(parser: argparse.ArgumentParser) -> None:
|
|
|
135
142
|
parser.add_argument("--root", type=Path, default=Path("."))
|
|
136
143
|
parser.add_argument("--project-file", type=Path)
|
|
137
144
|
parser.add_argument("--max-memory", type=parse_memory_size, default=DEFAULT_MAX_MEMORY_BYTES)
|
|
145
|
+
parser.add_argument("--workers", type=parse_worker_setting, default=0)
|
|
138
146
|
parser.add_argument("--idle-timeout", type=int, default=DEFAULT_IDLE_TIMEOUT)
|
|
147
|
+
parser.add_argument("--startup-timeout", type=_positive_float, default=DEFAULT_STARTUP_TIMEOUT)
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
def _positive_float(value: str) -> float:
|
|
151
|
+
parsed = float(value)
|
|
152
|
+
if not math.isfinite(parsed) or parsed <= 0:
|
|
153
|
+
raise argparse.ArgumentTypeError("value must be greater than zero")
|
|
154
|
+
return parsed
|
|
139
155
|
|
|
140
156
|
|
|
141
157
|
def main(argv: list[str] | None = None) -> int:
|
|
@@ -173,12 +189,22 @@ def _discard_broken_stdout() -> None:
|
|
|
173
189
|
|
|
174
190
|
|
|
175
191
|
def _view(args: argparse.Namespace) -> None:
|
|
176
|
-
index = build_codebase_index(
|
|
192
|
+
index = build_codebase_index(
|
|
193
|
+
args.root,
|
|
194
|
+
project_file=args.project_file,
|
|
195
|
+
index_projects=args.deep_projects,
|
|
196
|
+
workers=args.workers,
|
|
197
|
+
)
|
|
177
198
|
sys.stdout.write(render_layer(index, args.layer, query=args.query, output_format=args.format))
|
|
178
199
|
|
|
179
200
|
|
|
180
201
|
def _index(args: argparse.Namespace) -> None:
|
|
181
|
-
index = build_codebase_index(
|
|
202
|
+
index = build_codebase_index(
|
|
203
|
+
args.root,
|
|
204
|
+
project_file=args.project_file,
|
|
205
|
+
index_projects=True,
|
|
206
|
+
workers=args.workers,
|
|
207
|
+
)
|
|
182
208
|
payload = {
|
|
183
209
|
"overview": layer_payload(index, "overview"),
|
|
184
210
|
"projects": layer_payload(index, "projects"),
|
|
@@ -207,7 +233,7 @@ def _opencode_install(args: argparse.Namespace) -> None:
|
|
|
207
233
|
|
|
208
234
|
|
|
209
235
|
def _worker(args: argparse.Namespace) -> None:
|
|
210
|
-
context = AgentContext.open(args.root, args.project_file)
|
|
236
|
+
context = AgentContext.open(args.root, args.project_file, workers=args.workers)
|
|
211
237
|
try:
|
|
212
238
|
_serve_worker(context, sys.stdin.buffer, sys.stdout.buffer, sys.stderr)
|
|
213
239
|
finally:
|
|
@@ -235,7 +261,14 @@ def _cache_error(error: CacheClientError) -> int:
|
|
|
235
261
|
|
|
236
262
|
def _cache_start(args: argparse.Namespace) -> int:
|
|
237
263
|
try:
|
|
238
|
-
start_cache(
|
|
264
|
+
start_cache(
|
|
265
|
+
args.root,
|
|
266
|
+
project_file=args.project_file,
|
|
267
|
+
max_memory_bytes=args.max_memory,
|
|
268
|
+
workers=args.workers,
|
|
269
|
+
idle_timeout=args.idle_timeout,
|
|
270
|
+
startup_timeout=args.startup_timeout,
|
|
271
|
+
)
|
|
239
272
|
response = query_cache(args.root, {"action": "status"})
|
|
240
273
|
except CacheClientError as error:
|
|
241
274
|
return _cache_error(error)
|
|
@@ -283,7 +316,13 @@ def _cache_stop(args: argparse.Namespace) -> int:
|
|
|
283
316
|
|
|
284
317
|
|
|
285
318
|
def _cache_serve(args: argparse.Namespace) -> int:
|
|
286
|
-
run_cache_daemon(
|
|
319
|
+
run_cache_daemon(
|
|
320
|
+
args.root,
|
|
321
|
+
project_file=str(args.project_file or ""),
|
|
322
|
+
max_memory_bytes=args.max_memory,
|
|
323
|
+
workers=args.workers,
|
|
324
|
+
idle_timeout=args.idle_timeout,
|
|
325
|
+
)
|
|
287
326
|
return 0
|
|
288
327
|
|
|
289
328
|
|
delphi_lsp/agent_context.py
CHANGED
|
@@ -22,12 +22,12 @@ from .agent_metrics import build_workspace_metrics, project_metric_item, unit_me
|
|
|
22
22
|
from .agent_relations import ProjectRelationIndex, RelationTarget
|
|
23
23
|
from .agent_workspace import AgentUnit, AgentWorkspace, unit_display_path, unit_source_path, unit_target_id
|
|
24
24
|
from .consts import AttributeName, SyntaxNodeType
|
|
25
|
-
from .lsp_server import
|
|
25
|
+
from .lsp_server import multiline_string_block_end
|
|
26
26
|
from .nodes import CompoundSyntaxNode, SyntaxNode
|
|
27
27
|
from .parser import DelphiParser
|
|
28
28
|
from .semantic import Scope, ScopeKind, Symbol, SymbolKind
|
|
29
|
-
from .source_reader import read_source_text
|
|
30
29
|
from .metrics import ProjectMetrics
|
|
30
|
+
from .parallel_outline import OutlineResult, OutlineTask, ParallelBuildStats, run_outline_tasks
|
|
31
31
|
|
|
32
32
|
|
|
33
33
|
_ROUTINE_KINDS = frozenset(
|
|
@@ -239,8 +239,17 @@ class _Registry:
|
|
|
239
239
|
|
|
240
240
|
|
|
241
241
|
class AgentContext:
|
|
242
|
-
def __init__(
|
|
242
|
+
def __init__(
|
|
243
|
+
self,
|
|
244
|
+
workspace: AgentWorkspace,
|
|
245
|
+
*,
|
|
246
|
+
workers: int = 0,
|
|
247
|
+
worker_memory_budget_bytes: int | None = None,
|
|
248
|
+
) -> None:
|
|
243
249
|
self._workspace = workspace
|
|
250
|
+
self._workers = workers
|
|
251
|
+
self._worker_memory_budget_bytes = worker_memory_budget_bytes
|
|
252
|
+
self._parallel_stats = ParallelBuildStats(0, 0, 0, 0.0, 0)
|
|
244
253
|
project_id = workspace.active_project_id
|
|
245
254
|
self._focus = Focus(project_id=project_id) if project_id else Focus()
|
|
246
255
|
self._last_revision = workspace.workspace_revision
|
|
@@ -254,8 +263,15 @@ class AgentContext:
|
|
|
254
263
|
cls,
|
|
255
264
|
root: str | Path,
|
|
256
265
|
project_file: str | Path | None = None,
|
|
266
|
+
*,
|
|
267
|
+
workers: int = 0,
|
|
268
|
+
worker_memory_budget_bytes: int | None = None,
|
|
257
269
|
) -> AgentContext:
|
|
258
|
-
return cls(
|
|
270
|
+
return cls(
|
|
271
|
+
AgentWorkspace.open(root, project_file=project_file),
|
|
272
|
+
workers=workers,
|
|
273
|
+
worker_memory_budget_bytes=worker_memory_budget_bytes,
|
|
274
|
+
)
|
|
259
275
|
|
|
260
276
|
@property
|
|
261
277
|
def workspace(self) -> AgentWorkspace:
|
|
@@ -265,6 +281,10 @@ class AgentContext:
|
|
|
265
281
|
def navigation_cache_is_warm(self) -> bool:
|
|
266
282
|
return self._registry is not None
|
|
267
283
|
|
|
284
|
+
@property
|
|
285
|
+
def parallel_stats(self) -> ParallelBuildStats:
|
|
286
|
+
return self._parallel_stats
|
|
287
|
+
|
|
268
288
|
def cache_roots(self) -> tuple[object, ...]:
|
|
269
289
|
return (
|
|
270
290
|
*self._workspace.cache_roots(),
|
|
@@ -507,7 +527,13 @@ class AgentContext:
|
|
|
507
527
|
and self._registry.revision == revision
|
|
508
528
|
):
|
|
509
529
|
return self._registry
|
|
510
|
-
self._registry = _build_registry(
|
|
530
|
+
self._registry, self._parallel_stats = _build_registry(
|
|
531
|
+
self._workspace,
|
|
532
|
+
project_id,
|
|
533
|
+
revision,
|
|
534
|
+
workers=self._workers,
|
|
535
|
+
worker_memory_budget_bytes=self._worker_memory_budget_bytes,
|
|
536
|
+
)
|
|
511
537
|
if self._focus.target_id:
|
|
512
538
|
focused_entry = self._registry.by_target.get(self._focus.target_id)
|
|
513
539
|
if focused_entry is None:
|
|
@@ -696,38 +722,54 @@ def _validated_request(request: AgentRequest | Mapping[str, object]) -> AgentReq
|
|
|
696
722
|
return AgentRequest.from_mapping(request)
|
|
697
723
|
|
|
698
724
|
|
|
699
|
-
def _build_registry(
|
|
725
|
+
def _build_registry(
|
|
726
|
+
workspace: AgentWorkspace,
|
|
727
|
+
project_id: str,
|
|
728
|
+
revision: str,
|
|
729
|
+
*,
|
|
730
|
+
workers: int = 0,
|
|
731
|
+
worker_memory_budget_bytes: int | None = None,
|
|
732
|
+
) -> tuple[_Registry, ParallelBuildStats]:
|
|
700
733
|
raw_symbols: list[_RawSymbol] = []
|
|
701
734
|
sources: dict[Path, _SourceDocument] = {}
|
|
702
|
-
|
|
735
|
+
units = tuple(workspace.units)
|
|
736
|
+
|
|
737
|
+
def consume_result(result: OutlineResult) -> None:
|
|
738
|
+
unit = units[result.ordinal]
|
|
703
739
|
source_path = unit_source_path(workspace.root, unit)
|
|
704
740
|
display_path = unit_display_path(workspace.root, unit)
|
|
705
|
-
|
|
706
|
-
text = read_source_text(source_path)
|
|
707
|
-
except OSError as exc:
|
|
741
|
+
if result.read_error or result.model is None:
|
|
708
742
|
raise AgentProtocolError(
|
|
709
743
|
"source_unavailable",
|
|
710
|
-
f"Could not read selected source {
|
|
711
|
-
)
|
|
744
|
+
f"Could not read selected source {display_path}.",
|
|
745
|
+
)
|
|
712
746
|
document = _SourceDocument(
|
|
713
747
|
source_path,
|
|
714
748
|
display_path,
|
|
715
|
-
text,
|
|
749
|
+
result.text,
|
|
716
750
|
defines=workspace.defines,
|
|
717
751
|
include_paths=workspace.include_paths,
|
|
718
752
|
)
|
|
719
753
|
sources[source_path] = document
|
|
720
|
-
|
|
721
|
-
model = build_outline_semantic_model(
|
|
722
|
-
text,
|
|
723
|
-
str(source_path),
|
|
724
|
-
defines=workspace.defines,
|
|
725
|
-
)
|
|
726
|
-
else:
|
|
727
|
-
model = build_outline_semantic_model(text, str(source_path))
|
|
728
|
-
unit_symbols = _collect_raw_symbols(model.unit_scope, unit, source_path, document)
|
|
754
|
+
unit_symbols = _collect_raw_symbols(result.model.unit_scope, unit, source_path, document)
|
|
729
755
|
raw_symbols.extend(_exclude_routine_locals(unit_symbols, document))
|
|
730
756
|
|
|
757
|
+
outline_batch = run_outline_tasks(
|
|
758
|
+
(
|
|
759
|
+
OutlineTask(
|
|
760
|
+
ordinal,
|
|
761
|
+
str(unit_source_path(workspace.root, unit)),
|
|
762
|
+
workspace.defines,
|
|
763
|
+
True,
|
|
764
|
+
)
|
|
765
|
+
for ordinal, unit in enumerate(units)
|
|
766
|
+
),
|
|
767
|
+
configured_workers=workers,
|
|
768
|
+
memory_budget_bytes=worker_memory_budget_bytes,
|
|
769
|
+
on_complete=consume_result,
|
|
770
|
+
retain_results=False,
|
|
771
|
+
)
|
|
772
|
+
|
|
731
773
|
ordered = sorted(raw_symbols, key=_raw_sort_key)
|
|
732
774
|
overload_groups: dict[tuple[str, str, str, str], list[_RawSymbol]] = {}
|
|
733
775
|
for raw in raw_symbols:
|
|
@@ -817,13 +859,16 @@ def _build_registry(workspace: AgentWorkspace, project_id: str, revision: str) -
|
|
|
817
859
|
)
|
|
818
860
|
|
|
819
861
|
entries_tuple = tuple(sorted(with_parents, key=_entry_sort_key))
|
|
820
|
-
return
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
862
|
+
return (
|
|
863
|
+
_Registry(
|
|
864
|
+
project_id=project_id,
|
|
865
|
+
revision=revision,
|
|
866
|
+
entries=entries_tuple,
|
|
867
|
+
by_target={entry.target_id: entry for entry in entries_tuple},
|
|
868
|
+
sources=sources,
|
|
869
|
+
ranked_queries={},
|
|
870
|
+
),
|
|
871
|
+
outline_batch.stats,
|
|
827
872
|
)
|
|
828
873
|
|
|
829
874
|
|
delphi_lsp/agent_layers.py
CHANGED
|
@@ -5,8 +5,8 @@ from pathlib import Path
|
|
|
5
5
|
from typing import Any, Iterable
|
|
6
6
|
import json
|
|
7
7
|
|
|
8
|
-
from .lsp_server import build_outline_semantic_model
|
|
9
8
|
from .metrics import analyze_project
|
|
9
|
+
from .parallel_outline import OutlineResult, OutlineTask, ParallelBuildStats, run_outline_tasks
|
|
10
10
|
from .project_discovery import DelphiProjectDiscovery, discover_delphi_project
|
|
11
11
|
from .project_indexer import ProjectIndexResult, ProjectIndexer
|
|
12
12
|
from .progress import ProgressCallback, ProgressEvent
|
|
@@ -22,6 +22,7 @@ class CodebaseIndex:
|
|
|
22
22
|
models: dict[str, SemanticModel]
|
|
23
23
|
symbol_index: SymbolIndex
|
|
24
24
|
project_results: dict[str, ProjectIndexResult]
|
|
25
|
+
parallel_stats: ParallelBuildStats = ParallelBuildStats(0, 0, 0, 0.0, 0)
|
|
25
26
|
|
|
26
27
|
|
|
27
28
|
def build_codebase_index(
|
|
@@ -30,40 +31,49 @@ def build_codebase_index(
|
|
|
30
31
|
project_file: str | Path | None = None,
|
|
31
32
|
index_projects: bool = False,
|
|
32
33
|
on_progress: ProgressCallback | None = None,
|
|
34
|
+
workers: int = 0,
|
|
33
35
|
) -> CodebaseIndex:
|
|
34
36
|
progress = _MonotonicProgress(on_progress)
|
|
35
37
|
discovery = discover_delphi_project(root, project_file=project_file, on_progress=progress)
|
|
36
38
|
models: dict[str, SemanticModel] = {}
|
|
37
39
|
lines_processed = 0
|
|
38
40
|
symbols_discovered = 0
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
lines_processed += text.count("\n") + (0 if not text or text.endswith("\n") else 1)
|
|
54
|
-
symbols_discovered += sum(len(items) for items in model.index.name_index.values())
|
|
41
|
+
completed_outlines = 0
|
|
42
|
+
outline_tasks = tuple(
|
|
43
|
+
OutlineTask(ordinal, source, tuple(discovery.defines), False)
|
|
44
|
+
for ordinal, source in enumerate(discovery.source_files)
|
|
45
|
+
if Path(source).suffix.casefold() in {".pas", ".dpr", ".dpk", ".inc"}
|
|
46
|
+
)
|
|
47
|
+
outline_task_count = len(outline_tasks)
|
|
48
|
+
|
|
49
|
+
def on_outline_complete(result: OutlineResult) -> None:
|
|
50
|
+
nonlocal completed_outlines, lines_processed, symbols_discovered
|
|
51
|
+
completed_outlines += 1
|
|
52
|
+
if not result.read_error:
|
|
53
|
+
lines_processed += result.lines_processed
|
|
54
|
+
symbols_discovered += result.symbols_discovered
|
|
55
55
|
_emit_progress(
|
|
56
56
|
progress,
|
|
57
57
|
"outline",
|
|
58
|
-
|
|
59
|
-
len(discovery.source_files),
|
|
60
|
-
completed,
|
|
58
|
+
result.source_path,
|
|
61
59
|
len(discovery.source_files),
|
|
62
|
-
|
|
60
|
+
completed_outlines,
|
|
61
|
+
outline_task_count,
|
|
62
|
+
"source unreadable" if result.read_error else "source outlined",
|
|
63
63
|
lines_processed=lines_processed,
|
|
64
64
|
symbols_discovered=symbols_discovered,
|
|
65
65
|
)
|
|
66
66
|
|
|
67
|
+
outline_batch = run_outline_tasks(
|
|
68
|
+
outline_tasks,
|
|
69
|
+
configured_workers=workers,
|
|
70
|
+
on_complete=on_outline_complete,
|
|
71
|
+
)
|
|
72
|
+
for result in outline_batch.results:
|
|
73
|
+
if result.read_error or result.model is None:
|
|
74
|
+
continue
|
|
75
|
+
models[result.source_path] = result.model
|
|
76
|
+
|
|
67
77
|
symbol_index = SymbolIndex()
|
|
68
78
|
for model in models.values():
|
|
69
79
|
symbol_index.register_unit(model.unit_scope.name, model.unit_scope)
|
|
@@ -99,6 +109,7 @@ def build_codebase_index(
|
|
|
99
109
|
models=models,
|
|
100
110
|
symbol_index=symbol_index,
|
|
101
111
|
project_results=project_results,
|
|
112
|
+
parallel_stats=outline_batch.stats,
|
|
102
113
|
)
|
|
103
114
|
_emit_progress(
|
|
104
115
|
progress,
|
|
@@ -0,0 +1,278 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from concurrent.futures import ProcessPoolExecutor, as_completed
|
|
4
|
+
from concurrent.futures.process import BrokenProcessPool
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
import multiprocessing
|
|
7
|
+
import os
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
from time import perf_counter
|
|
10
|
+
from typing import Callable, Iterable
|
|
11
|
+
|
|
12
|
+
from .lsp_server import build_outline_semantic_model
|
|
13
|
+
from .semantic_builder import SemanticModel
|
|
14
|
+
from .source_reader import read_source_text
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
_MEBIBYTE = 1024 * 1024
|
|
18
|
+
_WORKER_MEMORY_BYTES = 128 * _MEBIBYTE
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class ParallelOutlineError(RuntimeError):
|
|
22
|
+
"""Raised when a parallel outline operation cannot complete."""
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
@dataclass(frozen=True)
|
|
26
|
+
class OutlineTask:
|
|
27
|
+
ordinal: int
|
|
28
|
+
source_path: str
|
|
29
|
+
defines: tuple[str, ...]
|
|
30
|
+
return_text: bool
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
@dataclass(frozen=True)
|
|
34
|
+
class OutlineResult:
|
|
35
|
+
ordinal: int
|
|
36
|
+
source_path: str
|
|
37
|
+
text: str
|
|
38
|
+
model: SemanticModel | None
|
|
39
|
+
lines_processed: int
|
|
40
|
+
symbols_discovered: int
|
|
41
|
+
read_error: str = ""
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
@dataclass(frozen=True)
|
|
45
|
+
class ParallelBuildStats:
|
|
46
|
+
configured_workers: int
|
|
47
|
+
effective_workers: int
|
|
48
|
+
files_completed: int
|
|
49
|
+
elapsed_seconds: float
|
|
50
|
+
fallbacks: int
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
@dataclass(frozen=True)
|
|
54
|
+
class OutlineBatch:
|
|
55
|
+
results: tuple[OutlineResult, ...]
|
|
56
|
+
stats: ParallelBuildStats
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def parse_worker_setting(value: str) -> int:
|
|
60
|
+
if value == "auto":
|
|
61
|
+
return 0
|
|
62
|
+
try:
|
|
63
|
+
workers = int(value)
|
|
64
|
+
except ValueError as error:
|
|
65
|
+
raise ValueError("workers must be 'auto' or an integer from 1 through 32") from error
|
|
66
|
+
if not 1 <= workers <= 32:
|
|
67
|
+
raise ValueError("workers must be 'auto' or an integer from 1 through 32")
|
|
68
|
+
return workers
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def resolve_worker_count(
|
|
72
|
+
configured_workers: int,
|
|
73
|
+
*,
|
|
74
|
+
task_count: int,
|
|
75
|
+
cpu_count: int | None = None,
|
|
76
|
+
memory_budget_bytes: int | None = None,
|
|
77
|
+
) -> int:
|
|
78
|
+
if not 0 <= configured_workers <= 32:
|
|
79
|
+
raise ValueError("workers must be 'auto' or an integer from 1 through 32")
|
|
80
|
+
if task_count <= 0:
|
|
81
|
+
return 0
|
|
82
|
+
if configured_workers:
|
|
83
|
+
return min(configured_workers, task_count)
|
|
84
|
+
|
|
85
|
+
detected_cpus = os.cpu_count() if cpu_count is None else cpu_count
|
|
86
|
+
candidates = [task_count, 4, max(1, (detected_cpus or 1) - 1)]
|
|
87
|
+
if memory_budget_bytes is not None:
|
|
88
|
+
candidates.append(max(1, memory_budget_bytes // _WORKER_MEMORY_BYTES))
|
|
89
|
+
return min(candidates)
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def _parse_outline_task(task: OutlineTask) -> OutlineResult:
|
|
93
|
+
try:
|
|
94
|
+
text = read_source_text(Path(task.source_path))
|
|
95
|
+
except (OSError, UnicodeError) as error:
|
|
96
|
+
return OutlineResult(
|
|
97
|
+
ordinal=task.ordinal,
|
|
98
|
+
source_path=task.source_path,
|
|
99
|
+
text="",
|
|
100
|
+
model=None,
|
|
101
|
+
lines_processed=0,
|
|
102
|
+
symbols_discovered=0,
|
|
103
|
+
read_error=str(error),
|
|
104
|
+
)
|
|
105
|
+
|
|
106
|
+
try:
|
|
107
|
+
model = build_outline_semantic_model(text, task.source_path, defines=task.defines)
|
|
108
|
+
except Exception as error:
|
|
109
|
+
raise ParallelOutlineError(f"failed to parse {task.source_path}: {error}") from error
|
|
110
|
+
return OutlineResult(
|
|
111
|
+
ordinal=task.ordinal,
|
|
112
|
+
source_path=task.source_path,
|
|
113
|
+
text=text if task.return_text else "",
|
|
114
|
+
model=model,
|
|
115
|
+
lines_processed=len(text.splitlines()),
|
|
116
|
+
symbols_discovered=sum(len(items) for items in model.index.name_index.values()),
|
|
117
|
+
)
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def run_outline_tasks(
|
|
121
|
+
tasks: Iterable[OutlineTask],
|
|
122
|
+
*,
|
|
123
|
+
configured_workers: int,
|
|
124
|
+
memory_budget_bytes: int | None = None,
|
|
125
|
+
cpu_count: int | None = None,
|
|
126
|
+
on_complete: Callable[[OutlineResult], None] | None = None,
|
|
127
|
+
retain_results: bool = True,
|
|
128
|
+
) -> OutlineBatch:
|
|
129
|
+
task_list = tuple(tasks)
|
|
130
|
+
started = perf_counter()
|
|
131
|
+
effective_workers = resolve_worker_count(
|
|
132
|
+
configured_workers,
|
|
133
|
+
task_count=len(task_list),
|
|
134
|
+
cpu_count=cpu_count,
|
|
135
|
+
memory_budget_bytes=memory_budget_bytes,
|
|
136
|
+
)
|
|
137
|
+
if effective_workers <= 1:
|
|
138
|
+
results, completed = _run_serial(task_list, on_complete, retain_results)
|
|
139
|
+
return _batch(
|
|
140
|
+
configured_workers,
|
|
141
|
+
effective_workers,
|
|
142
|
+
results,
|
|
143
|
+
started,
|
|
144
|
+
files_completed=completed,
|
|
145
|
+
fallbacks=0,
|
|
146
|
+
)
|
|
147
|
+
|
|
148
|
+
accepted: list[OutlineResult] = []
|
|
149
|
+
completed = 0
|
|
150
|
+
futures: dict[object, OutlineTask] = {}
|
|
151
|
+
executor: ProcessPoolExecutor | None = None
|
|
152
|
+
try:
|
|
153
|
+
context = multiprocessing.get_context("spawn")
|
|
154
|
+
executor = ProcessPoolExecutor(max_workers=effective_workers, mp_context=context)
|
|
155
|
+
task_iterator = iter(task_list)
|
|
156
|
+
for task in task_iterator:
|
|
157
|
+
futures[executor.submit(_parse_outline_task, task)] = task
|
|
158
|
+
if len(futures) == effective_workers:
|
|
159
|
+
break
|
|
160
|
+
while futures:
|
|
161
|
+
future = next(iter(as_completed(tuple(futures))))
|
|
162
|
+
task = futures.pop(future)
|
|
163
|
+
try:
|
|
164
|
+
result = future.result()
|
|
165
|
+
except ParallelOutlineError:
|
|
166
|
+
raise
|
|
167
|
+
except Exception as error:
|
|
168
|
+
wrapped = ParallelOutlineError(
|
|
169
|
+
f"parallel outline execution failed for {task.source_path}: {error}"
|
|
170
|
+
)
|
|
171
|
+
if configured_workers == 0 and completed == 0:
|
|
172
|
+
_cancel_futures(futures)
|
|
173
|
+
_shutdown_failed_executor(executor)
|
|
174
|
+
results, serial_completed = _run_serial(task_list, on_complete, retain_results)
|
|
175
|
+
return _batch(
|
|
176
|
+
configured_workers,
|
|
177
|
+
1,
|
|
178
|
+
results,
|
|
179
|
+
started,
|
|
180
|
+
files_completed=serial_completed,
|
|
181
|
+
fallbacks=1,
|
|
182
|
+
)
|
|
183
|
+
raise wrapped from error
|
|
184
|
+
completed += 1
|
|
185
|
+
if on_complete is not None:
|
|
186
|
+
on_complete(result)
|
|
187
|
+
if retain_results:
|
|
188
|
+
accepted.append(result)
|
|
189
|
+
try:
|
|
190
|
+
next_task = next(task_iterator)
|
|
191
|
+
except StopIteration:
|
|
192
|
+
continue
|
|
193
|
+
futures[executor.submit(_parse_outline_task, next_task)] = next_task
|
|
194
|
+
except ParallelOutlineError:
|
|
195
|
+
_cancel_futures(futures)
|
|
196
|
+
_shutdown_failed_executor(executor)
|
|
197
|
+
raise
|
|
198
|
+
except (OSError, BrokenProcessPool) as error:
|
|
199
|
+
_cancel_futures(futures)
|
|
200
|
+
_shutdown_failed_executor(executor)
|
|
201
|
+
if configured_workers == 0 and completed == 0:
|
|
202
|
+
results, serial_completed = _run_serial(task_list, on_complete, retain_results)
|
|
203
|
+
return _batch(
|
|
204
|
+
configured_workers,
|
|
205
|
+
1,
|
|
206
|
+
results,
|
|
207
|
+
started,
|
|
208
|
+
files_completed=serial_completed,
|
|
209
|
+
fallbacks=1,
|
|
210
|
+
)
|
|
211
|
+
raise ParallelOutlineError(f"parallel outline execution failed: {error}") from error
|
|
212
|
+
except BaseException:
|
|
213
|
+
_cancel_futures(futures)
|
|
214
|
+
_shutdown_failed_executor(executor)
|
|
215
|
+
raise
|
|
216
|
+
|
|
217
|
+
assert executor is not None
|
|
218
|
+
executor.shutdown(wait=True, cancel_futures=False)
|
|
219
|
+
|
|
220
|
+
return _batch(
|
|
221
|
+
configured_workers,
|
|
222
|
+
effective_workers,
|
|
223
|
+
accepted,
|
|
224
|
+
started,
|
|
225
|
+
files_completed=completed,
|
|
226
|
+
fallbacks=0,
|
|
227
|
+
)
|
|
228
|
+
|
|
229
|
+
|
|
230
|
+
def _run_serial(
|
|
231
|
+
tasks: tuple[OutlineTask, ...],
|
|
232
|
+
on_complete: Callable[[OutlineResult], None] | None,
|
|
233
|
+
retain_results: bool,
|
|
234
|
+
) -> tuple[list[OutlineResult], int]:
|
|
235
|
+
results = []
|
|
236
|
+
for task in tasks:
|
|
237
|
+
result = _parse_outline_task(task)
|
|
238
|
+
if on_complete is not None:
|
|
239
|
+
on_complete(result)
|
|
240
|
+
if retain_results:
|
|
241
|
+
results.append(result)
|
|
242
|
+
return results, len(tasks)
|
|
243
|
+
|
|
244
|
+
|
|
245
|
+
def _cancel_futures(futures: dict[object, OutlineTask]) -> None:
|
|
246
|
+
for future in futures:
|
|
247
|
+
future.cancel()
|
|
248
|
+
|
|
249
|
+
|
|
250
|
+
def _shutdown_failed_executor(executor: ProcessPoolExecutor | None) -> None:
|
|
251
|
+
if executor is None:
|
|
252
|
+
return
|
|
253
|
+
try:
|
|
254
|
+
executor.shutdown(wait=False, cancel_futures=True)
|
|
255
|
+
except BaseException:
|
|
256
|
+
pass
|
|
257
|
+
|
|
258
|
+
|
|
259
|
+
def _batch(
|
|
260
|
+
configured_workers: int,
|
|
261
|
+
effective_workers: int,
|
|
262
|
+
results: list[OutlineResult],
|
|
263
|
+
started: float,
|
|
264
|
+
*,
|
|
265
|
+
files_completed: int,
|
|
266
|
+
fallbacks: int,
|
|
267
|
+
) -> OutlineBatch:
|
|
268
|
+
ordered_results = tuple(sorted(results, key=lambda result: result.ordinal))
|
|
269
|
+
return OutlineBatch(
|
|
270
|
+
results=ordered_results,
|
|
271
|
+
stats=ParallelBuildStats(
|
|
272
|
+
configured_workers=configured_workers,
|
|
273
|
+
effective_workers=effective_workers,
|
|
274
|
+
files_completed=files_completed,
|
|
275
|
+
elapsed_seconds=perf_counter() - started,
|
|
276
|
+
fallbacks=fallbacks,
|
|
277
|
+
),
|
|
278
|
+
)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: python-delphi-lsp
|
|
3
|
-
Version: 2.0
|
|
3
|
+
Version: 2.1.0
|
|
4
4
|
Summary: Python Delphi/Object Pascal parser, semantic indexer, and language server.
|
|
5
5
|
Author: Dark Light
|
|
6
6
|
License-Expression: MPL-2.0
|
|
@@ -41,7 +41,7 @@ Dynamic: license-file
|
|
|
41
41
|
|
|
42
42
|
`python-delphi-lsp` parses Delphi/Object Pascal, builds semantic and project
|
|
43
43
|
indexes, serves LSP, and provides bounded codebase navigation for agents.
|
|
44
|
-
Version 2.0
|
|
44
|
+
Version 2.1.0 is authored by Dark Light and supports Windows, macOS, and Linux.
|
|
45
45
|
|
|
46
46
|
## Install and quick start
|
|
47
47
|
|
|
@@ -186,12 +186,15 @@ problems; paths are not guessed.
|
|
|
186
186
|
|
|
187
187
|
```text
|
|
188
188
|
delphi-lsp-agent cache start --root PATH [--project-file FILE] [--max-memory 512M]
|
|
189
|
+
[--workers auto|N] [--startup-timeout 120]
|
|
189
190
|
[--idle-timeout 1800]
|
|
190
191
|
delphi-lsp-agent cache status --root PATH [--format text|json]
|
|
191
192
|
delphi-lsp-agent cache stop --root PATH
|
|
192
193
|
delphi-lsp-agent view --root PATH [--project-file FILE] --layer LAYER
|
|
193
194
|
[--query TEXT] [--format markdown|json] [--deep-projects]
|
|
195
|
+
[--workers auto|N]
|
|
194
196
|
delphi-lsp-agent index --root PATH [--project-file FILE] [--out FILE]
|
|
197
|
+
[--workers auto|N]
|
|
195
198
|
delphi-lsp-agent query --root PATH ACTION [VALUE]
|
|
196
199
|
[--project-id FILE] [--detail summary|declaration|members|context|body|implementations]
|
|
197
200
|
[--relation references|callers|callees|uses|used_by|inherits|implements]
|
|
@@ -199,7 +202,7 @@ delphi-lsp-agent query --root PATH ACTION [VALUE]
|
|
|
199
202
|
delphi-lsp-agent skill install [--target PATH] [--force]
|
|
200
203
|
delphi-lsp-agent opencode install [--target PATH] [--python PYTHON]
|
|
201
204
|
[--force] [--write-agent|--write-config]
|
|
202
|
-
delphi-lsp-agent worker --root PATH [--project-file FILE]
|
|
205
|
+
delphi-lsp-agent worker --root PATH [--project-file FILE] [--workers auto|N]
|
|
203
206
|
```
|
|
204
207
|
|
|
205
208
|
The `cache` commands manage one daemon per canonical root. Use these:
|
|
@@ -233,6 +236,27 @@ fast. The cache retained-cache budget is `512 MiB` by default and tracks retaine
|
|
|
233
236
|
cache usage only, not a hard RSS/parse peak. Warnings are emitted on stderr at or
|
|
234
237
|
above 80 percent.
|
|
235
238
|
|
|
239
|
+
Cold builds parse independent source units in short-lived processes created with
|
|
240
|
+
the cross-platform `spawn` method. `--workers auto|N` defaults to `auto`.
|
|
241
|
+
Automatic selection uses at most four worker processes, leaves one detected CPU
|
|
242
|
+
free, never exceeds the source task count, and—for the cache daemon—allows one
|
|
243
|
+
worker per `128 MiB` of retained-cache budget. `view` and `index` use the same
|
|
244
|
+
task, CPU, and four-worker caps without the cache-budget term. An explicit value
|
|
245
|
+
from 1 through 32 overrides the automatic CPU and memory caps but is still
|
|
246
|
+
limited by the number of tasks.
|
|
247
|
+
|
|
248
|
+
Worker processes exit before retained-cache accounting. Their models and source
|
|
249
|
+
text are streamed into the parent, so transient worker memory is separate from
|
|
250
|
+
the retained navigation structures and the existing 80-percent warning. If an
|
|
251
|
+
automatic pool fails before accepting a result, one automatic serial fallback
|
|
252
|
+
is attempted; explicit worker counts fail instead of silently changing the
|
|
253
|
+
requested configuration.
|
|
254
|
+
|
|
255
|
+
`cache start` waits up to `--startup-timeout 120` seconds by default for a large
|
|
256
|
+
workspace to prewarm. The timeout belongs to the starting client and does not
|
|
257
|
+
change daemon compatibility or idle shutdown. Starting a live root with a
|
|
258
|
+
different worker configuration reports a configuration conflict.
|
|
259
|
+
|
|
236
260
|
Eviction is ordered: auxiliary caches are evicted first, navigation caches second.
|
|
237
261
|
If compaction removes navigable data, the daemon rebuilds the navigation state on demand
|
|
238
262
|
while preserving focus state for the next request.
|
|
@@ -240,7 +264,9 @@ while preserving focus state for the next request.
|
|
|
240
264
|
The daemon tracks a 30-minute idle timeout; idle state shows in JSON status (`cache status`).
|
|
241
265
|
`source revision` changes on source edits and invalidate reused request caches.
|
|
242
266
|
Workspace state appears in status as `requests`, `warm_hits`, `rebuilds`, `invalidations`,
|
|
243
|
-
`evictions`, and `cache_state`.
|
|
267
|
+
`evictions`, and `cache_state`. Parallel prewarm status adds
|
|
268
|
+
`workers_configured`, `workers_effective`, `parallel_files_completed`,
|
|
269
|
+
`prewarm_seconds`, `parallel_seconds`, and `parallel_fallbacks`.
|
|
244
270
|
|
|
245
271
|
Metadata is stored in `.delphi-lsp/agent-cache/daemon.json` with owner-only token and
|
|
246
272
|
permissions (`daemon.json` mode 600 and parent 700). Do not copy or share this token
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
delphi_lsp/__init__.py,sha256=mq3tF0NrXDgbkJXDuf5HhW6sOnmP7DQxs3zl8fGoAmo,3867
|
|
2
|
-
delphi_lsp/_version.py,sha256=
|
|
3
|
-
delphi_lsp/agent_cache.py,sha256=
|
|
4
|
-
delphi_lsp/agent_cli.py,sha256=
|
|
5
|
-
delphi_lsp/agent_context.py,sha256=
|
|
6
|
-
delphi_lsp/agent_layers.py,sha256=
|
|
2
|
+
delphi_lsp/_version.py,sha256=Xybt2skBZamGMNlLuOX1IG-h4uIxqUDGAO8MIGWrJac,22
|
|
3
|
+
delphi_lsp/agent_cache.py,sha256=mPoT94TYuv4RXHLS0rVQWn7TmbfiU34BaEkLOXP18m4,33299
|
|
4
|
+
delphi_lsp/agent_cli.py,sha256=J2zmZP2D28pXi7tx06YyEqKsojbXQcO0LdZx27kGJ1M,16311
|
|
5
|
+
delphi_lsp/agent_context.py,sha256=IJpGWkFZGYq7FU4ijvaf-xqSWJAuvEWdn3AOeM5TcnE,79572
|
|
6
|
+
delphi_lsp/agent_layers.py,sha256=Waiwp47BuNxxwzM8EfGb0tvLSRn2udRHkTmuX2JE6cg,25658
|
|
7
7
|
delphi_lsp/agent_metrics.py,sha256=6y9DrSnKitZbwRFQrofanw6k89YGqbvEacqgSKHgh2c,2695
|
|
8
8
|
delphi_lsp/agent_protocol.py,sha256=cZ1LIMhrkOb5CqiGcjSH1eZUNqtJCYySUc54h-PCyOs,12551
|
|
9
9
|
delphi_lsp/agent_relations.py,sha256=aOeRYa6pw8VO0OyQZLk1wHE529k9DIDm3ENPM5ZOJiw,32879
|
|
@@ -18,6 +18,7 @@ delphi_lsp/lark_tokens.py,sha256=xT_GES2z6p1IdN3EnoKE9RcUM6suu1RV3J-0FEJN2r8,417
|
|
|
18
18
|
delphi_lsp/lsp_server.py,sha256=hfuv8QXGQIO2s4oWYWzOjujejXNcS_ITPQ9ikw4Ua1U,71468
|
|
19
19
|
delphi_lsp/metrics.py,sha256=f70B1awypqVWZIW_SNpFHdShoS8OzDW5DWX5ftQMI0c,26166
|
|
20
20
|
delphi_lsp/nodes.py,sha256=LW8KUgNCg9MHK_2NmzrgCMMJNcBwa4C76XwKoS7kt4E,13556
|
|
21
|
+
delphi_lsp/parallel_outline.py,sha256=nXIv-a9sn3nBeKaBtIhdD2YxGzHObdW3GxYEZ5Sy0pE,8588
|
|
21
22
|
delphi_lsp/parser.py,sha256=4hckHD_fsiiUe3oeSRJwqbY8bDWqJaYLsU10NYDIW6E,6025
|
|
22
23
|
delphi_lsp/preprocessor.py,sha256=aXJiabmCvWoUdrxYAA4EVVxDDQXl_IeFUezoDdMlFyU,30984
|
|
23
24
|
delphi_lsp/progress.py,sha256=FF3lqdDKLvq3b7cOhuSc3P0FDzVnX0miD8U8_3xt5iY,535
|
|
@@ -28,9 +29,9 @@ delphi_lsp/semantic_builder.py,sha256=ulA7xHJpltjc6cTo5wh9yTcYazuIykM-YLMXD1h-wz
|
|
|
28
29
|
delphi_lsp/source_reader.py,sha256=HWiw25sLVZ6s1GGpMl17uZ-o68HoxTEd3Z0__m1vsSE,520
|
|
29
30
|
delphi_lsp/workspace.py,sha256=TU__SujItRlW8hB2kuYC7ZJx1WQVOj7frYxvo7rDqeI,2137
|
|
30
31
|
delphi_lsp/writer.py,sha256=j-cnyiHFNOTFECL7Ehm-m43ye7ev7qFeCCscFMjrAOY,2503
|
|
31
|
-
python_delphi_lsp-2.0.
|
|
32
|
-
python_delphi_lsp-2.0.
|
|
33
|
-
python_delphi_lsp-2.0.
|
|
34
|
-
python_delphi_lsp-2.0.
|
|
35
|
-
python_delphi_lsp-2.0.
|
|
36
|
-
python_delphi_lsp-2.0.
|
|
32
|
+
python_delphi_lsp-2.1.0.dist-info/licenses/LICENSE,sha256=-rPda9qyJvHAhjCx3ZF-Efy07F4eAg4sFvg6ChOGPoU,16726
|
|
33
|
+
python_delphi_lsp-2.1.0.dist-info/METADATA,sha256=REk-5BwUnYBx-IwSg9vYBrYl-gsEkJdrD9KEQjxPwss,19972
|
|
34
|
+
python_delphi_lsp-2.1.0.dist-info/WHEEL,sha256=K260EYznzXsJYBQGqmI8VTxEdiZYNvDZwW9cBh9-_MA,91
|
|
35
|
+
python_delphi_lsp-2.1.0.dist-info/entry_points.txt,sha256=rh_yGH7diowge_acQcST87A-ewH1QezEsAYC8T5ineY,103
|
|
36
|
+
python_delphi_lsp-2.1.0.dist-info/top_level.txt,sha256=0Zp0sjw5Qtic3EQX9zNeegURga8NuXk6c2e9PN-3OTs,11
|
|
37
|
+
python_delphi_lsp-2.1.0.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|