python-delphi-lsp 3.6.0__cp310-abi3-win_amd64.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 (56) hide show
  1. delphi_lsp/__init__.py +184 -0
  2. delphi_lsp/_version.py +1 -0
  3. delphi_lsp/agent_cache.py +1574 -0
  4. delphi_lsp/agent_cli.py +902 -0
  5. delphi_lsp/agent_context.py +3907 -0
  6. delphi_lsp/agent_cpg.py +349 -0
  7. delphi_lsp/agent_cpg_builder.py +666 -0
  8. delphi_lsp/agent_layers.py +1410 -0
  9. delphi_lsp/agent_metrics.py +526 -0
  10. delphi_lsp/agent_protocol.py +445 -0
  11. delphi_lsp/agent_relations.py +1881 -0
  12. delphi_lsp/agent_templates.py +614 -0
  13. delphi_lsp/agent_wiki.py +2853 -0
  14. delphi_lsp/agent_workspace.py +860 -0
  15. delphi_lsp/ast_serialize.py +126 -0
  16. delphi_lsp/binary.py +250 -0
  17. delphi_lsp/comment_builder.py +28 -0
  18. delphi_lsp/consts.py +345 -0
  19. delphi_lsp/delphi_lsp_native.pyd +0 -0
  20. delphi_lsp/delphiast_lexer.py +399 -0
  21. delphi_lsp/delphiast_parser.py +2189 -0
  22. delphi_lsp/delphiast_tokens.py +41 -0
  23. delphi_lsp/grammar.py +459 -0
  24. delphi_lsp/incremental.py +1139 -0
  25. delphi_lsp/lark_builder.py +2684 -0
  26. delphi_lsp/lark_tokens.py +236 -0
  27. delphi_lsp/lexical_scanner.py +30 -0
  28. delphi_lsp/lsp_server.py +6698 -0
  29. delphi_lsp/metrics.py +1452 -0
  30. delphi_lsp/native_bridge.py +300 -0
  31. delphi_lsp/navigation_cache.py +189 -0
  32. delphi_lsp/nodes.py +462 -0
  33. delphi_lsp/parallel_outline.py +662 -0
  34. delphi_lsp/parse_artifacts.py +969 -0
  35. delphi_lsp/parser.py +342 -0
  36. delphi_lsp/parser_backend.py +139 -0
  37. delphi_lsp/preprocessor.py +1122 -0
  38. delphi_lsp/progress.py +26 -0
  39. delphi_lsp/project_config.py +285 -0
  40. delphi_lsp/project_discovery.py +932 -0
  41. delphi_lsp/project_indexer.py +470 -0
  42. delphi_lsp/py.typed +1 -0
  43. delphi_lsp/response_cache.py +465 -0
  44. delphi_lsp/semantic.py +394 -0
  45. delphi_lsp/semantic_builder.py +2302 -0
  46. delphi_lsp/source_reader.py +17 -0
  47. delphi_lsp/wiki_external_sort.py +392 -0
  48. delphi_lsp/wiki_memory.py +548 -0
  49. delphi_lsp/workspace.py +83 -0
  50. delphi_lsp/writer.py +73 -0
  51. python_delphi_lsp-3.6.0.dist-info/METADATA +766 -0
  52. python_delphi_lsp-3.6.0.dist-info/RECORD +56 -0
  53. python_delphi_lsp-3.6.0.dist-info/WHEEL +4 -0
  54. python_delphi_lsp-3.6.0.dist-info/entry_points.txt +3 -0
  55. python_delphi_lsp-3.6.0.dist-info/licenses/LICENSE +373 -0
  56. python_delphi_lsp-3.6.0.dist-info/sboms/python-delphi-lsp.cyclonedx.json +770 -0
