python-delphi-lsp 2.0.3__tar.gz → 2.0.4__tar.gz

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 (137) hide show
  1. {python_delphi_lsp-2.0.3/python_delphi_lsp.egg-info → python_delphi_lsp-2.0.4}/PKG-INFO +15 -2
  2. {python_delphi_lsp-2.0.3 → python_delphi_lsp-2.0.4}/README.md +14 -1
  3. {python_delphi_lsp-2.0.3 → python_delphi_lsp-2.0.4}/delphi_lsp/__init__.py +3 -0
  4. python_delphi_lsp-2.0.4/delphi_lsp/_version.py +1 -0
  5. {python_delphi_lsp-2.0.3 → python_delphi_lsp-2.0.4}/delphi_lsp/agent_context.py +8 -1
  6. {python_delphi_lsp-2.0.3 → python_delphi_lsp-2.0.4}/delphi_lsp/agent_layers.py +115 -6
  7. {python_delphi_lsp-2.0.3 → python_delphi_lsp-2.0.4}/delphi_lsp/lsp_server.py +195 -33
  8. {python_delphi_lsp-2.0.3 → python_delphi_lsp-2.0.4}/delphi_lsp/preprocessor.py +32 -10
  9. python_delphi_lsp-2.0.4/delphi_lsp/progress.py +26 -0
  10. {python_delphi_lsp-2.0.3 → python_delphi_lsp-2.0.4}/delphi_lsp/project_discovery.py +125 -16
  11. {python_delphi_lsp-2.0.3 → python_delphi_lsp-2.0.4}/delphi_lsp/project_indexer.py +43 -1
  12. {python_delphi_lsp-2.0.3 → python_delphi_lsp-2.0.4}/pyproject.toml +1 -1
  13. {python_delphi_lsp-2.0.3 → python_delphi_lsp-2.0.4/python_delphi_lsp.egg-info}/PKG-INFO +15 -2
  14. {python_delphi_lsp-2.0.3 → python_delphi_lsp-2.0.4}/python_delphi_lsp.egg-info/SOURCES.txt +2 -0
  15. {python_delphi_lsp-2.0.3 → python_delphi_lsp-2.0.4}/tests/test_agent_codebase.py +79 -0
  16. {python_delphi_lsp-2.0.3 → python_delphi_lsp-2.0.4}/tests/test_lsp_support.py +243 -18
  17. {python_delphi_lsp-2.0.3 → python_delphi_lsp-2.0.4}/tests/test_package_metadata.py +2 -2
  18. python_delphi_lsp-2.0.4/tests/test_progress.py +58 -0
  19. {python_delphi_lsp-2.0.3 → python_delphi_lsp-2.0.4}/tests/test_project_discovery.py +77 -5
  20. python_delphi_lsp-2.0.3/delphi_lsp/_version.py +0 -1
  21. {python_delphi_lsp-2.0.3 → python_delphi_lsp-2.0.4}/.github/workflows/ci.yml +0 -0
  22. {python_delphi_lsp-2.0.3 → python_delphi_lsp-2.0.4}/LICENSE +0 -0
  23. {python_delphi_lsp-2.0.3 → python_delphi_lsp-2.0.4}/MANIFEST.in +0 -0
  24. {python_delphi_lsp-2.0.3 → python_delphi_lsp-2.0.4}/delphi_lsp/agent_cli.py +0 -0
  25. {python_delphi_lsp-2.0.3 → python_delphi_lsp-2.0.4}/delphi_lsp/agent_metrics.py +0 -0
  26. {python_delphi_lsp-2.0.3 → python_delphi_lsp-2.0.4}/delphi_lsp/agent_protocol.py +0 -0
  27. {python_delphi_lsp-2.0.3 → python_delphi_lsp-2.0.4}/delphi_lsp/agent_relations.py +0 -0
  28. {python_delphi_lsp-2.0.3 → python_delphi_lsp-2.0.4}/delphi_lsp/agent_templates.py +0 -0
  29. {python_delphi_lsp-2.0.3 → python_delphi_lsp-2.0.4}/delphi_lsp/agent_workspace.py +0 -0
  30. {python_delphi_lsp-2.0.3 → python_delphi_lsp-2.0.4}/delphi_lsp/binary.py +0 -0
  31. {python_delphi_lsp-2.0.3 → python_delphi_lsp-2.0.4}/delphi_lsp/comment_builder.py +0 -0
  32. {python_delphi_lsp-2.0.3 → python_delphi_lsp-2.0.4}/delphi_lsp/consts.py +0 -0
  33. {python_delphi_lsp-2.0.3 → python_delphi_lsp-2.0.4}/delphi_lsp/grammar.py +0 -0
  34. {python_delphi_lsp-2.0.3 → python_delphi_lsp-2.0.4}/delphi_lsp/lark_builder.py +0 -0
  35. {python_delphi_lsp-2.0.3 → python_delphi_lsp-2.0.4}/delphi_lsp/lark_tokens.py +0 -0
  36. {python_delphi_lsp-2.0.3 → python_delphi_lsp-2.0.4}/delphi_lsp/metrics.py +0 -0
  37. {python_delphi_lsp-2.0.3 → python_delphi_lsp-2.0.4}/delphi_lsp/nodes.py +0 -0
  38. {python_delphi_lsp-2.0.3 → python_delphi_lsp-2.0.4}/delphi_lsp/parser.py +0 -0
  39. {python_delphi_lsp-2.0.3 → python_delphi_lsp-2.0.4}/delphi_lsp/semantic.py +0 -0
  40. {python_delphi_lsp-2.0.3 → python_delphi_lsp-2.0.4}/delphi_lsp/semantic_builder.py +0 -0
  41. {python_delphi_lsp-2.0.3 → python_delphi_lsp-2.0.4}/delphi_lsp/source_reader.py +0 -0
  42. {python_delphi_lsp-2.0.3 → python_delphi_lsp-2.0.4}/delphi_lsp/workspace.py +0 -0
  43. {python_delphi_lsp-2.0.3 → python_delphi_lsp-2.0.4}/delphi_lsp/writer.py +0 -0
  44. {python_delphi_lsp-2.0.3 → python_delphi_lsp-2.0.4}/opencode.json +0 -0
  45. {python_delphi_lsp-2.0.3 → python_delphi_lsp-2.0.4}/python_delphi_lsp.egg-info/dependency_links.txt +0 -0
  46. {python_delphi_lsp-2.0.3 → python_delphi_lsp-2.0.4}/python_delphi_lsp.egg-info/entry_points.txt +0 -0
  47. {python_delphi_lsp-2.0.3 → python_delphi_lsp-2.0.4}/python_delphi_lsp.egg-info/requires.txt +0 -0
  48. {python_delphi_lsp-2.0.3 → python_delphi_lsp-2.0.4}/python_delphi_lsp.egg-info/top_level.txt +0 -0
  49. {python_delphi_lsp-2.0.3 → python_delphi_lsp-2.0.4}/scripts/audit_delphi_language_features.py +0 -0
  50. {python_delphi_lsp-2.0.3 → python_delphi_lsp-2.0.4}/scripts/benchmark_github_corpus.py +0 -0
  51. {python_delphi_lsp-2.0.3 → python_delphi_lsp-2.0.4}/scripts/bootstrap_vllm_codebase_skill_test.py +0 -0
  52. {python_delphi_lsp-2.0.3 → python_delphi_lsp-2.0.4}/scripts/bootstrap_vllm_opencode_test.ps1 +0 -0
  53. {python_delphi_lsp-2.0.3 → python_delphi_lsp-2.0.4}/scripts/bootstrap_vllm_opencode_test.py +0 -0
  54. {python_delphi_lsp-2.0.3 → python_delphi_lsp-2.0.4}/scripts/bootstrap_vllm_opencode_test.sh +0 -0
  55. {python_delphi_lsp-2.0.3 → python_delphi_lsp-2.0.4}/scripts/build_github_performance_corpus.py +0 -0
  56. {python_delphi_lsp-2.0.3 → python_delphi_lsp-2.0.4}/scripts/check_ornith_cache.py +0 -0
  57. {python_delphi_lsp-2.0.3 → python_delphi_lsp-2.0.4}/scripts/generate_progress_pdf.py +0 -0
  58. {python_delphi_lsp-2.0.3 → python_delphi_lsp-2.0.4}/scripts/generate_release_evidence.py +0 -0
  59. {python_delphi_lsp-2.0.3 → python_delphi_lsp-2.0.4}/scripts/ollama/ornith-lspctx.Modelfile +0 -0
  60. {python_delphi_lsp-2.0.3 → python_delphi_lsp-2.0.4}/scripts/prepare_ornith_cache.py +0 -0
  61. {python_delphi_lsp-2.0.3 → python_delphi_lsp-2.0.4}/scripts/run_opencode_lsp_probe.py +0 -0
  62. {python_delphi_lsp-2.0.3 → python_delphi_lsp-2.0.4}/scripts/run_openrouter_github_e2e.py +0 -0
  63. {python_delphi_lsp-2.0.3 → python_delphi_lsp-2.0.4}/scripts/start_ornith_vllm.sh +0 -0
  64. {python_delphi_lsp-2.0.3 → python_delphi_lsp-2.0.4}/scripts/summarize_opencode_request_payloads.py +0 -0
  65. {python_delphi_lsp-2.0.3 → python_delphi_lsp-2.0.4}/setup.cfg +0 -0
  66. {python_delphi_lsp-2.0.3 → python_delphi_lsp-2.0.4}/tests/corpora.performance.lock.json +0 -0
  67. {python_delphi_lsp-2.0.3 → python_delphi_lsp-2.0.4}/tests/fixtures/legacy_snippets/DeprecatedOnConst.pas +0 -0
  68. {python_delphi_lsp-2.0.3 → python_delphi_lsp-2.0.4}/tests/fixtures/legacy_snippets/VariantRecordFieldAttributes.pas +0 -0
  69. {python_delphi_lsp-2.0.3 → python_delphi_lsp-2.0.4}/tests/fixtures/legacy_snippets/alignedrecords.pas +0 -0
  70. {python_delphi_lsp-2.0.3 → python_delphi_lsp-2.0.4}/tests/fixtures/legacy_snippets/constset.pas +0 -0
  71. {python_delphi_lsp-2.0.3 → python_delphi_lsp-2.0.4}/tests/fixtures/legacy_snippets/deprecatedtype.pas +0 -0
  72. {python_delphi_lsp-2.0.3 → python_delphi_lsp-2.0.4}/tests/fixtures/legacy_snippets/dottedtypes.pas +0 -0
  73. {python_delphi_lsp-2.0.3 → python_delphi_lsp-2.0.4}/tests/fixtures/legacy_snippets/endtoken.pas +0 -0
  74. {python_delphi_lsp-2.0.3 → python_delphi_lsp-2.0.4}/tests/fixtures/legacy_snippets/experimentals.pas +0 -0
  75. {python_delphi_lsp-2.0.3 → python_delphi_lsp-2.0.4}/tests/fixtures/legacy_snippets/externalfunction.pas +0 -0
  76. {python_delphi_lsp-2.0.3 → python_delphi_lsp-2.0.4}/tests/fixtures/legacy_snippets/finalizationinitializationexports.pas +0 -0
  77. {python_delphi_lsp-2.0.3 → python_delphi_lsp-2.0.4}/tests/fixtures/legacy_snippets/forwardoverloaded.pas +0 -0
  78. {python_delphi_lsp-2.0.3 → python_delphi_lsp-2.0.4}/tests/fixtures/legacy_snippets/forwardwithoutsemicolon.pas +0 -0
  79. {python_delphi_lsp-2.0.3 → python_delphi_lsp-2.0.4}/tests/fixtures/legacy_snippets/genericconstraints.pas +0 -0
  80. {python_delphi_lsp-2.0.3 → python_delphi_lsp-2.0.4}/tests/fixtures/legacy_snippets/genericinterfacemethoddelegation.pas +0 -0
  81. {python_delphi_lsp-2.0.3 → python_delphi_lsp-2.0.4}/tests/fixtures/legacy_snippets/implementsgenerictype.pas +0 -0
  82. {python_delphi_lsp-2.0.3 → python_delphi_lsp-2.0.4}/tests/fixtures/legacy_snippets/include file2.inc +0 -0
  83. {python_delphi_lsp-2.0.3 → python_delphi_lsp-2.0.4}/tests/fixtures/legacy_snippets/includefile.inc +0 -0
  84. {python_delphi_lsp-2.0.3 → python_delphi_lsp-2.0.4}/tests/fixtures/legacy_snippets/includefile.pas +0 -0
  85. {python_delphi_lsp-2.0.3 → python_delphi_lsp-2.0.4}/tests/fixtures/legacy_snippets/managedrecords.pas +0 -0
  86. {python_delphi_lsp-2.0.3 → python_delphi_lsp-2.0.4}/tests/fixtures/legacy_snippets/messagemethod.pas +0 -0
  87. {python_delphi_lsp-2.0.3 → python_delphi_lsp-2.0.4}/tests/fixtures/legacy_snippets/multiline.pas +0 -0
  88. {python_delphi_lsp-2.0.3 → python_delphi_lsp-2.0.4}/tests/fixtures/legacy_snippets/nonalignedrecords.pas +0 -0
  89. {python_delphi_lsp-2.0.3 → python_delphi_lsp-2.0.4}/tests/fixtures/legacy_snippets/numbers.pas +0 -0
  90. {python_delphi_lsp-2.0.3 → python_delphi_lsp-2.0.4}/tests/fixtures/legacy_snippets/pointerchars.pas +0 -0
  91. {python_delphi_lsp-2.0.3 → python_delphi_lsp-2.0.4}/tests/fixtures/legacy_snippets/properties.pas +0 -0
  92. {python_delphi_lsp-2.0.3 → python_delphi_lsp-2.0.4}/tests/fixtures/legacy_snippets/strictvisibility.pas +0 -0
  93. {python_delphi_lsp-2.0.3 → python_delphi_lsp-2.0.4}/tests/fixtures/legacy_snippets/tryexcept.pas +0 -0
  94. {python_delphi_lsp-2.0.3 → python_delphi_lsp-2.0.4}/tests/fixtures/legacy_snippets/umlauts.pas +0 -0
  95. {python_delphi_lsp-2.0.3 → python_delphi_lsp-2.0.4}/tests/fixtures/legacy_snippets/whitespacearoundifdefcondition.pas +0 -0
  96. {python_delphi_lsp-2.0.3 → python_delphi_lsp-2.0.4}/tests/fixtures/library_demo.dpr +0 -0
  97. {python_delphi_lsp-2.0.3 → python_delphi_lsp-2.0.4}/tests/fixtures/package_demo.dpk +0 -0
  98. {python_delphi_lsp-2.0.3 → python_delphi_lsp-2.0.4}/tests/fixtures/program_demo.dpr +0 -0
  99. {python_delphi_lsp-2.0.3 → python_delphi_lsp-2.0.4}/tests/fixtures/unit_advanced.pas +0 -0
  100. {python_delphi_lsp-2.0.3 → python_delphi_lsp-2.0.4}/tests/fixtures/unit_attributes.pas +0 -0
  101. {python_delphi_lsp-2.0.3 → python_delphi_lsp-2.0.4}/tests/fixtures/unit_basic.pas +0 -0
  102. {python_delphi_lsp-2.0.3 → python_delphi_lsp-2.0.4}/tests/fixtures/unit_consumer.pas +0 -0
  103. {python_delphi_lsp-2.0.3 → python_delphi_lsp-2.0.4}/tests/fixtures/unit_generics.pas +0 -0
  104. {python_delphi_lsp-2.0.3 → python_delphi_lsp-2.0.4}/tests/fixtures/unit_inheritance.pas +0 -0
  105. {python_delphi_lsp-2.0.3 → python_delphi_lsp-2.0.4}/tests/fixtures/unit_math.pas +0 -0
  106. {python_delphi_lsp-2.0.3 → python_delphi_lsp-2.0.4}/tests/fixtures/unit_properties.pas +0 -0
  107. {python_delphi_lsp-2.0.3 → python_delphi_lsp-2.0.4}/tests/fixtures/unit_sections.pas +0 -0
  108. {python_delphi_lsp-2.0.3 → python_delphi_lsp-2.0.4}/tests/fixtures/unit_statements.pas +0 -0
  109. {python_delphi_lsp-2.0.3 → python_delphi_lsp-2.0.4}/tests/fixtures/unit_types.pas +0 -0
  110. {python_delphi_lsp-2.0.3 → python_delphi_lsp-2.0.4}/tests/fixtures/unit_unresolved.pas +0 -0
  111. {python_delphi_lsp-2.0.3 → python_delphi_lsp-2.0.4}/tests/fixtures/unit_with.pas +0 -0
  112. {python_delphi_lsp-2.0.3 → python_delphi_lsp-2.0.4}/tests/test_agent_context.py +0 -0
  113. {python_delphi_lsp-2.0.3 → python_delphi_lsp-2.0.4}/tests/test_agent_metrics.py +0 -0
  114. {python_delphi_lsp-2.0.3 → python_delphi_lsp-2.0.4}/tests/test_agent_protocol.py +0 -0
  115. {python_delphi_lsp-2.0.3 → python_delphi_lsp-2.0.4}/tests/test_agent_relations.py +0 -0
  116. {python_delphi_lsp-2.0.3 → python_delphi_lsp-2.0.4}/tests/test_agent_worker.py +0 -0
  117. {python_delphi_lsp-2.0.3 → python_delphi_lsp-2.0.4}/tests/test_agent_workspace.py +0 -0
  118. {python_delphi_lsp-2.0.3 → python_delphi_lsp-2.0.4}/tests/test_bootstrap_vllm_codebase_skill.py +0 -0
  119. {python_delphi_lsp-2.0.3 → python_delphi_lsp-2.0.4}/tests/test_bootstrap_vllm_opencode.py +0 -0
  120. {python_delphi_lsp-2.0.3 → python_delphi_lsp-2.0.4}/tests/test_diagnostics.py +0 -0
  121. {python_delphi_lsp-2.0.3 → python_delphi_lsp-2.0.4}/tests/test_github_performance_corpus.py +0 -0
  122. {python_delphi_lsp-2.0.3 → python_delphi_lsp-2.0.4}/tests/test_language_feature_matrix.py +0 -0
  123. {python_delphi_lsp-2.0.3 → python_delphi_lsp-2.0.4}/tests/test_legacy_snippets.py +0 -0
  124. {python_delphi_lsp-2.0.3 → python_delphi_lsp-2.0.4}/tests/test_lsp_features.py +0 -0
  125. {python_delphi_lsp-2.0.3 → python_delphi_lsp-2.0.4}/tests/test_metrics.py +0 -0
  126. {python_delphi_lsp-2.0.3 → python_delphi_lsp-2.0.4}/tests/test_opencode_config.py +0 -0
  127. {python_delphi_lsp-2.0.3 → python_delphi_lsp-2.0.4}/tests/test_opencode_probe.py +0 -0
  128. {python_delphi_lsp-2.0.3 → python_delphi_lsp-2.0.4}/tests/test_opencode_request_payloads.py +0 -0
  129. {python_delphi_lsp-2.0.3 → python_delphi_lsp-2.0.4}/tests/test_openrouter_github_e2e.py +0 -0
  130. {python_delphi_lsp-2.0.3 → python_delphi_lsp-2.0.4}/tests/test_ornith_cache_prepare.py +0 -0
  131. {python_delphi_lsp-2.0.3 → python_delphi_lsp-2.0.4}/tests/test_parser.py +0 -0
  132. {python_delphi_lsp-2.0.3 → python_delphi_lsp-2.0.4}/tests/test_preprocessor.py +0 -0
  133. {python_delphi_lsp-2.0.3 → python_delphi_lsp-2.0.4}/tests/test_project_indexer.py +0 -0
  134. {python_delphi_lsp-2.0.3 → python_delphi_lsp-2.0.4}/tests/test_release_evidence.py +0 -0
  135. {python_delphi_lsp-2.0.3 → python_delphi_lsp-2.0.4}/tests/test_semantic.py +0 -0
  136. {python_delphi_lsp-2.0.3 → python_delphi_lsp-2.0.4}/tests/test_vllm_start_script.py +0 -0
  137. {python_delphi_lsp-2.0.3 → python_delphi_lsp-2.0.4}/tests/test_workspace.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: python-delphi-lsp
