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 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.3"
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(
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(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))
@@ -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._consume_string(ctx)
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, replacement, ctx.file_name, start_line, active=True)
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, replacement, ctx.file_name, start_line, active=True)
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, replacement, ctx.file_name, start_line, active=True)
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, replacement, ctx.file_name, start_line, active=True)
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, replacement, ctx.file_name, start_line, active=True)
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 = replacement
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, replacement, ctx.file_name, start_line, active=True)
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, replacement, ctx.file_name, start_line, active=True)
479
+ self._emit_text(output, active_replacement, ctx.file_name, start_line, active=True)
458
480
  return
459
481
 
460
- self._emit_text(output, replacement, ctx.file_name, start_line, active=True)
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"]
@@ -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(discovery: DelphiProjectDiscovery) -> DelphiProjectDiscovery:
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(root: str | os.PathLike[str]) -> DelphiProjectDiscovery:
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
- return populate_workspace_sources(discovery)
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(root: Path, discovery: DelphiProjectDiscovery, seen_sources: set[str]) -> None:
413
- for path in root.rglob("*"):
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.rglob(pattern):
432
- if any(part in SKIP_DIRS for part in path.parts):
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],
@@ -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
- return ProjectIndexResult(
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
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:
@@ -1,8 +1,8 @@
1
- delphi_lsp/__init__.py,sha256=Gtoeg_D3hLGCFi34wdYiKrIByAbkWe1zS84v6_KJ6_4,3768
2
- delphi_lsp/_version.py,sha256=_GEKEa6BYjBV34SZkSlAR87aCM5Y9G0aSI0LXL52iJg,22
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=FdRQ2MFGDcqBDGMnD0w2X8p1kB3YwyTPDm_4cdCmwbA,77504
5
- delphi_lsp/agent_layers.py,sha256=7TmplTL7m_riS8BxZCD-3j1p8iOGiD9QWHay5ZcDYiw,21369
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=CCR9FwtZ6oOlEdy8FU-1kFlLawQS3PJ7XyBf2KG5Ijo,65950
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=X58ZUfI7Hifg-7KYomOF0BZEp-jl1NFGyNl9VCu6wtU,29935
22
- delphi_lsp/project_discovery.py,sha256=ei7Iy19EMpTWjOFpKPc9hDG2oH1MUGxKwCLnibJp_TM,17336
23
- delphi_lsp/project_indexer.py,sha256=R5oD4sYsIi2nAlOCr-Mq3-CFkINeC19t0vtKkciCRmg,11227
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.3.dist-info/licenses/LICENSE,sha256=-rPda9qyJvHAhjCx3ZF-Efy07F4eAg4sFvg6ChOGPoU,16726
30
- python_delphi_lsp-2.0.3.dist-info/METADATA,sha256=Dw8iQ0nH5GwHL2eIbYPEMXpVcoMp0Mc0tkGltO7iUMo,14942
31
- python_delphi_lsp-2.0.3.dist-info/WHEEL,sha256=K260EYznzXsJYBQGqmI8VTxEdiZYNvDZwW9cBh9-_MA,91
32
- python_delphi_lsp-2.0.3.dist-info/entry_points.txt,sha256=rh_yGH7diowge_acQcST87A-ewH1QezEsAYC8T5ineY,103
33
- python_delphi_lsp-2.0.3.dist-info/top_level.txt,sha256=0Zp0sjw5Qtic3EQX9zNeegURga8NuXk6c2e9PN-3OTs,11
34
- python_delphi_lsp-2.0.3.dist-info/RECORD,,
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,,