@@ -0,0 +1,526 @@
1
+ from __future__ import annotations
2
+
3
+ import pickle
4
+ import zlib
5
+ from collections.abc import Callable, Iterable
6
+ from dataclasses import dataclass, replace
7
+ from pathlib import Path
8
+
9
+ from .agent_protocol import make_target_id
10
+ from .agent_workspace import (
11
+ AgentWorkspace,
12
+ unit_display_path,
13
+ unit_source_path,
14
+ unit_target_id,
15
+ )
16
+ from .metrics import (
17
+ MetricProblem,
18
+ ProjectMetrics,
19
+ UnitMetrics,
20
+ aggregate_project_metrics,
21
+ analyze_include,
22
+ analyze_unit,
23
+ )
24
+ from .parallel_outline import resolve_worker_count, run_outline_tasks
25
+ from .parser_backend import ParserMode
26
+ from .preprocessor import IncludeLoader
27
+ from .project_config import ProjectPathConfig, workspace_include_loader
28
+ from .source_reader import read_source_text
29
+
30
+ _LARGE_PROJECT_UNIT_THRESHOLD = 256
31
+ _LARGE_PROJECT_BYTE_THRESHOLD = 16 * 1024 * 1024
32
+ _METRIC_TASK_BATCH_SIZE = 4
33
+ _LARGE_METRIC_TASK_THRESHOLD = 256
34
+ _LARGE_METRIC_BATCHES_PER_WORKER = 12
35
+ MetricProgressCallback = Callable[[int, int, str], None]
36
+
37
+
38
+ @dataclass(frozen=True, slots=True)
39
+ class _MetricTask:
40
+ ordinal: int
41
+ source_path: str
42
+ display_path: str
43
+ unit_id: str
44
+ defines: tuple[str, ...]
45
+ include_paths: tuple[str, ...]
46
+ parser_mode: ParserMode
47
+ project_config: ProjectPathConfig | None = None
48
+
49
+
50
+ @dataclass(frozen=True, slots=True)
51
+ class _MetricResult:
52
+ ordinal: int
53
+ source_path: str
54
+ metrics: UnitMetrics | None
55
+ problem: MetricProblem | None
56
+
57
+
58
+ @dataclass(frozen=True, slots=True)
59
+ class _MetricTaskBatch:
60
+ ordinal: int
61
+ source_path: str
62
+ tasks: tuple[_MetricTask, ...]
63
+ compress_vocabulary: bool = False
64
+
65
+
66
+ @dataclass(frozen=True, slots=True)
67
+ class _MetricResultBatch:
68
+ ordinal: int
69
+ source_path: str
70
+ results: tuple[_MetricResult, ...]
71
+ vocabulary_blob: bytes = b""
72
+
73
+
74
+ def _analyze_metric_task(task: _MetricTask) -> _MetricResult:
75
+ return _analyze_metric_task_with_loader(
76
+ task,
77
+ workspace_include_loader(
78
+ task.project_config,
79
+ task.include_paths,
80
+ ),
81
+ )
82
+
83
+
84
+ def _analyze_metric_task_with_loader(
85
+ task: _MetricTask,
86
+ include_loader: IncludeLoader | None,
87
+ ) -> _MetricResult:
88
+ try:
89
+ source = read_source_text(Path(task.source_path))
90
+ except (OSError, UnicodeError):
91
+ return _MetricResult(
92
+ task.ordinal,
93
+ task.source_path,
94
+ None,
95
+ MetricProblem(
96
+ kind="source_unavailable",
97
+ path=task.display_path,
98
+ message="Could not read source.",
99
+ ),
100
+ )
101
+ metrics = analyze_unit(
102
+ source,
103
+ task.display_path,
104
+ defines=task.defines,
105
+ include_paths=task.include_paths,
106
+ include_loader=include_loader,
107
+ parser_mode=task.parser_mode,
108
+ )
109
+ return _MetricResult(
110
+ task.ordinal,
111
+ task.source_path,
112
+ replace(metrics, unit_id=task.unit_id),
113
+ None,
114
+ )
115
+
116
+
117
+ def _analyze_metric_batch(batch: _MetricTaskBatch) -> _MetricResultBatch:
118
+ results: list[_MetricResult] = []
119
+ operators: set[str] = set()
120
+ operands: set[str] = set()
121
+ vocabulary_result_index: int | None = None
122
+ include_loaders: dict[
123
+ tuple[ProjectPathConfig | None, tuple[str, ...]],
124
+ IncludeLoader | None,
125
+ ] = {}
126
+ for task in batch.tasks:
127
+ context = (task.project_config, task.include_paths)
128
+ if context not in include_loaders:
129
+ include_loaders[context] = workspace_include_loader(*context)
130
+ result = _analyze_metric_task_with_loader(task, include_loaders[context])
131
+ if result.metrics is not None:
132
+ operators.update(result.metrics._operator_vocabulary)
133
+ operands.update(result.metrics._operand_vocabulary)
134
+ if vocabulary_result_index is None:
135
+ vocabulary_result_index = len(results)
136
+ result = replace(
137
+ result,
138
+ metrics=replace(
139
+ result.metrics,
140
+ _operator_vocabulary=frozenset(),
141
+ _operand_vocabulary=frozenset(),
142
+ ),
143
+ )
144
+ results.append(result)
145
+ vocabulary_blob = b""
146
+ if vocabulary_result_index is not None and batch.compress_vocabulary:
147
+ vocabulary_blob = zlib.compress(
148
+ pickle.dumps(
149
+ (frozenset(operators), frozenset(operands)),
150
+ protocol=pickle.HIGHEST_PROTOCOL,
151
+ ),
152
+ level=1,
153
+ )
154
+ elif vocabulary_result_index is not None:
155
+ vocabulary_result = results[vocabulary_result_index]
156
+ assert vocabulary_result.metrics is not None
157
+ results[vocabulary_result_index] = replace(
158
+ vocabulary_result,
159
+ metrics=replace(
160
+ vocabulary_result.metrics,
161
+ _operator_vocabulary=frozenset(operators),
162
+ _operand_vocabulary=frozenset(operands),
163
+ ),
164
+ )
165
+ return _MetricResultBatch(
166
+ ordinal=batch.ordinal,
167
+ source_path=batch.source_path,
168
+ results=tuple(results),
169
+ vocabulary_blob=vocabulary_blob,
170
+ )
171
+
172
+
173
+ def _metric_task_batches(
174
+ tasks: Iterable[_MetricTask],
175
+ *,
176
+ batch_count: int | None = None,
177
+ ) -> tuple[_MetricTaskBatch, ...]:
178
+ task_list = tuple(tasks)
179
+ if not task_list:
180
+ return ()
181
+ selected_batch_count = batch_count or (
182
+ (len(task_list) + _METRIC_TASK_BATCH_SIZE - 1) // _METRIC_TASK_BATCH_SIZE
183
+ )
184
+ selected_batch_count = min(max(1, selected_batch_count), len(task_list))
185
+ buckets: list[list[_MetricTask]] = [[] for _ in range(selected_batch_count)]
186
+ sizes: dict[str, int] = {}
187
+ for task in task_list:
188
+ try:
189
+ sizes[task.source_path] = Path(task.source_path).stat().st_size
190
+ except OSError:
191
+ sizes[task.source_path] = -1
192
+ ordered = sorted(
193
+ task_list,
194
+ key=lambda task: (-sizes[task.source_path], task.ordinal),
195
+ )
196
+ bucket_sizes = [0] * selected_batch_count
197
+ for index, task in enumerate(ordered):
198
+ if batch_count is None:
199
+ bucket_index = index % selected_batch_count
200
+ else:
201
+ bucket_index = min(
202
+ range(selected_batch_count),
203
+ key=lambda candidate: (bucket_sizes[candidate], candidate),
204
+ )
205
+ buckets[bucket_index].append(task)
206
+ bucket_sizes[bucket_index] += max(0, sizes[task.source_path])
207
+ return tuple(
208
+ _MetricTaskBatch(
209
+ ordinal=index,
210
+ source_path=bucket[0].source_path,
211
+ tasks=tuple(bucket),
212
+ compress_vocabulary=batch_count is not None,
213
+ )
214
+ for index, bucket in enumerate(buckets)
215
+ if bucket
216
+ )
217
+
218
+
219
+ def _run_metric_tasks(
220
+ tasks: Iterable[_MetricTask],
221
+ *,
222
+ configured_workers: int,
223
+ memory_budget_bytes: int | None = None,
224
+ on_complete: Callable[[_MetricResult], None],
225
+ cancel_check: Callable[[], None] | None = None,
226
+ ) -> None:
227
+ if cancel_check is not None:
228
+ cancel_check()
229
+ task_list = tuple(tasks)
230
+ batch_count = None
231
+ if (
232
+ len(task_list) >= _LARGE_METRIC_TASK_THRESHOLD
233
+ and configured_workers != 1
234
+ ):
235
+ batch_count = (
236
+ resolve_worker_count(
237
+ configured_workers,
238
+ task_count=len(task_list),
239
+ memory_budget_bytes=memory_budget_bytes,
240
+ )
241
+ * _LARGE_METRIC_BATCHES_PER_WORKER
242
+ )
243
+
244
+ def consume_batch(batch: _MetricResultBatch) -> None:
245
+ if cancel_check is not None:
246
+ cancel_check()
247
+ results = list(batch.results)
248
+ if batch.vocabulary_blob:
249
+ operators, operands = pickle.loads(zlib.decompress(batch.vocabulary_blob))
250
+ for index, result in enumerate(results):
251
+ if result.metrics is None:
252
+ continue
253
+ results[index] = replace(
254
+ result,
255
+ metrics=replace(
256
+ result.metrics,
257
+ _operator_vocabulary=operators,
258
+ _operand_vocabulary=operands,
259
+ ),
260
+ )
261
+ break
262
+ for result in results:
263
+ if cancel_check is not None:
264
+ cancel_check()
265
+ on_complete(result)
266
+
267
+ run_outline_tasks(
268
+ _metric_task_batches(task_list, batch_count=batch_count),
269
+ configured_workers=configured_workers,
270
+ memory_budget_bytes=memory_budget_bytes,
271
+ on_complete=consume_batch,
272
+ retain_results=False,
273
+ task_runner=_analyze_metric_batch,
274
+ )
275
+
276
+
277
+ def build_workspace_metrics(
278
+ workspace: AgentWorkspace,
279
+ *,
280
+ workers: int = 0,
281
+ worker_memory_budget_bytes: int | None = None,
282
+ cancel_check: Callable[[], None] | None = None,
283
+ ) -> ProjectMetrics:
284
+ units = tuple(workspace.units)
285
+ source_bytes = 0
286
+ for unit in units:
287
+ if cancel_check is not None:
288
+ cancel_check()
289
+ try:
290
+ source_bytes += unit_source_path(workspace.root, unit).stat().st_size
291
+ except OSError:
292
+ continue
293
+ parser_mode = (
294
+ ParserMode.TOLERANT
295
+ if len(units) >= _LARGE_PROJECT_UNIT_THRESHOLD
296
+ or source_bytes >= _LARGE_PROJECT_BYTE_THRESHOLD
297
+ else ParserMode.STRICT
298
+ )
299
+ analyzed_units: list[UnitMetrics] = []
300
+ include_metrics: list[UnitMetrics] = []
301
+ problems: list[MetricProblem] = []
302
+
303
+ def consume_result(result: _MetricResult) -> None:
304
+ if result.metrics is not None:
305
+ analyzed_units.append(result.metrics)
306
+ if result.problem is not None:
307
+ problems.append(result.problem)
308
+
309
+ _run_metric_tasks(
310
+ (
311
+ _MetricTask(
312
+ ordinal,
313
+ str(unit_source_path(workspace.root, unit)),
314
+ unit_display_path(workspace.root, unit),
315
+ unit_target_id(workspace.root, unit),
316
+ workspace.defines,
317
+ workspace.include_paths,
318
+ parser_mode,
319
+ workspace.project_config,
320
+ )
321
+ for ordinal, unit in enumerate(units)
322
+ ),
323
+ configured_workers=workers,
324
+ memory_budget_bytes=worker_memory_budget_bytes,
325
+ on_complete=consume_result,
326
+ cancel_check=cancel_check,
327
+ )
328
+
329
+ include_loc = 0
330
+ seen_includes: set[str] = set()
331
+ for include_file in workspace.include_files:
332
+ if cancel_check is not None:
333
+ cancel_check()
334
+ recorded_path = include_file["path"]
335
+ display_path = workspace.include_display_path(recorded_path)
336
+ normalized_path = str(display_path).replace("\\", "/").casefold()
337
+ if normalized_path in seen_includes:
338
+ continue
339
+ seen_includes.add(normalized_path)
340
+ source_path = workspace.include_source_path(recorded_path)
341
+ try:
342
+ include_source = read_source_text(source_path)
343
+ item = replace(
344
+ analyze_include(include_source, display_path),
345
+ unit_id=make_target_id(
346
+ "unit",
347
+ display_path,
348
+ Path(include_file["name"]).stem,
349
+ ),
350
+ )
351
+ include_metrics.append(item)
352
+ include_loc += item.lines.total_lines
353
+ except (OSError, UnicodeError):
354
+ problems.append(
355
+ MetricProblem(
356
+ kind="include_unavailable",
357
+ path=display_path,
358
+ message="Could not read include file.",
359
+ )
360
+ )
361
+
362
+ project = workspace.active_project
363
+ metrics = aggregate_project_metrics(
364
+ analyzed_units,
365
+ include_loc=include_loc,
366
+ include_metrics=include_metrics,
367
+ project_id=workspace.active_project_id,
368
+ project_name=project.name if project is not None else "",
369
+ )
370
+ if problems:
371
+ metrics = replace(
372
+ metrics,
373
+ problems=(
374
+ *metrics.problems,
375
+ *sorted(problems, key=lambda item: (item.path.casefold(), item.kind)),
376
+ ),
377
+ )
378
+ return metrics
379
+
380
+
381
+ def build_path_metrics(
382
+ root: str | Path,
383
+ source_files: Iterable[str | Path],
384
+ *,
385
+ defines: Iterable[str] = (),
386
+ include_paths: Iterable[str | Path] = (),
387
+ project_config: ProjectPathConfig | None = None,
388
+ project_name: str = "",
389
+ workers: int = 0,
390
+ on_progress: MetricProgressCallback | None = None,
391
+ ) -> ProjectMetrics:
392
+ """Analyze file paths without retaining the complete source corpus in RAM."""
393
+
394
+ root_path = Path(root).expanduser().resolve()
395
+ unique_paths: dict[str, Path] = {}
396
+ for value in source_files:
397
+ path = Path(value).expanduser()
398
+ if not path.is_absolute():
399
+ path = root_path / path
400
+ path = path.resolve()
401
+ if (
402
+ project_config is not None
403
+ and project_config.excludes_workspace_path(path)
404
+ ):
405
+ continue
406
+ unique_paths.setdefault(str(path).replace("\\", "/").casefold(), path)
407
+ unit_paths = [
408
+ path for path in unique_paths.values() if path.suffix.casefold() in {".pas", ".dpr", ".dpk"}
409
+ ]
410
+ include_files = [
411
+ path for path in unique_paths.values() if path.suffix.casefold() == ".inc"
412
+ ]
413
+ source_bytes = 0
414
+ for path in unit_paths:
415
+ try:
416
+ source_bytes += path.stat().st_size
417
+ except OSError:
418
+ continue
419
+ parser_mode = (
420
+ ParserMode.TOLERANT
421
+ if len(unit_paths) >= _LARGE_PROJECT_UNIT_THRESHOLD
422
+ or source_bytes >= _LARGE_PROJECT_BYTE_THRESHOLD
423
+ else ParserMode.STRICT
424
+ )
425
+ define_values = tuple(defines)
426
+ include_path_values = tuple(str(Path(value)) for value in include_paths)
427
+ analyzed_units: list[UnitMetrics] = []
428
+ include_metrics: list[UnitMetrics] = []
429
+ problems: list[MetricProblem] = []
430
+ total_files = len(unit_paths) + len(include_files)
431
+ completed_files = 0
432
+
433
+ def consume_result(result: _MetricResult) -> None:
434
+ nonlocal completed_files
435
+ if result.metrics is not None:
436
+ analyzed_units.append(result.metrics)
437
+ if result.problem is not None:
438
+ problems.append(result.problem)
439
+ completed_files += 1
440
+ if on_progress is not None:
441
+ on_progress(completed_files, total_files, result.source_path)
442
+
443
+ _run_metric_tasks(
444
+ (
445
+ _MetricTask(
446
+ ordinal,
447
+ str(path),
448
+ _display_path(path, root_path),
449
+ "",
450
+ define_values,
451
+ include_path_values,
452
+ parser_mode,
453
+ project_config,
454
+ )
455
+ for ordinal, path in enumerate(unit_paths)
456
+ ),
457
+ configured_workers=workers,
458
+ on_complete=consume_result,
459
+ )
460
+
461
+ include_loc = 0
462
+ for path in include_files:
463
+ try:
464
+ source = read_source_text(path)
465
+ item = analyze_include(source, _display_path(path, root_path))
466
+ include_metrics.append(item)
467
+ include_loc += item.lines.total_lines
468
+ except (OSError, UnicodeError):
469
+ problems.append(
470
+ MetricProblem(
471
+ kind="include_unavailable",
472
+ path=_display_path(path, root_path),
473
+ message="Could not read include file.",
474
+ )
475
+ )
476
+ finally:
477
+ completed_files += 1
478
+ if on_progress is not None:
479
+ on_progress(completed_files, total_files, str(path))
480
+ if total_files == 0 and on_progress is not None:
481
+ on_progress(1, 1, "")
482
+ metrics = aggregate_project_metrics(
483
+ analyzed_units,
484
+ include_loc=include_loc,
485
+ include_metrics=include_metrics,
486
+ project_name=project_name,
487
+ )
488
+ if problems:
489
+ metrics = replace(
490
+ metrics,
491
+ problems=(
492
+ *metrics.problems,
493
+ *sorted(problems, key=lambda item: (item.path.casefold(), item.kind)),
494
+ ),
495
+ )
496
+ return metrics
497
+
498
+
499
+ def project_metric_item(metrics: ProjectMetrics) -> dict[str, object]:
500
+ return {"item_type": "project_metrics", **metrics.to_mapping()}
501
+
502
+
503
+ def unit_metric_item(metrics: UnitMetrics, *, detail: bool = False) -> dict[str, object]:
504
+ return {"item_type": "unit_metrics", **metrics.to_mapping(detail=detail)}
505
+
506
+
507
+ def _workspace_path(root: Path, value: str) -> Path:
508
+ path = Path(value).expanduser()
509
+ if not path.is_absolute():
510
+ path = root / path
511
+ return path.resolve()
512
+
513
+
514
+ def _display_path(path: Path, root: Path) -> str:
515
+ try:
516
+ return path.relative_to(root).as_posix()
517
+ except ValueError:
518
+ return path.as_posix()
519
+
520
+
521
+ __all__ = [
522
+ "build_path_metrics",
523
+ "build_workspace_metrics",
524
+ "project_metric_item",
525
+ "unit_metric_item",
526
+ ]