python-delphi-lsp 2.0.2__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 (138) hide show
  1. python_delphi_lsp-2.0.4/.github/workflows/ci.yml +42 -0
  2. {python_delphi_lsp-2.0.2 → python_delphi_lsp-2.0.4}/MANIFEST.in +6 -0
  3. {python_delphi_lsp-2.0.2/python_delphi_lsp.egg-info → python_delphi_lsp-2.0.4}/PKG-INFO +33 -4
  4. {python_delphi_lsp-2.0.2 → python_delphi_lsp-2.0.4}/README.md +32 -3
  5. {python_delphi_lsp-2.0.2 → python_delphi_lsp-2.0.4}/delphi_lsp/__init__.py +3 -0
  6. python_delphi_lsp-2.0.4/delphi_lsp/_version.py +1 -0
  7. {python_delphi_lsp-2.0.2 → python_delphi_lsp-2.0.4}/delphi_lsp/agent_context.py +171 -63
  8. {python_delphi_lsp-2.0.2 → python_delphi_lsp-2.0.4}/delphi_lsp/agent_layers.py +115 -6
  9. {python_delphi_lsp-2.0.2 → python_delphi_lsp-2.0.4}/delphi_lsp/agent_templates.py +18 -15
  10. {python_delphi_lsp-2.0.2 → python_delphi_lsp-2.0.4}/delphi_lsp/agent_workspace.py +7 -2
  11. {python_delphi_lsp-2.0.2 → python_delphi_lsp-2.0.4}/delphi_lsp/lsp_server.py +195 -33
  12. {python_delphi_lsp-2.0.2 → python_delphi_lsp-2.0.4}/delphi_lsp/preprocessor.py +32 -10
  13. python_delphi_lsp-2.0.4/delphi_lsp/progress.py +26 -0
  14. {python_delphi_lsp-2.0.2 → python_delphi_lsp-2.0.4}/delphi_lsp/project_discovery.py +125 -16
  15. {python_delphi_lsp-2.0.2 → python_delphi_lsp-2.0.4}/delphi_lsp/project_indexer.py +43 -1
  16. {python_delphi_lsp-2.0.2 → python_delphi_lsp-2.0.4}/pyproject.toml +1 -1
  17. {python_delphi_lsp-2.0.2 → python_delphi_lsp-2.0.4/python_delphi_lsp.egg-info}/PKG-INFO +33 -4
  18. {python_delphi_lsp-2.0.2 → python_delphi_lsp-2.0.4}/python_delphi_lsp.egg-info/SOURCES.txt +10 -0
  19. python_delphi_lsp-2.0.4/scripts/benchmark_github_corpus.py +256 -0
  20. python_delphi_lsp-2.0.4/scripts/build_github_performance_corpus.py +521 -0
  21. python_delphi_lsp-2.0.4/scripts/generate_progress_pdf.py +518 -0
  22. python_delphi_lsp-2.0.4/scripts/run_opencode_lsp_probe.py +816 -0
  23. python_delphi_lsp-2.0.4/scripts/run_openrouter_github_e2e.py +1393 -0
  24. python_delphi_lsp-2.0.4/tests/corpora.performance.lock.json +66 -0
  25. {python_delphi_lsp-2.0.2 → python_delphi_lsp-2.0.4}/tests/test_agent_codebase.py +113 -1
  26. {python_delphi_lsp-2.0.2 → python_delphi_lsp-2.0.4}/tests/test_agent_context.py +176 -0
  27. python_delphi_lsp-2.0.4/tests/test_github_performance_corpus.py +240 -0
  28. {python_delphi_lsp-2.0.2 → python_delphi_lsp-2.0.4}/tests/test_lsp_support.py +243 -18
  29. {python_delphi_lsp-2.0.2 → python_delphi_lsp-2.0.4}/tests/test_opencode_probe.py +475 -25
  30. python_delphi_lsp-2.0.4/tests/test_openrouter_github_e2e.py +1817 -0
  31. {python_delphi_lsp-2.0.2 → python_delphi_lsp-2.0.4}/tests/test_package_metadata.py +8 -2
  32. python_delphi_lsp-2.0.4/tests/test_progress.py +58 -0
  33. {python_delphi_lsp-2.0.2 → python_delphi_lsp-2.0.4}/tests/test_project_discovery.py +77 -5
  34. python_delphi_lsp-2.0.2/delphi_lsp/_version.py +0 -1
  35. python_delphi_lsp-2.0.2/scripts/run_opencode_lsp_probe.py +0 -389
  36. {python_delphi_lsp-2.0.2 → python_delphi_lsp-2.0.4}/LICENSE +0 -0
  37. {python_delphi_lsp-2.0.2 → python_delphi_lsp-2.0.4}/delphi_lsp/agent_cli.py +0 -0
  38. {python_delphi_lsp-2.0.2 → python_delphi_lsp-2.0.4}/delphi_lsp/agent_metrics.py +0 -0
  39. {python_delphi_lsp-2.0.2 → python_delphi_lsp-2.0.4}/delphi_lsp/agent_protocol.py +0 -0
  40. {python_delphi_lsp-2.0.2 → python_delphi_lsp-2.0.4}/delphi_lsp/agent_relations.py +0 -0
  41. {python_delphi_lsp-2.0.2 → python_delphi_lsp-2.0.4}/delphi_lsp/binary.py +0 -0
  42. {python_delphi_lsp-2.0.2 → python_delphi_lsp-2.0.4}/delphi_lsp/comment_builder.py +0 -0
  43. {python_delphi_lsp-2.0.2 → python_delphi_lsp-2.0.4}/delphi_lsp/consts.py +0 -0
  44. {python_delphi_lsp-2.0.2 → python_delphi_lsp-2.0.4}/delphi_lsp/grammar.py +0 -0
  45. {python_delphi_lsp-2.0.2 → python_delphi_lsp-2.0.4}/delphi_lsp/lark_builder.py +0 -0
  46. {python_delphi_lsp-2.0.2 → python_delphi_lsp-2.0.4}/delphi_lsp/lark_tokens.py +0 -0
  47. {python_delphi_lsp-2.0.2 → python_delphi_lsp-2.0.4}/delphi_lsp/metrics.py +0 -0
  48. {python_delphi_lsp-2.0.2 → python_delphi_lsp-2.0.4}/delphi_lsp/nodes.py +0 -0
  49. {python_delphi_lsp-2.0.2 → python_delphi_lsp-2.0.4}/delphi_lsp/parser.py +0 -0
  50. {python_delphi_lsp-2.0.2 → python_delphi_lsp-2.0.4}/delphi_lsp/semantic.py +0 -0
  51. {python_delphi_lsp-2.0.2 → python_delphi_lsp-2.0.4}/delphi_lsp/semantic_builder.py +0 -0
  52. {python_delphi_lsp-2.0.2 → python_delphi_lsp-2.0.4}/delphi_lsp/source_reader.py +0 -0
  53. {python_delphi_lsp-2.0.2 → python_delphi_lsp-2.0.4}/delphi_lsp/workspace.py +0 -0
  54. {python_delphi_lsp-2.0.2 → python_delphi_lsp-2.0.4}/delphi_lsp/writer.py +0 -0
  55. {python_delphi_lsp-2.0.2 → python_delphi_lsp-2.0.4}/opencode.json +0 -0
  56. {python_delphi_lsp-2.0.2 → python_delphi_lsp-2.0.4}/python_delphi_lsp.egg-info/dependency_links.txt +0 -0
  57. {python_delphi_lsp-2.0.2 → python_delphi_lsp-2.0.4}/python_delphi_lsp.egg-info/entry_points.txt +0 -0
  58. {python_delphi_lsp-2.0.2 → python_delphi_lsp-2.0.4}/python_delphi_lsp.egg-info/requires.txt +0 -0
  59. {python_delphi_lsp-2.0.2 → python_delphi_lsp-2.0.4}/python_delphi_lsp.egg-info/top_level.txt +0 -0
  60. {python_delphi_lsp-2.0.2 → python_delphi_lsp-2.0.4}/scripts/audit_delphi_language_features.py +0 -0
  61. {python_delphi_lsp-2.0.2 → python_delphi_lsp-2.0.4}/scripts/bootstrap_vllm_codebase_skill_test.py +0 -0
  62. {python_delphi_lsp-2.0.2 → python_delphi_lsp-2.0.4}/scripts/bootstrap_vllm_opencode_test.ps1 +0 -0
  63. {python_delphi_lsp-2.0.2 → python_delphi_lsp-2.0.4}/scripts/bootstrap_vllm_opencode_test.py +0 -0
  64. {python_delphi_lsp-2.0.2 → python_delphi_lsp-2.0.4}/scripts/bootstrap_vllm_opencode_test.sh +0 -0
  65. {python_delphi_lsp-2.0.2 → python_delphi_lsp-2.0.4}/scripts/check_ornith_cache.py +0 -0
  66. {python_delphi_lsp-2.0.2 → python_delphi_lsp-2.0.4}/scripts/generate_release_evidence.py +0 -0
  67. {python_delphi_lsp-2.0.2 → python_delphi_lsp-2.0.4}/scripts/ollama/ornith-lspctx.Modelfile +0 -0
  68. {python_delphi_lsp-2.0.2 → python_delphi_lsp-2.0.4}/scripts/prepare_ornith_cache.py +0 -0
  69. {python_delphi_lsp-2.0.2 → python_delphi_lsp-2.0.4}/scripts/start_ornith_vllm.sh +0 -0
  70. {python_delphi_lsp-2.0.2 → python_delphi_lsp-2.0.4}/scripts/summarize_opencode_request_payloads.py +0 -0
  71. {python_delphi_lsp-2.0.2 → python_delphi_lsp-2.0.4}/setup.cfg +0 -0
  72. {python_delphi_lsp-2.0.2 → python_delphi_lsp-2.0.4}/tests/fixtures/legacy_snippets/DeprecatedOnConst.pas +0 -0
  73. {python_delphi_lsp-2.0.2 → python_delphi_lsp-2.0.4}/tests/fixtures/legacy_snippets/VariantRecordFieldAttributes.pas +0 -0
  74. {python_delphi_lsp-2.0.2 → python_delphi_lsp-2.0.4}/tests/fixtures/legacy_snippets/alignedrecords.pas +0 -0
  75. {python_delphi_lsp-2.0.2 → python_delphi_lsp-2.0.4}/tests/fixtures/legacy_snippets/constset.pas +0 -0
  76. {python_delphi_lsp-2.0.2 → python_delphi_lsp-2.0.4}/tests/fixtures/legacy_snippets/deprecatedtype.pas +0 -0
  77. {python_delphi_lsp-2.0.2 → python_delphi_lsp-2.0.4}/tests/fixtures/legacy_snippets/dottedtypes.pas +0 -0
  78. {python_delphi_lsp-2.0.2 → python_delphi_lsp-2.0.4}/tests/fixtures/legacy_snippets/endtoken.pas +0 -0
  79. {python_delphi_lsp-2.0.2 → python_delphi_lsp-2.0.4}/tests/fixtures/legacy_snippets/experimentals.pas +0 -0
  80. {python_delphi_lsp-2.0.2 → python_delphi_lsp-2.0.4}/tests/fixtures/legacy_snippets/externalfunction.pas +0 -0
  81. {python_delphi_lsp-2.0.2 → python_delphi_lsp-2.0.4}/tests/fixtures/legacy_snippets/finalizationinitializationexports.pas +0 -0
  82. {python_delphi_lsp-2.0.2 → python_delphi_lsp-2.0.4}/tests/fixtures/legacy_snippets/forwardoverloaded.pas +0 -0
  83. {python_delphi_lsp-2.0.2 → python_delphi_lsp-2.0.4}/tests/fixtures/legacy_snippets/forwardwithoutsemicolon.pas +0 -0
  84. {python_delphi_lsp-2.0.2 → python_delphi_lsp-2.0.4}/tests/fixtures/legacy_snippets/genericconstraints.pas +0 -0
  85. {python_delphi_lsp-2.0.2 → python_delphi_lsp-2.0.4}/tests/fixtures/legacy_snippets/genericinterfacemethoddelegation.pas +0 -0
  86. {python_delphi_lsp-2.0.2 → python_delphi_lsp-2.0.4}/tests/fixtures/legacy_snippets/implementsgenerictype.pas +0 -0
  87. {python_delphi_lsp-2.0.2 → python_delphi_lsp-2.0.4}/tests/fixtures/legacy_snippets/include file2.inc +0 -0
  88. {python_delphi_lsp-2.0.2 → python_delphi_lsp-2.0.4}/tests/fixtures/legacy_snippets/includefile.inc +0 -0
  89. {python_delphi_lsp-2.0.2 → python_delphi_lsp-2.0.4}/tests/fixtures/legacy_snippets/includefile.pas +0 -0
  90. {python_delphi_lsp-2.0.2 → python_delphi_lsp-2.0.4}/tests/fixtures/legacy_snippets/managedrecords.pas +0 -0
  91. {python_delphi_lsp-2.0.2 → python_delphi_lsp-2.0.4}/tests/fixtures/legacy_snippets/messagemethod.pas +0 -0
  92. {python_delphi_lsp-2.0.2 → python_delphi_lsp-2.0.4}/tests/fixtures/legacy_snippets/multiline.pas +0 -0
  93. {python_delphi_lsp-2.0.2 → python_delphi_lsp-2.0.4}/tests/fixtures/legacy_snippets/nonalignedrecords.pas +0 -0
  94. {python_delphi_lsp-2.0.2 → python_delphi_lsp-2.0.4}/tests/fixtures/legacy_snippets/numbers.pas +0 -0
  95. {python_delphi_lsp-2.0.2 → python_delphi_lsp-2.0.4}/tests/fixtures/legacy_snippets/pointerchars.pas +0 -0
  96. {python_delphi_lsp-2.0.2 → python_delphi_lsp-2.0.4}/tests/fixtures/legacy_snippets/properties.pas +0 -0
  97. {python_delphi_lsp-2.0.2 → python_delphi_lsp-2.0.4}/tests/fixtures/legacy_snippets/strictvisibility.pas +0 -0
  98. {python_delphi_lsp-2.0.2 → python_delphi_lsp-2.0.4}/tests/fixtures/legacy_snippets/tryexcept.pas +0 -0
  99. {python_delphi_lsp-2.0.2 → python_delphi_lsp-2.0.4}/tests/fixtures/legacy_snippets/umlauts.pas +0 -0
  100. {python_delphi_lsp-2.0.2 → python_delphi_lsp-2.0.4}/tests/fixtures/legacy_snippets/whitespacearoundifdefcondition.pas +0 -0
  101. {python_delphi_lsp-2.0.2 → python_delphi_lsp-2.0.4}/tests/fixtures/library_demo.dpr +0 -0
  102. {python_delphi_lsp-2.0.2 → python_delphi_lsp-2.0.4}/tests/fixtures/package_demo.dpk +0 -0
  103. {python_delphi_lsp-2.0.2 → python_delphi_lsp-2.0.4}/tests/fixtures/program_demo.dpr +0 -0
  104. {python_delphi_lsp-2.0.2 → python_delphi_lsp-2.0.4}/tests/fixtures/unit_advanced.pas +0 -0
  105. {python_delphi_lsp-2.0.2 → python_delphi_lsp-2.0.4}/tests/fixtures/unit_attributes.pas +0 -0
  106. {python_delphi_lsp-2.0.2 → python_delphi_lsp-2.0.4}/tests/fixtures/unit_basic.pas +0 -0
  107. {python_delphi_lsp-2.0.2 → python_delphi_lsp-2.0.4}/tests/fixtures/unit_consumer.pas +0 -0
  108. {python_delphi_lsp-2.0.2 → python_delphi_lsp-2.0.4}/tests/fixtures/unit_generics.pas +0 -0
  109. {python_delphi_lsp-2.0.2 → python_delphi_lsp-2.0.4}/tests/fixtures/unit_inheritance.pas +0 -0
  110. {python_delphi_lsp-2.0.2 → python_delphi_lsp-2.0.4}/tests/fixtures/unit_math.pas +0 -0
  111. {python_delphi_lsp-2.0.2 → python_delphi_lsp-2.0.4}/tests/fixtures/unit_properties.pas +0 -0
  112. {python_delphi_lsp-2.0.2 → python_delphi_lsp-2.0.4}/tests/fixtures/unit_sections.pas +0 -0
  113. {python_delphi_lsp-2.0.2 → python_delphi_lsp-2.0.4}/tests/fixtures/unit_statements.pas +0 -0
  114. {python_delphi_lsp-2.0.2 → python_delphi_lsp-2.0.4}/tests/fixtures/unit_types.pas +0 -0
  115. {python_delphi_lsp-2.0.2 → python_delphi_lsp-2.0.4}/tests/fixtures/unit_unresolved.pas +0 -0
  116. {python_delphi_lsp-2.0.2 → python_delphi_lsp-2.0.4}/tests/fixtures/unit_with.pas +0 -0
  117. {python_delphi_lsp-2.0.2 → python_delphi_lsp-2.0.4}/tests/test_agent_metrics.py +0 -0
  118. {python_delphi_lsp-2.0.2 → python_delphi_lsp-2.0.4}/tests/test_agent_protocol.py +0 -0
  119. {python_delphi_lsp-2.0.2 → python_delphi_lsp-2.0.4}/tests/test_agent_relations.py +0 -0
  120. {python_delphi_lsp-2.0.2 → python_delphi_lsp-2.0.4}/tests/test_agent_worker.py +0 -0
  121. {python_delphi_lsp-2.0.2 → python_delphi_lsp-2.0.4}/tests/test_agent_workspace.py +0 -0
  122. {python_delphi_lsp-2.0.2 → python_delphi_lsp-2.0.4}/tests/test_bootstrap_vllm_codebase_skill.py +0 -0
  123. {python_delphi_lsp-2.0.2 → python_delphi_lsp-2.0.4}/tests/test_bootstrap_vllm_opencode.py +0 -0
  124. {python_delphi_lsp-2.0.2 → python_delphi_lsp-2.0.4}/tests/test_diagnostics.py +0 -0
  125. {python_delphi_lsp-2.0.2 → python_delphi_lsp-2.0.4}/tests/test_language_feature_matrix.py +0 -0
  126. {python_delphi_lsp-2.0.2 → python_delphi_lsp-2.0.4}/tests/test_legacy_snippets.py +0 -0
  127. {python_delphi_lsp-2.0.2 → python_delphi_lsp-2.0.4}/tests/test_lsp_features.py +0 -0
  128. {python_delphi_lsp-2.0.2 → python_delphi_lsp-2.0.4}/tests/test_metrics.py +0 -0
  129. {python_delphi_lsp-2.0.2 → python_delphi_lsp-2.0.4}/tests/test_opencode_config.py +0 -0
  130. {python_delphi_lsp-2.0.2 → python_delphi_lsp-2.0.4}/tests/test_opencode_request_payloads.py +0 -0
  131. {python_delphi_lsp-2.0.2 → python_delphi_lsp-2.0.4}/tests/test_ornith_cache_prepare.py +0 -0
  132. {python_delphi_lsp-2.0.2 → python_delphi_lsp-2.0.4}/tests/test_parser.py +0 -0
  133. {python_delphi_lsp-2.0.2 → python_delphi_lsp-2.0.4}/tests/test_preprocessor.py +0 -0
  134. {python_delphi_lsp-2.0.2 → python_delphi_lsp-2.0.4}/tests/test_project_indexer.py +0 -0
  135. {python_delphi_lsp-2.0.2 → python_delphi_lsp-2.0.4}/tests/test_release_evidence.py +0 -0
  136. {python_delphi_lsp-2.0.2 → python_delphi_lsp-2.0.4}/tests/test_semantic.py +0 -0
  137. {python_delphi_lsp-2.0.2 → python_delphi_lsp-2.0.4}/tests/test_vllm_start_script.py +0 -0
  138. {python_delphi_lsp-2.0.2 → python_delphi_lsp-2.0.4}/tests/test_workspace.py +0 -0
