python-delphi-lsp 2.0.3__py3-none-any.whl → 2.0.4__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/__init__.py +3 -0
- delphi_lsp/_version.py +1 -1
- delphi_lsp/agent_context.py +8 -1
- delphi_lsp/agent_layers.py +115 -6
- delphi_lsp/lsp_server.py +195 -33
- delphi_lsp/preprocessor.py +32 -10
- delphi_lsp/progress.py +26 -0
- delphi_lsp/project_discovery.py +125 -16
- delphi_lsp/project_indexer.py +43 -1
- {python_delphi_lsp-2.0.3.dist-info → python_delphi_lsp-2.0.4.dist-info}/METADATA +15 -2
- {python_delphi_lsp-2.0.3.dist-info → python_delphi_lsp-2.0.4.dist-info}/RECORD +15 -14
- {python_delphi_lsp-2.0.3.dist-info → python_delphi_lsp-2.0.4.dist-info}/WHEEL +0 -0
- {python_delphi_lsp-2.0.3.dist-info → python_delphi_lsp-2.0.4.dist-info}/entry_points.txt +0 -0
- {python_delphi_lsp-2.0.3.dist-info → python_delphi_lsp-2.0.4.dist-info}/licenses/LICENSE +0 -0
- {python_delphi_lsp-2.0.3.dist-info → python_delphi_lsp-2.0.4.dist-info}/top_level.txt +0 -0
delphi_lsp/__init__.py
CHANGED
|
@@ -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',
|
delphi_lsp/_version.py
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
__version__ = "2.0.
|
|
1
|
+
__version__ = "2.0.4"
|
delphi_lsp/agent_context.py
CHANGED
|
@@ -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
|
-
|
|
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
|
|
delphi_lsp/agent_layers.py
CHANGED
|
@@ -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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
delphi_lsp/lsp_server.py
CHANGED
|
@@ -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(
|
|
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(
|
|
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 = {
|
|
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
|
-
|
|
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(
|
|
412
|
-
|
|
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(
|
|
501
|
+
n = len(scanned)
|
|
425
502
|
while i < n:
|
|
426
|
-
ch =
|
|
427
|
-
nxt =
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
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
|
|
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(
|
|
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
|
|
453
|
-
if i + 1 < n and
|
|
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
|
|
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
|
|
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 (
|
|
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 (
|
|
588
|
+
while i < n and (scanned[i].isalnum() or scanned[i] == '_'):
|
|
490
589
|
i += 1
|
|
491
|
-
previous_token =
|
|
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 (
|
|
598
|
+
while i < n and (scanned[i].isalnum() or scanned[i] == '_'):
|
|
500
599
|
i += 1
|
|
501
|
-
word =
|
|
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=
|
|
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
|
|
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
|
|
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
|
|
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(
|
|
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))
|
delphi_lsp/preprocessor.py
CHANGED
|
@@ -136,11 +136,13 @@ class Preprocessor:
|
|
|
136
136
|
include_paths: Iterable[str] = (),
|
|
137
137
|
include_loader: Optional[IncludeLoader] = None,
|
|
138
138
|
options: Optional[PreprocessorOptions] = None,
|
|
139
|
+
preserve_active_directives: bool = False,
|
|
139
140
|
) -> None:
|
|
140
141
|
self.defines: set[str] = {self._normalize_define(d) for d in defines if d}
|
|
141
142
|
self.include_paths = [Path(p) for p in include_paths]
|
|
142
143
|
self.include_loader = include_loader or self._default_include_loader
|
|
143
144
|
self.options = options or PreprocessorOptions()
|
|
145
|
+
self.preserve_active_directives = preserve_active_directives
|
|
144
146
|
self._apply_default_compiler_defines()
|
|
145
147
|
self.scoped_enums = self.options.scoped_enums
|
|
146
148
|
self._option_values: dict[str, str] = {}
|
|
@@ -191,7 +193,9 @@ class Preprocessor:
|
|
|
191
193
|
if ch == "'":
|
|
192
194
|
start_line = ctx.line
|
|
193
195
|
active = self._is_active()
|
|
194
|
-
literal = self.
|
|
196
|
+
literal = self._consume_multiline_string(ctx)
|
|
197
|
+
if literal is None:
|
|
198
|
+
literal = self._consume_string(ctx)
|
|
195
199
|
self._emit_text(output, literal, ctx.file_name, start_line, active=active)
|
|
196
200
|
continue
|
|
197
201
|
|
|
@@ -273,6 +277,19 @@ class Preprocessor:
|
|
|
273
277
|
version_define = int(round(self.options.compiler_version * 10))
|
|
274
278
|
self.defines.add(f'VER{version_define}')
|
|
275
279
|
|
|
280
|
+
def _consume_multiline_string(self, ctx: _FileContext) -> str | None:
|
|
281
|
+
for quote_count in (5, 3):
|
|
282
|
+
delimiter = "'" * quote_count
|
|
283
|
+
if not ctx.text.startswith(delimiter, ctx.index):
|
|
284
|
+
continue
|
|
285
|
+
content_start = ctx.index + quote_count
|
|
286
|
+
if content_start >= len(ctx.text) or ctx.text[content_start] != '\n':
|
|
287
|
+
continue
|
|
288
|
+
close = ctx.text.find(delimiter, content_start + 1)
|
|
289
|
+
end = len(ctx.text) if close < 0 else close + quote_count
|
|
290
|
+
return ctx.advance(end - ctx.index)
|
|
291
|
+
return None
|
|
292
|
+
|
|
276
293
|
def _emit_text(
|
|
277
294
|
self,
|
|
278
295
|
output: _OutputBuffer,
|
|
@@ -310,9 +327,14 @@ class Preprocessor:
|
|
|
310
327
|
) -> None:
|
|
311
328
|
name, param = self._parse_directive(directive_content)
|
|
312
329
|
replacement = self._replace_with_spaces(directive_raw)
|
|
330
|
+
active_replacement = (
|
|
331
|
+
directive_raw
|
|
332
|
+
if self.preserve_active_directives and self._is_active()
|
|
333
|
+
else replacement
|
|
334
|
+
)
|
|
313
335
|
|
|
314
336
|
if not name:
|
|
315
|
-
self._emit_text(output,
|
|
337
|
+
self._emit_text(output, active_replacement, ctx.file_name, start_line, active=True)
|
|
316
338
|
return
|
|
317
339
|
|
|
318
340
|
if name in {'IFDEF', 'IFNDEF', 'IF', 'IFOPT'}:
|
|
@@ -378,14 +400,14 @@ class Preprocessor:
|
|
|
378
400
|
self.defines.add(define_name)
|
|
379
401
|
else:
|
|
380
402
|
self.defines.discard(define_name)
|
|
381
|
-
self._emit_text(output,
|
|
403
|
+
self._emit_text(output, active_replacement, ctx.file_name, start_line, active=True)
|
|
382
404
|
return
|
|
383
405
|
|
|
384
406
|
if name in {'SCOPEDENUMS'}:
|
|
385
407
|
if self._is_active():
|
|
386
408
|
self.scoped_enums = self._parse_on_off(param)
|
|
387
409
|
self._set_option('SCOPEDENUMS', 'ON' if self.scoped_enums else 'OFF')
|
|
388
|
-
self._emit_text(output,
|
|
410
|
+
self._emit_text(output, active_replacement, ctx.file_name, start_line, active=True)
|
|
389
411
|
return
|
|
390
412
|
|
|
391
413
|
if name in {'PUSHOPT', 'POPOPT'}:
|
|
@@ -405,13 +427,13 @@ class Preprocessor:
|
|
|
405
427
|
col=ctx.col,
|
|
406
428
|
)
|
|
407
429
|
)
|
|
408
|
-
self._emit_text(output,
|
|
430
|
+
self._emit_text(output, active_replacement, ctx.file_name, start_line, active=True)
|
|
409
431
|
return
|
|
410
432
|
|
|
411
433
|
if name == 'OPT':
|
|
412
434
|
if self._is_active():
|
|
413
435
|
self._apply_opt_directive(param)
|
|
414
|
-
self._emit_text(output,
|
|
436
|
+
self._emit_text(output, active_replacement, ctx.file_name, start_line, active=True)
|
|
415
437
|
return
|
|
416
438
|
|
|
417
439
|
if name in {'I', 'INCLUDE'}:
|
|
@@ -443,21 +465,21 @@ class Preprocessor:
|
|
|
443
465
|
)
|
|
444
466
|
else:
|
|
445
467
|
include_stack.append(resolved_path)
|
|
446
|
-
ctx.pending_output =
|
|
468
|
+
ctx.pending_output = active_replacement
|
|
447
469
|
ctx.pending_line = start_line
|
|
448
470
|
normalized = self._normalize_newlines(content)
|
|
449
471
|
if not normalized.endswith('\n'):
|
|
450
472
|
normalized += '\n'
|
|
451
473
|
contexts.append(_FileContext(text=normalized, file_name=resolved_path))
|
|
452
474
|
return
|
|
453
|
-
self._emit_text(output,
|
|
475
|
+
self._emit_text(output, active_replacement, ctx.file_name, start_line, active=True)
|
|
454
476
|
return
|
|
455
477
|
|
|
456
478
|
if self._is_active() and self._apply_named_option(name, param):
|
|
457
|
-
self._emit_text(output,
|
|
479
|
+
self._emit_text(output, active_replacement, ctx.file_name, start_line, active=True)
|
|
458
480
|
return
|
|
459
481
|
|
|
460
|
-
self._emit_text(output,
|
|
482
|
+
self._emit_text(output, active_replacement, ctx.file_name, start_line, active=True)
|
|
461
483
|
|
|
462
484
|
def _parse_directive(self, directive_text: str) -> tuple[str, str]:
|
|
463
485
|
text = directive_text.strip()
|
delphi_lsp/progress.py
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
from typing import Callable
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
@dataclass(frozen=True)
|
|
8
|
+
class ProgressEvent:
|
|
9
|
+
"""A package-controlled snapshot of Delphi indexing progress."""
|
|
10
|
+
|
|
11
|
+
phase: str
|
|
12
|
+
language: str
|
|
13
|
+
path: str
|
|
14
|
+
files_discovered: int
|
|
15
|
+
files_completed: int
|
|
16
|
+
files_total: int | None
|
|
17
|
+
lines_processed: int
|
|
18
|
+
symbols_discovered: int
|
|
19
|
+
cached_files: int
|
|
20
|
+
detail: str
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
ProgressCallback = Callable[[ProgressEvent], None]
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
__all__ = ["ProgressCallback", "ProgressEvent"]
|
delphi_lsp/project_discovery.py
CHANGED
|
@@ -1,12 +1,15 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
|
+
import errno
|
|
3
4
|
from dataclasses import dataclass, field
|
|
4
5
|
from pathlib import Path
|
|
5
|
-
from typing import Iterable
|
|
6
|
+
from typing import Iterable, Iterator
|
|
6
7
|
import os
|
|
7
8
|
import re
|
|
8
9
|
import xml.etree.ElementTree as ET
|
|
9
10
|
|
|
11
|
+
from .progress import ProgressCallback, ProgressEvent
|
|
12
|
+
|
|
10
13
|
|
|
11
14
|
SOURCE_EXTENSIONS = (".pas", ".dpr", ".dpk", ".inc")
|
|
12
15
|
PROJECT_EXTENSIONS = (".dpr", ".dpk")
|
|
@@ -69,10 +72,12 @@ def discover_delphi_project(
|
|
|
69
72
|
search_paths: Iterable[str | os.PathLike[str]] = (),
|
|
70
73
|
defines: Iterable[str] = (),
|
|
71
74
|
scan_workspace_sources: bool = True,
|
|
75
|
+
on_progress: ProgressCallback | None = None,
|
|
72
76
|
) -> DelphiProjectDiscovery:
|
|
73
77
|
root_path = Path(root).expanduser().resolve()
|
|
74
78
|
project_path = Path(project_file).expanduser().resolve() if project_file is not None else None
|
|
75
79
|
discovery = DelphiProjectDiscovery(root=str(root_path))
|
|
80
|
+
_emit_progress(on_progress, "discovery", str(root_path), 0, 0, None, "project discovery started")
|
|
76
81
|
|
|
77
82
|
seen_search: set[str] = set()
|
|
78
83
|
seen_include: set[str] = set()
|
|
@@ -185,18 +190,42 @@ def discover_delphi_project(
|
|
|
185
190
|
discovery.config_files.append(str(cfg))
|
|
186
191
|
|
|
187
192
|
if scan_workspace_sources:
|
|
188
|
-
populate_workspace_sources(discovery)
|
|
193
|
+
populate_workspace_sources(discovery, on_progress=on_progress)
|
|
194
|
+
|
|
195
|
+
_emit_progress(
|
|
196
|
+
on_progress,
|
|
197
|
+
"inventory",
|
|
198
|
+
str(root_path),
|
|
199
|
+
len(discovery.source_files),
|
|
200
|
+
0,
|
|
201
|
+
len(discovery.source_files),
|
|
202
|
+
"project discovery complete",
|
|
203
|
+
)
|
|
189
204
|
|
|
190
205
|
return discovery
|
|
191
206
|
|
|
192
207
|
|
|
193
|
-
def populate_workspace_sources(
|
|
208
|
+
def populate_workspace_sources(
|
|
209
|
+
discovery: DelphiProjectDiscovery,
|
|
210
|
+
*,
|
|
211
|
+
on_progress: ProgressCallback | None = None,
|
|
212
|
+
) -> DelphiProjectDiscovery:
|
|
194
213
|
root_path = Path(discovery.root).expanduser().resolve()
|
|
195
214
|
seen_sources = {source.casefold() for source in discovery.source_files}
|
|
196
215
|
seen_search = {path.casefold() for path in discovery.search_paths}
|
|
197
216
|
seen_include = {path.casefold() for path in discovery.include_paths}
|
|
198
217
|
|
|
199
|
-
_scan_sources(root_path, discovery, seen_sources)
|
|
218
|
+
_scan_sources(root_path, discovery, seen_sources, on_progress=on_progress)
|
|
219
|
+
total_sources = len(discovery.source_files)
|
|
220
|
+
_emit_progress(
|
|
221
|
+
on_progress,
|
|
222
|
+
"inventory",
|
|
223
|
+
str(root_path),
|
|
224
|
+
total_sources,
|
|
225
|
+
0,
|
|
226
|
+
total_sources,
|
|
227
|
+
"workspace source inventory complete",
|
|
228
|
+
)
|
|
200
229
|
for source in discovery.source_files:
|
|
201
230
|
path = Path(source)
|
|
202
231
|
unit_key = path.stem.casefold()
|
|
@@ -227,10 +256,51 @@ def populate_workspace_sources(discovery: DelphiProjectDiscovery) -> DelphiProje
|
|
|
227
256
|
return discovery
|
|
228
257
|
|
|
229
258
|
|
|
230
|
-
def discover_workspace_sources(
|
|
259
|
+
def discover_workspace_sources(
|
|
260
|
+
root: str | os.PathLike[str],
|
|
261
|
+
*,
|
|
262
|
+
on_progress: ProgressCallback | None = None,
|
|
263
|
+
) -> DelphiProjectDiscovery:
|
|
231
264
|
root_path = Path(root).expanduser().resolve()
|
|
232
265
|
discovery = DelphiProjectDiscovery(root=str(root_path))
|
|
233
|
-
|
|
266
|
+
_emit_progress(on_progress, "discovery", str(root_path), 0, 0, None, "workspace discovery started")
|
|
267
|
+
populate_workspace_sources(discovery, on_progress=on_progress)
|
|
268
|
+
_emit_progress(
|
|
269
|
+
on_progress,
|
|
270
|
+
"complete",
|
|
271
|
+
str(root_path),
|
|
272
|
+
len(discovery.source_files),
|
|
273
|
+
len(discovery.source_files),
|
|
274
|
+
len(discovery.source_files),
|
|
275
|
+
"workspace discovery complete",
|
|
276
|
+
)
|
|
277
|
+
return discovery
|
|
278
|
+
|
|
279
|
+
|
|
280
|
+
def _emit_progress(
|
|
281
|
+
callback: ProgressCallback | None,
|
|
282
|
+
phase: str,
|
|
283
|
+
path: str,
|
|
284
|
+
files_discovered: int,
|
|
285
|
+
files_completed: int,
|
|
286
|
+
files_total: int | None,
|
|
287
|
+
detail: str,
|
|
288
|
+
) -> None:
|
|
289
|
+
if callback is not None:
|
|
290
|
+
callback(
|
|
291
|
+
ProgressEvent(
|
|
292
|
+
phase=phase,
|
|
293
|
+
language="delphi",
|
|
294
|
+
path=path,
|
|
295
|
+
files_discovered=files_discovered,
|
|
296
|
+
files_completed=files_completed,
|
|
297
|
+
files_total=files_total,
|
|
298
|
+
lines_processed=0,
|
|
299
|
+
symbols_discovered=0,
|
|
300
|
+
cached_files=0,
|
|
301
|
+
detail=detail,
|
|
302
|
+
)
|
|
303
|
+
)
|
|
234
304
|
|
|
235
305
|
|
|
236
306
|
def _project_candidates(root: Path, explicit: Path | None) -> list[Path]:
|
|
@@ -409,33 +479,72 @@ def _main_source_from_dproj(path: Path) -> str | None:
|
|
|
409
479
|
return None
|
|
410
480
|
|
|
411
481
|
|
|
412
|
-
def _scan_sources(
|
|
413
|
-
|
|
482
|
+
def _scan_sources(
|
|
483
|
+
root: Path,
|
|
484
|
+
discovery: DelphiProjectDiscovery,
|
|
485
|
+
seen_sources: set[str],
|
|
486
|
+
*,
|
|
487
|
+
on_progress: ProgressCallback | None = None,
|
|
488
|
+
) -> None:
|
|
489
|
+
for path in _walk_files(root):
|
|
414
490
|
if path.suffix.casefold() not in SOURCE_EXTENSIONS:
|
|
415
491
|
continue
|
|
416
|
-
if any(part in SKIP_DIRS for part in path.parts):
|
|
417
|
-
continue
|
|
418
|
-
if not path.is_file():
|
|
419
|
-
continue
|
|
420
492
|
resolved = path.resolve()
|
|
421
493
|
key = str(resolved).casefold()
|
|
422
494
|
if key in seen_sources:
|
|
423
495
|
continue
|
|
424
496
|
seen_sources.add(key)
|
|
425
497
|
discovery.source_files.append(str(resolved))
|
|
498
|
+
_emit_progress(
|
|
499
|
+
on_progress,
|
|
500
|
+
"discovery",
|
|
501
|
+
str(resolved),
|
|
502
|
+
len(discovery.source_files),
|
|
503
|
+
0,
|
|
504
|
+
None,
|
|
505
|
+
"source file discovered",
|
|
506
|
+
)
|
|
426
507
|
discovery.source_files.sort(key=lambda item: (item.casefold(), item))
|
|
427
508
|
|
|
428
509
|
|
|
429
510
|
def _walk_sources(root: Path, pattern: str) -> list[Path]:
|
|
430
511
|
results: list[Path] = []
|
|
431
|
-
for path in root
|
|
432
|
-
if
|
|
433
|
-
continue
|
|
434
|
-
if path.is_file():
|
|
512
|
+
for path in _walk_files(root):
|
|
513
|
+
if path.match(pattern):
|
|
435
514
|
results.append(path.resolve())
|
|
436
515
|
return results
|
|
437
516
|
|
|
438
517
|
|
|
518
|
+
def _walk_files(root: Path) -> Iterator[Path]:
|
|
519
|
+
def handle_error(error: OSError) -> None:
|
|
520
|
+
if _is_path_too_long(error):
|
|
521
|
+
return
|
|
522
|
+
raise error
|
|
523
|
+
|
|
524
|
+
for directory, directory_names, file_names in os.walk(
|
|
525
|
+
root,
|
|
526
|
+
followlinks=False,
|
|
527
|
+
onerror=handle_error,
|
|
528
|
+
):
|
|
529
|
+
directory_names[:] = sorted(
|
|
530
|
+
name for name in directory_names if name not in SKIP_DIRS
|
|
531
|
+
)
|
|
532
|
+
current = Path(directory)
|
|
533
|
+
for name in sorted(file_names):
|
|
534
|
+
path = current / name
|
|
535
|
+
try:
|
|
536
|
+
if path.is_file():
|
|
537
|
+
yield path
|
|
538
|
+
except OSError as error:
|
|
539
|
+
if _is_path_too_long(error):
|
|
540
|
+
continue
|
|
541
|
+
raise
|
|
542
|
+
|
|
543
|
+
|
|
544
|
+
def _is_path_too_long(error: OSError) -> bool:
|
|
545
|
+
return getattr(error, "winerror", None) == 206 or error.errno == errno.ENAMETOOLONG
|
|
546
|
+
|
|
547
|
+
|
|
439
548
|
def _add_resolved_path(
|
|
440
549
|
target: list[str],
|
|
441
550
|
seen: set[str],
|
delphi_lsp/project_indexer.py
CHANGED
|
@@ -9,6 +9,7 @@ from .consts import AttributeName, SyntaxNodeType
|
|
|
9
9
|
from .nodes import SyntaxNode
|
|
10
10
|
from .parser import DelphiParser
|
|
11
11
|
from .preprocessor import IncludeLoader
|
|
12
|
+
from .progress import ProgressCallback, ProgressEvent
|
|
12
13
|
from .source_reader import read_source_text
|
|
13
14
|
|
|
14
15
|
|
|
@@ -71,6 +72,7 @@ class ProjectIndexer:
|
|
|
71
72
|
on_get_unit_syntax: GetUnitSyntaxHook | None = None,
|
|
72
73
|
on_unit_parsed: UnitParsedHook | None = None,
|
|
73
74
|
source_transform: SourceTransform | None = None,
|
|
75
|
+
on_progress: ProgressCallback | None = None,
|
|
74
76
|
) -> None:
|
|
75
77
|
self.search_paths = [Path(path) for path in search_paths]
|
|
76
78
|
self.include_paths = [Path(path) for path in include_paths]
|
|
@@ -79,6 +81,7 @@ class ProjectIndexer:
|
|
|
79
81
|
self.on_get_unit_syntax = on_get_unit_syntax
|
|
80
82
|
self.on_unit_parsed = on_unit_parsed
|
|
81
83
|
self.source_transform = source_transform
|
|
84
|
+
self.on_progress = on_progress
|
|
82
85
|
|
|
83
86
|
self._parsed_units: dict[str, UnitInfo] = {}
|
|
84
87
|
self._problems: list[ProjectProblem] = []
|
|
@@ -86,6 +89,9 @@ class ProjectIndexer:
|
|
|
86
89
|
self._include_files: dict[str, IncludeFileInfo] = {}
|
|
87
90
|
self._project_folder: Path = Path('.')
|
|
88
91
|
self._aborting: bool = False
|
|
92
|
+
self._files_discovered = 0
|
|
93
|
+
self._files_completed = 0
|
|
94
|
+
self._lines_processed = 0
|
|
89
95
|
|
|
90
96
|
def index(self, file_name: str) -> ProjectIndexResult:
|
|
91
97
|
self._parsed_units = {}
|
|
@@ -93,9 +99,13 @@ class ProjectIndexer:
|
|
|
93
99
|
self._not_found_units = set()
|
|
94
100
|
self._include_files = {}
|
|
95
101
|
self._aborting = False
|
|
102
|
+
self._files_discovered = 0
|
|
103
|
+
self._files_completed = 0
|
|
104
|
+
self._lines_processed = 0
|
|
96
105
|
|
|
97
106
|
entry = Path(file_name).expanduser().resolve()
|
|
98
107
|
self._project_folder = entry.parent
|
|
108
|
+
self._emit_progress("discovery", str(entry), "project index started")
|
|
99
109
|
|
|
100
110
|
is_project = entry.suffix.casefold() in {'.dpr', '.dpk'}
|
|
101
111
|
self._parse_unit(entry.stem, entry, is_project=is_project)
|
|
@@ -105,12 +115,14 @@ class ProjectIndexer:
|
|
|
105
115
|
problems = list(self._problems)
|
|
106
116
|
not_found = sorted(self._not_found_units, key=str.casefold)
|
|
107
117
|
|
|
108
|
-
|
|
118
|
+
result = ProjectIndexResult(
|
|
109
119
|
parsed_units=parsed_units,
|
|
110
120
|
include_files=include_files,
|
|
111
121
|
problems=problems,
|
|
112
122
|
not_found_units=not_found,
|
|
113
123
|
)
|
|
124
|
+
self._emit_progress("complete", str(entry), "project index complete")
|
|
125
|
+
return result
|
|
114
126
|
|
|
115
127
|
def _parse_unit(self, unit_name: str, file_path: Path, *, is_project: bool) -> None:
|
|
116
128
|
if self._aborting:
|
|
@@ -119,6 +131,8 @@ class ProjectIndexer:
|
|
|
119
131
|
normalized_name = unit_name.casefold()
|
|
120
132
|
if normalized_name in self._parsed_units:
|
|
121
133
|
return
|
|
134
|
+
self._files_discovered += 1
|
|
135
|
+
self._emit_progress("inventory", str(file_path), "unit discovered")
|
|
122
136
|
|
|
123
137
|
hook_tree: Optional[SyntaxNode] = None
|
|
124
138
|
do_parse_unit = True
|
|
@@ -134,7 +148,12 @@ class ProjectIndexer:
|
|
|
134
148
|
if syntax_tree is None and do_parse_unit:
|
|
135
149
|
source = self._read_file(file_path)
|
|
136
150
|
if source is None:
|
|
151
|
+
self._files_completed += 1
|
|
152
|
+
self._emit_progress("detail", str(file_path), "unit unavailable")
|
|
137
153
|
return
|
|
154
|
+
self._lines_processed += source.count("\n") + (
|
|
155
|
+
0 if not source or source.endswith("\n") else 1
|
|
156
|
+
)
|
|
138
157
|
parser = DelphiParser(
|
|
139
158
|
include_paths=[str(path) for path in self.include_paths],
|
|
140
159
|
defines=self.defines,
|
|
@@ -161,15 +180,21 @@ class ProjectIndexer:
|
|
|
161
180
|
has_error=True,
|
|
162
181
|
error_info=UnitErrorInfo(error=str(exc)),
|
|
163
182
|
)
|
|
183
|
+
self._files_completed += 1
|
|
184
|
+
self._emit_progress("detail", str(file_path), "unit parse failed")
|
|
164
185
|
return
|
|
165
186
|
|
|
166
187
|
if syntax_tree is None:
|
|
188
|
+
self._files_completed += 1
|
|
189
|
+
self._emit_progress("detail", str(file_path), "unit skipped")
|
|
167
190
|
return
|
|
168
191
|
|
|
169
192
|
actual_name = syntax_tree.get_attribute(AttributeName.anName) or unit_name
|
|
170
193
|
normalized_name = actual_name.casefold()
|
|
171
194
|
unit_info = UnitInfo(name=actual_name, path=str(file_path), syntax_tree=syntax_tree)
|
|
172
195
|
self._parsed_units[normalized_name] = unit_info
|
|
196
|
+
self._files_completed += 1
|
|
197
|
+
self._emit_progress("detail", str(file_path), "unit parsed")
|
|
173
198
|
|
|
174
199
|
if self.on_unit_parsed is not None:
|
|
175
200
|
do_abort = self.on_unit_parsed(actual_name, str(file_path), syntax_tree, from_parser)
|
|
@@ -290,6 +315,23 @@ class ProjectIndexer:
|
|
|
290
315
|
|
|
291
316
|
return wrapped
|
|
292
317
|
|
|
318
|
+
def _emit_progress(self, phase: str, path: str, detail: str) -> None:
|
|
319
|
+
if self.on_progress is not None:
|
|
320
|
+
self.on_progress(
|
|
321
|
+
ProgressEvent(
|
|
322
|
+
phase=phase,
|
|
323
|
+
language="delphi",
|
|
324
|
+
path=path,
|
|
325
|
+
files_discovered=self._files_discovered,
|
|
326
|
+
files_completed=self._files_completed,
|
|
327
|
+
files_total=self._files_discovered,
|
|
328
|
+
lines_processed=self._lines_processed,
|
|
329
|
+
symbols_discovered=0,
|
|
330
|
+
cached_files=0,
|
|
331
|
+
detail=detail,
|
|
332
|
+
)
|
|
333
|
+
)
|
|
334
|
+
|
|
293
335
|
def _default_include_loader(self, parent_file: str, include_name: str) -> Optional[tuple[str, str]]:
|
|
294
336
|
parent = Path(parent_file).resolve().parent
|
|
295
337
|
include_path = Path(include_name.replace('\\', '/'))
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: python-delphi-lsp
|
|
3
|
-
Version: 2.0.
|
|
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.
|
|
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:
|
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
delphi_lsp/__init__.py,sha256=
|
|
2
|
-
delphi_lsp/_version.py,sha256=
|
|
1
|
+
delphi_lsp/__init__.py,sha256=mq3tF0NrXDgbkJXDuf5HhW6sOnmP7DQxs3zl8fGoAmo,3867
|
|
2
|
+
delphi_lsp/_version.py,sha256=ZOg41aCgKEuObK5WhegikXektFWPJaeZ-TMDh0Ke95M,22
|
|
3
3
|
delphi_lsp/agent_cli.py,sha256=Mma4C1Ca_Hd_9dl0mePT7uIypw6j6gDWdKu9XxIMXQQ,9226
|
|
4
|
-
delphi_lsp/agent_context.py,sha256=
|
|
5
|
-
delphi_lsp/agent_layers.py,sha256=
|
|
4
|
+
delphi_lsp/agent_context.py,sha256=BfmsCbTIWSaMNFkPuyC31yh7wkzCzBI5xt3BE8vsIa4,77715
|
|
5
|
+
delphi_lsp/agent_layers.py,sha256=ngFn-zH2Jv6CBjOhd3UkoyVgsrdXW0BChnQxSVmkRRc,25131
|
|
6
6
|
delphi_lsp/agent_metrics.py,sha256=6y9DrSnKitZbwRFQrofanw6k89YGqbvEacqgSKHgh2c,2695
|
|
7
7
|
delphi_lsp/agent_protocol.py,sha256=cZ1LIMhrkOb5CqiGcjSH1eZUNqtJCYySUc54h-PCyOs,12551
|
|
8
8
|
delphi_lsp/agent_relations.py,sha256=pXhuCjfwmicG3VG7lYtGvKKv-mFPQ_hJ3w0eavqMoV0,32900
|
|
@@ -14,21 +14,22 @@ delphi_lsp/consts.py,sha256=qM40iwxXmvEileWHJVLMYlQSkLoLyQaqM-Zscz3VJbs,5958
|
|
|
14
14
|
delphi_lsp/grammar.py,sha256=QWEMSUKr0e1xlJOveZtp7K-MvhlkvDgxkD0iUKIyxKU,18577
|
|
15
15
|
delphi_lsp/lark_builder.py,sha256=c0_kFB_o3U64fwsC2BnsFibAojnqxjaZSrzASg6sTSM,119202
|
|
16
16
|
delphi_lsp/lark_tokens.py,sha256=xT_GES2z6p1IdN3EnoKE9RcUM6suu1RV3J-0FEJN2r8,4176
|
|
17
|
-
delphi_lsp/lsp_server.py,sha256=
|
|
17
|
+
delphi_lsp/lsp_server.py,sha256=kxm0A-VNDkkNZKb6TSqFxur6wNcvh1m34EKIKfOr2LU,71514
|
|
18
18
|
delphi_lsp/metrics.py,sha256=f70B1awypqVWZIW_SNpFHdShoS8OzDW5DWX5ftQMI0c,26166
|
|
19
19
|
delphi_lsp/nodes.py,sha256=LW8KUgNCg9MHK_2NmzrgCMMJNcBwa4C76XwKoS7kt4E,13556
|
|
20
20
|
delphi_lsp/parser.py,sha256=4hckHD_fsiiUe3oeSRJwqbY8bDWqJaYLsU10NYDIW6E,6025
|
|
21
|
-
delphi_lsp/preprocessor.py,sha256=
|
|
22
|
-
delphi_lsp/
|
|
23
|
-
delphi_lsp/
|
|
21
|
+
delphi_lsp/preprocessor.py,sha256=aXJiabmCvWoUdrxYAA4EVVxDDQXl_IeFUezoDdMlFyU,30984
|
|
22
|
+
delphi_lsp/progress.py,sha256=FF3lqdDKLvq3b7cOhuSc3P0FDzVnX0miD8U8_3xt5iY,535
|
|
23
|
+
delphi_lsp/project_discovery.py,sha256=UpezFlNJDaF5sW8tkG_HUxHpAULSf1GSWXJTlUF8vMM,20230
|
|
24
|
+
delphi_lsp/project_indexer.py,sha256=J_GA1GlLrmxAajS7zHeeVHbVH7MEcf8278Yd9hcIh7o,13137
|
|
24
25
|
delphi_lsp/semantic.py,sha256=egQ-_AjmnAATvL1hRUfjd4FDapFxdwi5rt8QExb0POc,9818
|
|
25
26
|
delphi_lsp/semantic_builder.py,sha256=ulA7xHJpltjc6cTo5wh9yTcYazuIykM-YLMXD1h-wzk,58680
|
|
26
27
|
delphi_lsp/source_reader.py,sha256=HWiw25sLVZ6s1GGpMl17uZ-o68HoxTEd3Z0__m1vsSE,520
|
|
27
28
|
delphi_lsp/workspace.py,sha256=TU__SujItRlW8hB2kuYC7ZJx1WQVOj7frYxvo7rDqeI,2137
|
|
28
29
|
delphi_lsp/writer.py,sha256=j-cnyiHFNOTFECL7Ehm-m43ye7ev7qFeCCscFMjrAOY,2503
|
|
29
|
-
python_delphi_lsp-2.0.
|
|
30
|
-
python_delphi_lsp-2.0.
|
|
31
|
-
python_delphi_lsp-2.0.
|
|
32
|
-
python_delphi_lsp-2.0.
|
|
33
|
-
python_delphi_lsp-2.0.
|
|
34
|
-
python_delphi_lsp-2.0.
|
|
30
|
+
python_delphi_lsp-2.0.4.dist-info/licenses/LICENSE,sha256=-rPda9qyJvHAhjCx3ZF-Efy07F4eAg4sFvg6ChOGPoU,16726
|
|
31
|
+
python_delphi_lsp-2.0.4.dist-info/METADATA,sha256=y1GTsjlMI2yUCd5Z9ZmGOHiiuRxQlf2IIQEmM5G1TyY,15390
|
|
32
|
+
python_delphi_lsp-2.0.4.dist-info/WHEEL,sha256=K260EYznzXsJYBQGqmI8VTxEdiZYNvDZwW9cBh9-_MA,91
|
|
33
|
+
python_delphi_lsp-2.0.4.dist-info/entry_points.txt,sha256=rh_yGH7diowge_acQcST87A-ewH1QezEsAYC8T5ineY,103
|
|
34
|
+
python_delphi_lsp-2.0.4.dist-info/top_level.txt,sha256=0Zp0sjw5Qtic3EQX9zNeegURga8NuXk6c2e9PN-3OTs,11
|
|
35
|
+
python_delphi_lsp-2.0.4.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|