3
- Version: 2.0.3
3
+ Version: 2.0.4
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.3 is authored by Dark Light and supports Windows, macOS, and Linux.
44
+ Version 2.0.4 is authored by Dark Light and supports Windows, macOS, and Linux.
45
45
 
46
46
  ## Install and quick start
47
47
 
@@ -82,6 +82,19 @@ project = ProjectIndexer(
82
82
  print(project.parsed_units)
83
83
  ```
84
84
 
85
+ Long-running discovery and indexing accept a keyword-only `on_progress`
86
+ callback. It receives an immutable `ProgressEvent` with package-controlled
87
+ phase, path, and monotonic counters; callback exceptions are not suppressed.
88
+
89
+ ```python
90
+ from delphi_lsp import ProjectIndexer, ProgressEvent
91
+
92
+ def report(event: ProgressEvent) -> None:
93
+ print(event.phase, event.files_completed, event.path)
94
+
95
+ ProjectIndexer(on_progress=report).index("Main.dpr")
96
+ ```
97
+
85
98
  ## Architecture metrics
86
99
 
87
100
  The public metrics API analyzes a single unit or aggregates a complete project:
@@ -2,7 +2,7 @@
2
2
 
3
3
  `python-delphi-lsp` parses Delphi/Object Pascal, builds semantic and project
4
4
  indexes, serves LSP, and provides bounded codebase navigation for agents.
5
- Version 2.0.3 is authored by Dark Light and supports Windows, macOS, and Linux.
5
+ Version 2.0.4 is authored by Dark Light and supports Windows, macOS, and Linux.
6
6
 
7
7
  ## Install and quick start
8
8
 
@@ -43,6 +43,19 @@ project = ProjectIndexer(
43
43
  print(project.parsed_units)
44
44
  ```
45
45
 
46
+ Long-running discovery and indexing accept a keyword-only `on_progress`
47
+ callback. It receives an immutable `ProgressEvent` with package-controlled
48
+ phase, path, and monotonic counters; callback exceptions are not suppressed.
49
+
50
+ ```python
51
+ from delphi_lsp import ProjectIndexer, ProgressEvent
52
+
53
+ def report(event: ProgressEvent) -> None:
54
+ print(event.phase, event.files_completed, event.path)
55
+
56
+ ProjectIndexer(on_progress=report).index("Main.dpr")
57
+ ```
58
+
46
59
  ## Architecture metrics
47
60
 
48
61
  The public metrics API analyzes a single unit or aggregates a complete project:
@@ -48,6 +48,7 @@ from .semantic_builder import SemanticBuilder, SemanticModel, SemanticProblem
48
48
  from .workspace import WorkspaceSemanticResult, build_workspace_semantics
49
49
  from .lsp_server import LspWorkspaceState, create_server
50
50
  from .project_discovery import DelphiProjectDiscovery, DiscoveryProblem, discover_delphi_project
51
+ from .progress import ProgressCallback, ProgressEvent
51
52
  from .project_indexer import (
52
53
  GetUnitSyntaxHook,
53
54
  IncludeFileInfo,
@@ -136,6 +137,8 @@ __all__ = [
136
137
  'DelphiProjectDiscovery',
137
138
  'DiscoveryProblem',
138
139
  'discover_delphi_project',
140
+ 'ProgressCallback',
141
+ 'ProgressEvent',
139
142
  'ProjectIndexer',
140
143
  'ProjectIndexResult',
141
144
  'ProjectProblemType',
@@ -0,0 +1 @@
1
+ __version__ = "2.0.4"
@@ -695,7 +695,14 @@ def _build_registry(workspace: AgentWorkspace, project_id: str, revision: str) -
695
695
  include_paths=workspace.include_paths,
696
696
  )
697
697
  sources[source_path] = document
698
- model = build_outline_semantic_model(text, str(source_path))
698
+ if workspace.defines:
699
+ model = build_outline_semantic_model(
700
+ text,
701
+ str(source_path),
702
+ defines=workspace.defines,
703
+ )
704
+ else:
705
+ model = build_outline_semantic_model(text, str(source_path))
699
706
  unit_symbols = _collect_raw_symbols(model.unit_scope, unit, source_path, document)
700
707
  raw_symbols.extend(_exclude_routine_locals(unit_symbols, document))
701
708
 
@@ -1,14 +1,15 @@
1
1
  from __future__ import annotations
2
2
 
3
- from dataclasses import dataclass
3
+ from dataclasses import dataclass, replace
4
4
  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, outline_source
8
+ from .lsp_server import build_outline_semantic_model
9
9
  from .metrics import analyze_project
10
10
  from .project_discovery import DelphiProjectDiscovery, discover_delphi_project
11
11
  from .project_indexer import ProjectIndexResult, ProjectIndexer
12
+ from .progress import ProgressCallback, ProgressEvent
12
13
  from .semantic import Scope, SourceRange, Symbol, SymbolIndex, SymbolKind
13
14
  from .semantic_builder import SemanticModel
14
15
  from .source_reader import read_source_text
@@ -28,10 +29,14 @@ def build_codebase_index(
28
29
  *,
29
30
  project_file: str | Path | None = None,
30
31
  index_projects: bool = False,
32
+ on_progress: ProgressCallback | None = None,
31
33
  ) -> CodebaseIndex:
32
- discovery = discover_delphi_project(root, project_file=project_file)
34
+ progress = _MonotonicProgress(on_progress)
35
+ discovery = discover_delphi_project(root, project_file=project_file, on_progress=progress)
33
36
  models: dict[str, SemanticModel] = {}
34
- for source in discovery.source_files:
37
+ lines_processed = 0
38
+ symbols_discovered = 0
39
+ for completed, source in enumerate(discovery.source_files, start=1):
35
40
  path = Path(source)
36
41
  if path.suffix.casefold() not in {".pas", ".dpr", ".dpk", ".inc"}:
37
42
  continue
@@ -39,7 +44,25 @@ def build_codebase_index(
39
44
  text = read_source_text(path)
40
45
  except (OSError, UnicodeError):
41
46
  continue
42
- models[source] = build_outline_semantic_model(outline_source(text), source)
47
+ model = build_outline_semantic_model(
48
+ text,
49
+ source,
50
+ defines=discovery.defines,
51
+ )
52
+ models[source] = model
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())
55
+ _emit_progress(
56
+ progress,
57
+ "outline",
58
+ source,
59
+ len(discovery.source_files),
60
+ completed,
61
+ len(discovery.source_files),
62
+ "source outlined",
63
+ lines_processed=lines_processed,
64
+ symbols_discovered=symbols_discovered,
65
+ )
43
66
 
44
67
  symbol_index = SymbolIndex()
45
68
  for model in models.values():
@@ -47,6 +70,18 @@ def build_codebase_index(
47
70
  for model in models.values():
48
71
  model.index = symbol_index
49
72
 
73
+ _emit_progress(
74
+ progress,
75
+ "relations",
76
+ str(Path(root).expanduser().resolve()),
77
+ len(discovery.source_files),
78
+ len(discovery.source_files),
79
+ len(discovery.source_files),
80
+ "semantic relations indexed",
81
+ lines_processed=lines_processed,
82
+ symbols_discovered=sum(len(items) for items in symbol_index.name_index.values()),
83
+ )
84
+
50
85
  project_results: dict[str, ProjectIndexResult] = {}
51
86
  if index_projects:
52
87
  for project in discovery.project_files:
@@ -54,16 +89,90 @@ def build_codebase_index(
54
89
  search_paths=discovery.search_paths,
55
90
  include_paths=discovery.include_paths,
56
91
  defines=discovery.defines,
92
+ on_progress=progress,
57
93
  )
58
94
  project_results[project] = indexer.index(project)
59
95
 
60
- return CodebaseIndex(
96
+ index = CodebaseIndex(
61
97
  root=str(Path(root).expanduser().resolve()),
62
98
  discovery=discovery,
63
99
  models=models,
64
100
  symbol_index=symbol_index,
65
101
  project_results=project_results,
66
102
  )
103
+ _emit_progress(
104
+ progress,
105
+ "complete",
106
+ str(Path(root).expanduser().resolve()),
107
+ len(discovery.source_files),
108
+ len(discovery.source_files),
109
+ len(discovery.source_files),
110
+ "codebase index complete",
111
+ lines_processed=lines_processed,
112
+ symbols_discovered=sum(len(items) for items in symbol_index.name_index.values()),
113
+ )
114
+ return index
115
+
116
+
117
+ def _emit_progress(
118
+ callback: ProgressCallback | None,
119
+ phase: str,
120
+ path: str,
121
+ files_discovered: int,
122
+ files_completed: int,
123
+ files_total: int | None,
124
+ detail: str,
125
+ *,
126
+ lines_processed: int = 0,
127
+ symbols_discovered: int = 0,
128
+ ) -> None:
129
+ if callback is not None:
130
+ callback(
131
+ ProgressEvent(
132
+ phase,
133
+ "delphi",
134
+ path,
135
+ files_discovered,
136
+ files_completed,
137
+ files_total,
138
+ lines_processed,
139
+ symbols_discovered,
140
+ 0,
141
+ detail,
142
+ )
143
+ )
144
+
145
+
146
+ class _MonotonicProgress:
147
+ def __init__(self, callback: ProgressCallback | None) -> None:
148
+ self._callback = callback
149
+ self._files_discovered = 0
150
+ self._files_completed = 0
151
+ self._files_total: int | None = None
152
+ self._lines_processed = 0
153
+ self._symbols_discovered = 0
154
+ self._cached_files = 0
155
+
156
+ def __call__(self, event: ProgressEvent) -> None:
157
+ self._files_discovered = max(self._files_discovered, event.files_discovered)
158
+ self._files_completed = max(self._files_completed, event.files_completed)
159
+ if event.files_total is not None:
160
+ self._files_total = max(self._files_total or 0, event.files_total)
161
+ self._lines_processed = max(self._lines_processed, event.lines_processed)
162
+ self._symbols_discovered = max(self._symbols_discovered, event.symbols_discovered)
163
+ self._cached_files = max(self._cached_files, event.cached_files)
164
+ if self._callback is not None:
165
+ self._callback(
166
+ replace(
167
+ event,
168
+ files_discovered=self._files_discovered,
169
+ files_completed=self._files_completed,
170
+ files_total=self._files_total,
171
+ lines_processed=self._lines_processed,
172
+ symbols_discovered=self._symbols_discovered,
173
+ cached_files=self._cached_files,
174
+ )
175
+ )
67
176
 
68
177
 
69
178
  def render_layer(
@@ -7,6 +7,7 @@ from urllib.parse import unquote, urlparse
7
7
  import os
8
8
  import re
9
9
 
10
+ from .preprocessor import Preprocessor
10
11
  from .semantic import (
11
12
  ReferenceKind,
12
13
  Scope,
@@ -249,7 +250,11 @@ class LspWorkspaceState:
249
250
  text = read_source_text(path)
250
251
  except (OSError, UnicodeError):
251
252
  return None
252
- return build_outline_semantic_model(text, file_name)
253
+ return build_outline_semantic_model(
254
+ text,
255
+ file_name,
256
+ defines=self.config.defines,
257
+ )
253
258
 
254
259
  def full_semantic_for_uri(self, uri: str) -> Optional[SemanticModel]:
255
260
  file_name = uri_to_path(uri)
@@ -267,7 +272,11 @@ class LspWorkspaceState:
267
272
  text = self.text_for_uri(uri)
268
273
  if text is None:
269
274
  return None
270
- return build_outline_semantic_model(text, uri_to_path(uri))
275
+ return build_outline_semantic_model(
276
+ text,
277
+ uri_to_path(uri),
278
+ defines=self.config.defines,
279
+ )
271
280
 
272
281
  def text_for_uri(self, uri: str) -> Optional[str]:
273
282
  doc = self.documents.get(uri)
@@ -293,7 +302,14 @@ class LspWorkspaceState:
293
302
  ) -> Optional[WorkspaceSemanticResult]:
294
303
  if not sources:
295
304
  return None
296
- models = {file_name: build_outline_semantic_model(text, file_name) for file_name, text in sources.items()}
305
+ models = {
306
+ file_name: build_outline_semantic_model(
307
+ text,
308
+ file_name,
309
+ defines=self.config.defines,
310
+ )
311
+ for file_name, text in sources.items()
312
+ }
297
313
  index = SymbolIndex()
298
314
  for model in models.values():
299
315
  index.register_unit(model.unit_scope.name, model.unit_scope)
@@ -343,8 +359,51 @@ def uri_to_path(uri: str) -> str:
343
359
  return path or uri
344
360
 
345
361
 
346
- def outline_source(text: str) -> str:
347
- return _blank_compound_statement_bodies(text)
362
+ def outline_source(text: str, *, defines: Iterable[str] | None = None) -> str:
363
+ scan_text = (
364
+ text
365
+ if defines is None
366
+ else _conditional_outline_scan_text(text, defines=defines)
367
+ )
368
+ return _blank_compound_statement_bodies(
369
+ text,
370
+ scan_text=scan_text,
371
+ )
372
+
373
+
374
+ def _conditional_outline_scan_text(
375
+ text: str,
376
+ *,
377
+ defines: Iterable[str],
378
+ ) -> str:
379
+ if '{$' not in text and '(*$' not in text:
380
+ return text
381
+ selected = Preprocessor(
382
+ defines=defines,
383
+ include_loader=lambda _name, _current: None,
384
+ preserve_active_directives=True,
385
+ ).process(text, '<outline>').text
386
+ original_lines = text.splitlines(keepends=True)
387
+ selected_lines = selected.splitlines(keepends=True)
388
+ if len(original_lines) != len(selected_lines):
389
+ return text
390
+ restored: list[str] = []
391
+ for original_line, selected_line in zip(original_lines, selected_lines, strict=True):
392
+ original_body, original_ending = _split_line_ending(original_line)
393
+ selected_body, _selected_ending = _split_line_ending(selected_line)
394
+ if len(original_body) != len(selected_body):
395
+ return text
396
+ restored.append(selected_body + original_ending)
397
+ result = ''.join(restored)
398
+ return result if len(result) == len(text) else text
399
+
400
+
401
+ def _split_line_ending(line: str) -> tuple[str, str]:
402
+ if line.endswith('\r\n'):
403
+ return line[:-2], '\r\n'
404
+ if line.endswith(('\r', '\n')):
405
+ return line[:-1], line[-1]
406
+ return line, ''
348
407
 
349
408
 
350
409
  def multiline_string_block_end(text: str, start: int) -> int | None:
@@ -406,11 +465,29 @@ _CLASS_MEMBER_BOUNDARIES = frozenset(
406
465
  _GENERIC_ROUTINE_HEADINGS = frozenset(
407
466
  {'constructor', 'destructor', 'function', 'operator', 'procedure'}
408
467
  )
468
+ _CONDITIONAL_DIRECTIVE_OPENERS = frozenset({'if', 'ifdef', 'ifndef', 'ifopt'})
469
+ _CONDITIONAL_DIRECTIVE_ALTERNATIVES = frozenset({'else', 'elseif'})
470
+ _CONDITIONAL_DIRECTIVE_CLOSERS = frozenset({'endif', 'ifend'})
471
+
472
+
473
+ @dataclass
474
+ class _ConditionalOutlineFrame:
475
+ base_stack: tuple[str, ...]
476
+ branch_stacks: list[tuple[str, ...]] = field(default_factory=list)
477
+ has_alternative: bool = False
409
478
 
410
479
 
411
- def _blank_compound_statement_bodies(text: str) -> str:
412
- chars = list(text)
480
+ def _blank_compound_statement_bodies(
481
+ text: str,
482
+ *,
483
+ scan_text: str | None = None,
484
+ ) -> str:
485
+ scanned = scan_text if scan_text is not None else text
486
+ if len(scanned) != len(text):
487
+ return text
488
+ chars = list(scanned)
413
489
  end_stack: list[str] = []
490
+ conditional_frames: list[_ConditionalOutlineFrame] = []
414
491
  body_start: int | None = None
415
492
  previous_token: str | None = None
416
493
  previous_token_is_identifier = False
@@ -421,26 +498,48 @@ def _blank_compound_statement_bodies(text: str) -> str:
421
498
  routine_heading_active = False
422
499
  type_constructors: dict[tuple[int, int], str] = {}
423
500
  i = 0
424
- n = len(text)
501
+ n = len(scanned)
425
502
  while i < n:
426
- ch = text[i]
427
- nxt = text[i + 1] if i + 1 < n else ''
428
-
429
- if end_stack and (
430
- (ch == '{' and nxt == '$')
431
- or (ch == '(' and nxt == '*' and i + 2 < n and text[i + 2] == '$')
432
- ):
433
- # Raw conditional branches can disagree on END nesting, so keep the file all-or-safe.
434
- return text
503
+ ch = scanned[i]
504
+ nxt = scanned[i + 1] if i + 1 < n else ''
505
+
506
+ directive = _compiler_directive_at(scanned, i)
507
+ if directive is not None and (end_stack or conditional_frames):
508
+ directive_name, directive_end = directive
509
+ if directive_name in _CONDITIONAL_DIRECTIVE_OPENERS:
510
+ conditional_frames.append(
511
+ _ConditionalOutlineFrame(base_stack=tuple(end_stack))
512
+ )
513
+ elif directive_name in _CONDITIONAL_DIRECTIVE_ALTERNATIVES:
514
+ if not conditional_frames:
515
+ return scanned
516
+ frame = conditional_frames[-1]
517
+ frame.branch_stacks.append(tuple(end_stack))
518
+ frame.has_alternative = True
519
+ end_stack[:] = frame.base_stack
520
+ elif directive_name in _CONDITIONAL_DIRECTIVE_CLOSERS:
521
+ if not conditional_frames:
522
+ return scanned
523
+ frame = conditional_frames.pop()
524
+ frame.branch_stacks.append(tuple(end_stack))
525
+ if not frame.has_alternative:
526
+ frame.branch_stacks.append(frame.base_stack)
527
+ merged_stack = _merge_conditional_outline_stacks(frame)
528
+ if merged_stack is None:
529
+ merged_stack = _common_conditional_outline_stack(frame)
530
+ body_start = None
531
+ end_stack[:] = merged_stack
532
+ i = directive_end
533
+ continue
435
534
 
436
535
  if ch.isspace():
437
536
  i += 1
438
- while i < n and text[i].isspace():
537
+ while i < n and scanned[i].isspace():
439
538
  i += 1
440
539
  continue
441
540
 
442
541
  if ch == "'":
443
- block_end = multiline_string_block_end(text, i)
542
+ block_end = multiline_string_block_end(scanned, i)
444
543
  if block_end is not None:
445
544
  i = block_end
446
545
  previous_token = 'literal'
@@ -449,8 +548,8 @@ def _blank_compound_statement_bodies(text: str) -> str:
449
548
  continue
450
549
  i += 1
451
550
  while i < n:
452
- if text[i] == "'":
453
- if i + 1 < n and text[i + 1] == "'":
551
+ if scanned[i] == "'":
552
+ if i + 1 < n and scanned[i + 1] == "'":
454
553
  i += 2
455
554
  continue
456
555
  i += 1
@@ -463,13 +562,13 @@ def _blank_compound_statement_bodies(text: str) -> str:
463
562
 
464
563
  if ch == '/' and nxt == '/':
465
564
  i += 2
466
- while i < n and text[i] not in {'\n', '\r'}:
565
+ while i < n and scanned[i] not in {'\n', '\r'}:
467
566
  i += 1
468
567
  continue
469
568
 
470
569
  if ch == '{':
471
570
  i += 1
472
- while i < n and text[i] != '}':
571
+ while i < n and scanned[i] != '}':
473
572
  i += 1
474
573
  if i < n:
475
574
  i += 1
@@ -477,7 +576,7 @@ def _blank_compound_statement_bodies(text: str) -> str:
477
576
 
478
577
  if ch == '(' and nxt == '*':
479
578
  i += 2
480
- while i + 1 < n and not (text[i] == '*' and text[i + 1] == ')'):
579
+ while i + 1 < n and not (scanned[i] == '*' and scanned[i + 1] == ')'):
481
580
  i += 1
482
581
  if i + 1 < n:
483
582
  i += 2
@@ -486,9 +585,9 @@ def _blank_compound_statement_bodies(text: str) -> str:
486
585
  if ch == '&' and (nxt.isalpha() or nxt == '_'):
487
586
  start = i
488
587
  i += 2
489
- while i < n and (text[i].isalnum() or text[i] == '_'):
588
+ while i < n and (scanned[i].isalnum() or scanned[i] == '_'):
490
589
  i += 1
491
- previous_token = text[start:i].casefold()
590
+ previous_token = scanned[start:i].casefold()
492
591
  previous_token_is_identifier = True
493
592
  generic_owner_ready = False
494
593
  continue
@@ -496,9 +595,9 @@ def _blank_compound_statement_bodies(text: str) -> str:
496
595
  if ch.isalpha() or ch == '_':
497
596
  start = i
498
597
  i += 1
499
- while i < n and (text[i].isalnum() or text[i] == '_'):
598
+ while i < n and (scanned[i].isalnum() or scanned[i] == '_'):
500
599
  i += 1
501
- word = text[start:i].casefold()
600
+ word = scanned[start:i].casefold()
502
601
  generic_owner_ready = False
503
602
  opens_construct: bool | None = False
504
603
  if not end_stack:
@@ -517,14 +616,14 @@ def _blank_compound_statement_bodies(text: str) -> str:
517
616
  else:
518
617
  opens_construct = _opens_end_terminated_construct(
519
618
  word,
520
- text=text,
619
+ text=scanned,
521
620
  word_end=i,
522
621
  previous_token=previous_token,
523
622
  end_stack=end_stack,
524
623
  type_constructor=type_constructors.get((paren_depth, bracket_depth)),
525
624
  )
526
625
  if opens_construct is None:
527
- return text
626
+ return scanned
528
627
  if opens_construct:
529
628
  end_stack.append(word)
530
629
  if word == 'class' and opens_construct:
@@ -546,7 +645,7 @@ def _blank_compound_statement_bodies(text: str) -> str:
546
645
  if ch == '<' and nxt not in {'=', '>'}:
547
646
  if angle_depth > 0:
548
647
  if not previous_token_is_identifier:
549
- return text
648
+ return scanned
550
649
  angle_depth += 1
551
650
  elif generic_owner_ready or (
552
651
  routine_heading_active
@@ -582,7 +681,64 @@ def _blank_compound_statement_bodies(text: str) -> str:
582
681
  previous_token = ch
583
682
  previous_token_is_identifier = False
584
683
  i += 1
585
- return text if angle_depth > 0 else ''.join(chars)
684
+ return scanned if angle_depth > 0 or conditional_frames else ''.join(chars)
685
+
686
+
687
+ def _compiler_directive_at(text: str, start: int) -> tuple[str, int] | None:
688
+ if text.startswith('{$', start):
689
+ content_start = start + 2
690
+ close = text.find('}', content_start)
691
+ if close < 0:
692
+ return None
693
+ end = close + 1
694
+ elif text.startswith('(*$', start):
695
+ content_start = start + 3
696
+ close = text.find('*)', content_start)
697
+ if close < 0:
698
+ return None
699
+ end = close + 2
700
+ else:
701
+ return None
702
+ match = re.match(r'\s*([A-Za-z]+)', text[content_start:close])
703
+ return ((match.group(1).casefold() if match else ''), end)
704
+
705
+
706
+ def _merge_conditional_outline_stacks(
707
+ frame: _ConditionalOutlineFrame,
708
+ ) -> list[str] | None:
709
+ base = frame.base_stack
710
+ branches = frame.branch_stacks
711
+ if not branches or any(
712
+ len(branch) < len(base) or branch[:len(base)] != base
713
+ for branch in branches
714
+ ):
715
+ return None
716
+ tails = [branch[len(base):] for branch in branches]
717
+ shapes = [tuple(_outline_block_shape(item) for item in tail) for tail in tails]
718
+ if any(shape != shapes[0] for shape in shapes[1:]):
719
+ return None
720
+ merged = list(base)
721
+ for values in zip(*tails, strict=True):
722
+ merged.append(values[0] if all(value == values[0] for value in values) else 'begin')
723
+ return merged
724
+
725
+
726
+ def _outline_block_shape(value: str) -> str:
727
+ return 'statement' if value in _END_TERMINATED_STATEMENTS else value
728
+
729
+
730
+ def _common_conditional_outline_stack(
731
+ frame: _ConditionalOutlineFrame,
732
+ ) -> list[str]:
733
+ common: list[str] = []
734
+ for values in zip(*frame.branch_stacks):
735
+ shapes = {_outline_block_shape(value) for value in values}
736
+ if len(shapes) != 1:
737
+ break
738
+ common.append(
739
+ values[0] if all(value == values[0] for value in values) else 'begin'
740
+ )
741
+ return common
586
742
 
587
743
 
588
744
  def _opens_end_terminated_construct(
@@ -684,7 +840,13 @@ _TYPE_REF_STOP_RE = re.compile(
684
840
  )
685
841
 
686
842
 
687
- def build_outline_semantic_model(text: str, file_name: str) -> SemanticModel:
843
+ def build_outline_semantic_model(
844
+ text: str,
845
+ file_name: str,
846
+ *,
847
+ defines: Iterable[str] = (),
848
+ ) -> SemanticModel:
849
+ text = outline_source(text, defines=defines)
688
850
  unit_name = _outline_unit_name(text, file_name)
689
851
  unit_scope = Scope(kind=ScopeKind.UNIT, name=unit_name)
690
852
  unit_range = SourceRange(file_name, 1, 1, 1, max(2, len(unit_name) + 1))