@@ -0,0 +1,42 @@
1
+ name: CI
2
+
3
+ on:
4
+ push:
5
+ pull_request:
6
+
7
+ jobs:
8
+ test:
9
+ name: ${{ matrix.os }} / Python ${{ matrix.python }}
10
+ runs-on: ${{ matrix.os }}
11
+ strategy:
12
+ fail-fast: false
13
+ matrix:
14
+ os: [ubuntu-latest, macos-latest, windows-latest]
15
+ python: ["3.10", "3.14"]
16
+ steps:
17
+ - uses: actions/checkout@v4
18
+ - uses: actions/setup-python@v5
19
+ with:
20
+ python-version: ${{ matrix.python }}
21
+ cache: pip
22
+ cache-dependency-path: pyproject.toml
23
+ - run: python -m pip install --upgrade pip
24
+ - run: pip install .[test]
25
+ - run: pytest
26
+
27
+ package:
28
+ name: Build and validate distributions
29
+ runs-on: ubuntu-latest
30
+ steps:
31
+ - uses: actions/checkout@v4
32
+ - uses: actions/setup-python@v5
33
+ with:
34
+ python-version: "3.14"
35
+ cache: pip
36
+ cache-dependency-path: pyproject.toml
37
+ - run: python -m pip install --upgrade pip build twine
38
+ - run: python -m build
39
+ - run: python -m twine check dist/*
40
+ - run: python -m venv smoke-venv
41
+ - run: smoke-venv/bin/python -m pip install dist/*.whl
42
+ - run: smoke-venv/bin/python -c "import delphi_lsp; print(delphi_lsp.__name__)"
@@ -1,16 +1,22 @@
1
1
  include LICENSE
2
2
  include README.md
3
3
  include opencode.json
4
+ include .github/workflows/ci.yml
4
5
  include scripts/check_ornith_cache.py
5
6
  include scripts/audit_delphi_language_features.py
7
+ include scripts/build_github_performance_corpus.py
8
+ include scripts/benchmark_github_corpus.py
6
9
  include scripts/bootstrap_vllm_opencode_test.py
7
10
  include scripts/bootstrap_vllm_codebase_skill_test.py
8
11
  include scripts/bootstrap_vllm_opencode_test.ps1
9
12
  include scripts/bootstrap_vllm_opencode_test.sh
10
13
  include scripts/generate_release_evidence.py
14
+ include scripts/generate_progress_pdf.py
11
15
  include scripts/prepare_ornith_cache.py
12
16
  include scripts/run_opencode_lsp_probe.py
17
+ include scripts/run_openrouter_github_e2e.py
13
18
  include scripts/start_ornith_vllm.sh
14
19
  include scripts/summarize_opencode_request_payloads.py
15
20
  include scripts/ollama/ornith-lspctx.Modelfile
21
+ include tests/corpora.performance.lock.json
16
22
  recursive-include tests *.py *.pas *.inc *.dpr *.dpk
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: python-delphi-lsp
3
- Version: 2.0.2
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.2 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:
@@ -254,14 +267,30 @@ A generated OpenCode agent starts with this Markdown frontmatter:
254
267
  ```markdown
255
268
  ---
256
269
  description: Inspect Delphi and Object Pascal codebases through python-delphi-lsp.
257
- mode: subagent
270
+ mode: all
258
271
  temperature: 0
259
272
  permission:
260
- "*": deny
261
273
  delphi_codebase: allow
262
274
  skill:
263
275
  "*": deny
264
276
  python-delphi-lsp: allow
277
+ lsp: deny
278
+ bash: deny
279
+ read: deny
280
+ glob: deny
281
+ grep: deny
282
+ list: deny
283
+ edit: deny
284
+ write: deny
285
+ patch: deny
286
+ task: deny
287
+ webfetch: deny
288
+ websearch: deny
289
+ question: deny
290
+ todowrite: deny
291
+ todoread: deny
292
+ codebase_map: deny
293
+ code_guidelines: deny
265
294
  ---
266
295
  ```
267
296
 
@@ -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.2 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:
@@ -215,14 +228,30 @@ A generated OpenCode agent starts with this Markdown frontmatter:
215
228
  ```markdown
216
229
  ---
217
230
  description: Inspect Delphi and Object Pascal codebases through python-delphi-lsp.
218
- mode: subagent
231
+ mode: all
219
232
  temperature: 0
220
233
  permission:
221
- "*": deny
222
234
  delphi_codebase: allow
223
235
  skill:
224
236
  "*": deny
225
237
  python-delphi-lsp: allow
238
+ lsp: deny
239
+ bash: deny
240
+ read: deny
241
+ glob: deny
242
+ grep: deny
243
+ list: deny
244
+ edit: deny
245
+ write: deny
246
+ patch: deny
247
+ task: deny
248
+ webfetch: deny
249
+ websearch: deny
250
+ question: deny
251
+ todowrite: deny
252
+ todoread: deny
253
+ codebase_map: deny
254
+ code_guidelines: deny
226
255
  ---
227
256
  ```
228
257
 
@@ -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"
@@ -3,6 +3,7 @@ from __future__ import annotations
3
3
  from bisect import bisect_left, bisect_right
4
4
  from collections.abc import Mapping
5
5
  from dataclasses import dataclass
6
+ from heapq import heappop, heappush
6
7
  import hashlib
7
8
  import json
8
9
  from pathlib import Path, PureWindowsPath
@@ -14,13 +15,12 @@ from .agent_protocol import (
14
15
  AgentResponse,
15
16
  ContextBudget,
16
17
  Focus,
17
- Page,
18
18
  make_target_id,
19
19
  paginate_items,
20
20
  )
21
21
  from .agent_metrics import build_workspace_metrics, project_metric_item, unit_metric_item
22
22
  from .agent_relations import ProjectRelationIndex, RelationTarget
23
- from .agent_workspace import AgentWorkspace, unit_display_path, unit_source_path, unit_target_id
23
+ from .agent_workspace import AgentUnit, AgentWorkspace, unit_display_path, unit_source_path, unit_target_id
24
24
  from .consts import AttributeName, SyntaxNodeType
25
25
  from .lsp_server import build_outline_semantic_model, multiline_string_block_end
26
26
  from .nodes import CompoundSyntaxNode, SyntaxNode
@@ -121,6 +121,10 @@ class _SourceDocument:
121
121
  self.tokens = tuple(_lex_delphi(text))
122
122
  self.token_starts = tuple(token.start for token in self.tokens)
123
123
  self.directive_starts = tuple(token.start for token in self.tokens if token.directive)
124
+ self.declaration_section_indexes = [0]
125
+ self.declaration_section_checkpoints: dict[int, tuple[str, int, int, int]] = {
126
+ 0: ("", 0, 0, 0)
127
+ }
124
128
  words = [token for token in self.tokens if token.word and not token.escaped]
125
129
  self.unit_kind = next(
126
130
  (token.value for token in words if token.value in {"unit", "program", "library", "package"}),
@@ -129,6 +133,7 @@ class _SourceDocument:
129
133
  implementation = next((token for token in words if token.value == "implementation"), None)
130
134
  self.implementation_line = self.line_col(implementation.start)[0] if implementation else 0
131
135
  self.routine_spans: dict[int, tuple[int, int] | None] = {}
136
+ self.routine_token_spans: dict[int, tuple[int, int, int] | None] = {}
132
137
  self.parser_spans: dict[str, tuple[int, int] | None] = {}
133
138
  self._full_parse_attempted = False
134
139
  self._full_parse_result: object | None = None
@@ -230,6 +235,7 @@ class _Registry:
230
235
  entries: tuple[_SymbolEntry, ...]
231
236
  by_target: dict[str, _SymbolEntry]
232
237
  sources: dict[Path, _SourceDocument]
238
+ ranked_queries: dict[str, tuple[_SymbolEntry, ...]]
233
239
 
234
240
 
235
241
  class AgentContext:
@@ -280,7 +286,12 @@ class AgentContext:
280
286
  return self._handle_focus(parsed, revision)
281
287
  if parsed.action == "find":
282
288
  registry = self._require_registry(revision)
283
- items = [entry.card() for entry in _ranked_entries(registry.entries, parsed.query)]
289
+ ranked = registry.ranked_queries.get(parsed.query)
290
+ if ranked is None:
291
+ ranked = tuple(_ranked_entries(registry.entries, parsed.query))
292
+ registry.ranked_queries.clear()
293
+ registry.ranked_queries[parsed.query] = ranked
294
+ items = [entry.card() for entry in ranked]
284
295
  return self._response(parsed, revision, items)
285
296
  if parsed.action == "inspect":
286
297
  registry = self._require_registry(revision)
@@ -293,9 +304,10 @@ class AgentContext:
293
304
  previous_project_id = self._workspace.active_project_id
294
305
  selected_project_id = requested_project_id or previous_project_id
295
306
  if selected_project_id:
296
- self._workspace.select_project(selected_project_id)
307
+ revision = self._workspace._select_project_with_revision(selected_project_id)
308
+ else:
309
+ revision = self._workspace.workspace_revision
297
310
  current_project_id = self._workspace.active_project_id
298
- revision = self._workspace.workspace_revision
299
311
 
300
312
  if current_project_id != previous_project_id:
301
313
  self._registry = None
@@ -683,7 +695,14 @@ def _build_registry(workspace: AgentWorkspace, project_id: str, revision: str) -
683
695
  include_paths=workspace.include_paths,
684
696
  )
685
697
  sources[source_path] = document
686
- 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))
687
706
  unit_symbols = _collect_raw_symbols(model.unit_scope, unit, source_path, document)
688
707
  raw_symbols.extend(_exclude_routine_locals(unit_symbols, document))
689
708
 
@@ -782,6 +801,7 @@ def _build_registry(workspace: AgentWorkspace, project_id: str, revision: str) -
782
801
  entries=entries_tuple,
783
802
  by_target={entry.target_id: entry for entry in entries_tuple},
784
803
  sources=sources,
804
+ ranked_queries={},
785
805
  )
786
806
 
787
807
 
@@ -925,32 +945,47 @@ def _correct_outline_symbol_kind(document: _SourceDocument, symbol: Symbol) -> N
925
945
  symbol.kind = SymbolKind.TYPE
926
946
 
927
947
 
948
+ def _advance_declaration_section(
949
+ state: tuple[str, int, int, int],
950
+ token: _Token,
951
+ ) -> tuple[str, int, int, int]:
952
+ section, parentheses, brackets, angles = state
953
+ if token.directive:
954
+ return state
955
+ if token.value == "(":
956
+ parentheses += 1
957
+ elif token.value == ")":
958
+ parentheses = max(0, parentheses - 1)
959
+ elif token.value == "[":
960
+ brackets += 1
961
+ elif token.value == "]":
962
+ brackets = max(0, brackets - 1)
963
+ elif token.value == "<":
964
+ angles += 1
965
+ elif token.value == ">":
966
+ angles = max(0, angles - 1)
967
+ elif not parentheses and not brackets and not angles and token.word:
968
+ if token.value in {"const", "resourcestring", "threadvar", "type", "var"}:
969
+ section = token.value
970
+ elif token.value in {"implementation", "initialization", "finalization"}:
971
+ section = ""
972
+ return section, parentheses, brackets, angles
973
+
974
+
928
975
  def _declaration_section(document: _SourceDocument, offset: int) -> str:
929
- section = ""
930
- parentheses = 0
931
- brackets = 0
932
- angles = 0
933
- for token in document.tokens[:document.first_token_index(offset)]:
934
- if token.directive:
935
- continue
936
- if token.value == "(":
937
- parentheses += 1
938
- elif token.value == ")":
939
- parentheses = max(0, parentheses - 1)
940
- elif token.value == "[":
941
- brackets += 1
942
- elif token.value == "]":
943
- brackets = max(0, brackets - 1)
944
- elif token.value == "<":
945
- angles += 1
946
- elif token.value == ">":
947
- angles = max(0, angles - 1)
948
- elif not any((parentheses, brackets, angles)) and token.word:
949
- if token.value in {"const", "resourcestring", "threadvar", "type", "var"}:
950
- section = token.value
951
- elif token.value in {"implementation", "initialization", "finalization"}:
952
- section = ""
953
- return section
976
+ target_index = document.first_token_index(offset)
977
+ cached = document.declaration_section_checkpoints.get(target_index)
978
+ if cached is not None:
979
+ return cached[0]
980
+ checkpoint_position = bisect_right(document.declaration_section_indexes, target_index) - 1
981
+ checkpoint_index = document.declaration_section_indexes[checkpoint_position]
982
+ state = document.declaration_section_checkpoints[checkpoint_index]
983
+ for token in document.tokens[checkpoint_index:target_index]:
984
+ state = _advance_declaration_section(state, token)
985
+ insert_at = bisect_left(document.declaration_section_indexes, target_index)
986
+ document.declaration_section_indexes.insert(insert_at, target_index)
987
+ document.declaration_section_checkpoints[target_index] = state
988
+ return state[0]
954
989
 
955
990
 
956
991
  def _declared_symbol_name(document: _SourceDocument, symbol: Symbol) -> str:
@@ -1208,20 +1243,47 @@ def _exclude_routine_locals(
1208
1243
  span = _raw_routine_span(raw, document)
1209
1244
  if span is not None:
1210
1245
  containers.append((span[0], span[1], raw))
1246
+ if not containers:
1247
+ return symbols
1211
1248
 
1212
- filtered: list[_RawSymbol] = []
1213
- for raw in symbols:
1214
- offset = document.offset(
1215
- raw.symbol.decl_range.start_line,
1216
- raw.symbol.decl_range.start_col,
1249
+ containers.sort(key=lambda item: (item[0], item[1]))
1250
+ positioned = sorted(
1251
+ (
1252
+ document.offset(
1253
+ raw.symbol.decl_range.start_line,
1254
+ raw.symbol.decl_range.start_col,
1255
+ ),
1256
+ order,
1257
+ raw,
1217
1258
  )
1218
- if any(
1219
- start < offset < end and raw is not container
1220
- for start, end, container in containers
1259
+ for order, raw in enumerate(symbols)
1260
+ )
1261
+ active_ends: list[tuple[int, int, int]] = []
1262
+ active_ids: set[int] = set()
1263
+ excluded_orders: set[int] = set()
1264
+ container_index = 0
1265
+ for offset, order, raw in positioned:
1266
+ while (
1267
+ container_index < len(containers)
1268
+ and containers[container_index][0] < offset
1221
1269
  ):
1222
- continue
1223
- filtered.append(raw)
1224
- return filtered
1270
+ _, end, container = containers[container_index]
1271
+ container_id = id(container)
1272
+ heappush(active_ends, (end, container_index, container_id))
1273
+ active_ids.add(container_id)
1274
+ container_index += 1
1275
+ while active_ends and active_ends[0][0] <= offset:
1276
+ _, _, container_id = heappop(active_ends)
1277
+ active_ids.discard(container_id)
1278
+ raw_id = id(raw)
1279
+ if active_ids and (raw_id not in active_ids or len(active_ids) > 1):
1280
+ excluded_orders.add(order)
1281
+
1282
+ return [
1283
+ raw
1284
+ for order, raw in enumerate(symbols)
1285
+ if order not in excluded_orders
1286
+ ]
1225
1287
 
1226
1288
 
1227
1289
  def _raw_routine_span(
@@ -1523,6 +1585,7 @@ def _routine_span(
1523
1585
  document.tokens,
1524
1586
  document.token_starts,
1525
1587
  token_index,
1588
+ cache=document.routine_token_spans,
1526
1589
  )
1527
1590
  span = (start, found[1]) if found is not None else None
1528
1591
  document.routine_spans[start] = span
@@ -1534,6 +1597,7 @@ def _find_routine_token_span(
1534
1597
  token_starts: tuple[int, ...],
1535
1598
  start_index: int,
1536
1599
  *,
1600
+ cache: dict[int, tuple[int, int, int] | None] | None = None,
1537
1601
  depth: int = 0,
1538
1602
  ) -> tuple[int, int, int] | None:
1539
1603
  if depth > 64:
@@ -1541,56 +1605,100 @@ def _find_routine_token_span(
1541
1605
  routine_index = _routine_keyword_index(tokens, start_index)
1542
1606
  if routine_index is None:
1543
1607
  return None
1608
+ spans = cache if cache is not None else {}
1609
+ missing = object()
1610
+ cached = spans.get(routine_index, missing)
1611
+ if cached is not missing:
1612
+ if cached is None:
1613
+ return None
1614
+ return tokens[start_index].start, cached[1], cached[2]
1615
+
1544
1616
  heading_end = _heading_semicolon_index(tokens, routine_index)
1545
1617
  if heading_end is None:
1618
+ spans[routine_index] = None
1546
1619
  return None
1547
1620
 
1548
- index = heading_end + 1
1549
- while index < len(tokens):
1621
+ frames: list[list[int]] = [[routine_index, heading_end + 1]]
1622
+
1623
+ def reject_active_frames() -> None:
1624
+ for active_routine_index, _ in frames:
1625
+ spans[active_routine_index] = None
1626
+ frames.clear()
1627
+
1628
+ while frames:
1629
+ frame = frames[-1]
1630
+ frame_routine_index, index = frame
1631
+ if index >= len(tokens):
1632
+ reject_active_frames()
1633
+ continue
1634
+
1550
1635
  token = tokens[index]
1551
1636
  if token.directive:
1552
- return None
1637
+ reject_active_frames()
1638
+ continue
1553
1639
  if token.word and not token.escaped:
1554
1640
  if token.value in _NO_BODY_DIRECTIVES:
1555
- return None
1641
+ spans[frame_routine_index] = None
1642
+ frames.pop()
1643
+ continue
1556
1644
  if token.value in {"implementation", "initialization", "finalization"}:
1557
- return None
1645
+ reject_active_frames()
1646
+ continue
1558
1647
  if (
1559
1648
  token.value in _STRUCTURED_TYPE_WORDS
1560
1649
  and _is_structured_type_opener(tokens, index)
1561
1650
  ):
1562
1651
  if index + 1 < len(tokens) and tokens[index + 1].value == ";":
1563
- index += 2
1652
+ frame[1] = index + 2
1564
1653
  continue
1565
1654
  structured_end = _match_end_terminated_block(tokens, index)
1566
1655
  if structured_end is None:
1567
- return None
1568
- index = bisect_left(token_starts, structured_end)
1656
+ reject_active_frames()
1657
+ continue
1658
+ frame[1] = bisect_left(token_starts, structured_end)
1569
1659
  continue
1570
1660
  if token.value == "end":
1571
- return None
1661
+ reject_active_frames()
1662
+ continue
1572
1663
  if token.value in {"begin", "asm"}:
1573
1664
  end = _match_end_terminated_block(tokens, index)
1574
1665
  if end is None:
1575
- return None
1666
+ reject_active_frames()
1667
+ continue
1576
1668
  end_index = bisect_left(token_starts, end)
1577
- return tokens[start_index].start, end, end_index
1578
- if token.value in _ROUTINE_WORDS and _is_nested_routine_declaration(tokens, index):
1579
- nested = _find_routine_token_span(
1580
- tokens,
1581
- token_starts,
1582
- index,
1583
- depth=depth + 1,
1669
+ spans[frame_routine_index] = (
1670
+ tokens[frame_routine_index].start,
1671
+ end,
1672
+ end_index,
1584
1673
  )
1674
+ frames.pop()
1675
+ continue
1676
+ if token.value in _ROUTINE_WORDS and _is_nested_routine_declaration(tokens, index):
1677
+ nested_routine_index = _routine_keyword_index(tokens, index)
1678
+ if nested_routine_index is None:
1679
+ frame[1] = index + 1
1680
+ continue
1681
+ nested = spans.get(nested_routine_index, missing)
1682
+ if nested is missing:
1683
+ nested_heading_end = _heading_semicolon_index(tokens, nested_routine_index)
1684
+ if nested_heading_end is None:
1685
+ spans[nested_routine_index] = None
1686
+ continue
1687
+ frames.append([nested_routine_index, nested_heading_end + 1])
1688
+ continue
1585
1689
  if nested is not None:
1586
- index = max(index + 1, nested[2])
1690
+ frame[1] = max(index + 1, nested[2])
1587
1691
  continue
1588
1692
  skipped = _routine_declaration_end_index(tokens, index)
1589
1693
  if skipped is not None:
1590
- index = skipped + 1
1694
+ frame[1] = skipped + 1
1591
1695
  continue
1592
- index += 1
1593
- return None
1696
+ frame[1] = index + 1
1697
+
1698
+ result = spans.get(routine_index)
1699
+ if result is None:
1700
+ return None
1701
+ return tokens[start_index].start, result[1], result[2]
1594
1702
 
1595
1703
 
1596
1704
  def _routine_keyword_index(