python-delphi-lsp 3.6.0__cp310-abi3-win_amd64.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (56) hide show
  1. delphi_lsp/__init__.py +184 -0
  2. delphi_lsp/_version.py +1 -0
  3. delphi_lsp/agent_cache.py +1574 -0
  4. delphi_lsp/agent_cli.py +902 -0
  5. delphi_lsp/agent_context.py +3907 -0
  6. delphi_lsp/agent_cpg.py +349 -0
  7. delphi_lsp/agent_cpg_builder.py +666 -0
  8. delphi_lsp/agent_layers.py +1410 -0
  9. delphi_lsp/agent_metrics.py +526 -0
  10. delphi_lsp/agent_protocol.py +445 -0
  11. delphi_lsp/agent_relations.py +1881 -0
  12. delphi_lsp/agent_templates.py +614 -0
  13. delphi_lsp/agent_wiki.py +2853 -0
  14. delphi_lsp/agent_workspace.py +860 -0
  15. delphi_lsp/ast_serialize.py +126 -0
  16. delphi_lsp/binary.py +250 -0
  17. delphi_lsp/comment_builder.py +28 -0
  18. delphi_lsp/consts.py +345 -0
  19. delphi_lsp/delphi_lsp_native.pyd +0 -0
  20. delphi_lsp/delphiast_lexer.py +399 -0
  21. delphi_lsp/delphiast_parser.py +2189 -0
  22. delphi_lsp/delphiast_tokens.py +41 -0
  23. delphi_lsp/grammar.py +459 -0
  24. delphi_lsp/incremental.py +1139 -0
  25. delphi_lsp/lark_builder.py +2684 -0
  26. delphi_lsp/lark_tokens.py +236 -0
  27. delphi_lsp/lexical_scanner.py +30 -0
  28. delphi_lsp/lsp_server.py +6698 -0
  29. delphi_lsp/metrics.py +1452 -0
  30. delphi_lsp/native_bridge.py +300 -0
  31. delphi_lsp/navigation_cache.py +189 -0
  32. delphi_lsp/nodes.py +462 -0
  33. delphi_lsp/parallel_outline.py +662 -0
  34. delphi_lsp/parse_artifacts.py +969 -0
  35. delphi_lsp/parser.py +342 -0
  36. delphi_lsp/parser_backend.py +139 -0
  37. delphi_lsp/preprocessor.py +1122 -0
  38. delphi_lsp/progress.py +26 -0
  39. delphi_lsp/project_config.py +285 -0
  40. delphi_lsp/project_discovery.py +932 -0
  41. delphi_lsp/project_indexer.py +470 -0
  42. delphi_lsp/py.typed +1 -0
  43. delphi_lsp/response_cache.py +465 -0
  44. delphi_lsp/semantic.py +394 -0
  45. delphi_lsp/semantic_builder.py +2302 -0
  46. delphi_lsp/source_reader.py +17 -0
  47. delphi_lsp/wiki_external_sort.py +392 -0
  48. delphi_lsp/wiki_memory.py +548 -0
  49. delphi_lsp/workspace.py +83 -0
  50. delphi_lsp/writer.py +73 -0
  51. python_delphi_lsp-3.6.0.dist-info/METADATA +766 -0
  52. python_delphi_lsp-3.6.0.dist-info/RECORD +56 -0
  53. python_delphi_lsp-3.6.0.dist-info/WHEEL +4 -0
  54. python_delphi_lsp-3.6.0.dist-info/entry_points.txt +3 -0
  55. python_delphi_lsp-3.6.0.dist-info/licenses/LICENSE +373 -0
  56. python_delphi_lsp-3.6.0.dist-info/sboms/python-delphi-lsp.cyclonedx.json +770 -0
@@ -0,0 +1,1139 @@
1
+ """Conservative sub-unit reparsing for editor changes.
2
+
3
+ The engine deliberately optimizes one case: edits wholly inside the statement
4
+ body of a top-level implementation routine. Every ambiguous edit is planned
5
+ as a full reparse.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ from bisect import bisect_right
11
+ from dataclasses import dataclass, is_dataclass, replace
12
+ from enum import Enum
13
+ from hashlib import sha256
14
+ from typing import TYPE_CHECKING, Any, cast
15
+
16
+ from .consts import AttributeName, SyntaxNodeType
17
+ from .semantic import SourceRange, Symbol, SymbolIndex, normalize_name
18
+ from .semantic_builder import SemanticBuilder
19
+
20
+ if TYPE_CHECKING:
21
+ from .nodes import SyntaxNode
22
+ from .parser import DelphiParser, ParseResult
23
+
24
+
25
+ class EditKind(str, Enum):
26
+ INCREMENTAL = "incremental"
27
+ FULL_REPARSE = "full-reparse"
28
+
29
+
30
+ FULL_REPARSE = EditKind.FULL_REPARSE
31
+
32
+
33
+ @dataclass(frozen=True)
34
+ class TextEdit:
35
+ """A Python-codepoint edit using half-open absolute source offsets."""
36
+
37
+ start: int
38
+ end: int
39
+ text: str
40
+
41
+ def __post_init__(self) -> None:
42
+ if self.start < 0 or self.end < self.start:
43
+ raise ValueError("edit offsets must satisfy 0 <= start <= end")
44
+
45
+
46
+ @dataclass(frozen=True)
47
+ class RoutineSpan:
48
+ name: str
49
+ start_line: int
50
+ end_line: int
51
+ body_start_line: int
52
+ body_end_line: int
53
+ start_offset: int
54
+ end_offset: int
55
+ body_start_offset: int
56
+ body_end_offset: int
57
+ context_hash: str
58
+
59
+
60
+ @dataclass(frozen=True)
61
+ class SectionSpan:
62
+ kind: str
63
+ start_line: int
64
+ end_line: int
65
+ start_offset: int
66
+ end_offset: int
67
+ context_hash: str
68
+
69
+
70
+ @dataclass(frozen=True)
71
+ class DeclarationSpan:
72
+ section: str
73
+ kind: str
74
+ start_line: int
75
+ end_line: int
76
+ start_offset: int
77
+ end_offset: int
78
+ context_hash: str
79
+
80
+
81
+ @dataclass(frozen=True)
82
+ class PathCounters:
83
+ incremental: int = 0
84
+ full_reparse: int = 0
85
+
86
+
87
+ @dataclass(frozen=True)
88
+ class SpanIndex:
89
+ source_hash: str
90
+ source_length: int
91
+ file_name: str
92
+ line_starts: tuple[int, ...]
93
+ sections: tuple[SectionSpan, ...]
94
+ declarations: tuple[DeclarationSpan, ...]
95
+ routines: tuple[RoutineSpan, ...]
96
+ result: ParseResult
97
+ parser: DelphiParser
98
+ counters: PathCounters = PathCounters()
99
+
100
+
101
+ @dataclass(frozen=True)
102
+ class EditPlan:
103
+ kind: EditKind
104
+ reason: str
105
+ edit: TextEdit
106
+ updated_source: str
107
+ origin_index: SpanIndex
108
+ updated_index: SpanIndex | None
109
+ counters: PathCounters
110
+ routine: RoutineSpan | None = None
111
+ previous_routine: RoutineSpan | None = None
112
+ line_delta: int = 0
113
+ character_delta: int = 0
114
+
115
+
116
+ @dataclass(frozen=True)
117
+ class SplicedModel:
118
+ result: ParseResult
119
+ span_index: SpanIndex
120
+ kind: EditKind
121
+ reason: str
122
+ counters: PathCounters
123
+
124
+
125
+ def build_span_index(
126
+ source: str,
127
+ model: ParseResult,
128
+ *,
129
+ parser: DelphiParser | None = None,
130
+ ) -> SpanIndex:
131
+ """Build implementation-routine boundaries from a completed full parse."""
132
+
133
+ if parser is None:
134
+ from .parser import DelphiParser
135
+ from .parser_backend import ParserMode
136
+
137
+ parser = DelphiParser(mode=ParserMode.TOLERANT, legacy_fallback_max_chars=0)
138
+ _normalize_syntax_positions(model)
139
+ line_starts = _line_starts(source)
140
+ sections, declarations = _structural_spans(source, line_starts, model.root)
141
+ implementation = _implementation_node(model.root)
142
+ routines: list[RoutineSpan] = []
143
+ if implementation is not None:
144
+ for node in implementation.child_nodes:
145
+ if node.typ is not SyntaxNodeType.ntMethod:
146
+ continue
147
+ span = _routine_span(source, line_starts, node)
148
+ if span is not None:
149
+ routines.append(span)
150
+ return SpanIndex(
151
+ source_hash=_digest(source),
152
+ source_length=len(source),
153
+ file_name=model.root.file_name,
154
+ line_starts=line_starts,
155
+ sections=sections,
156
+ declarations=declarations,
157
+ routines=tuple(routines),
158
+ result=model,
159
+ parser=parser,
160
+ )
161
+
162
+
163
+ def _normalize_syntax_positions(model: ParseResult) -> None:
164
+ root = model.root
165
+ if getattr(root, "_incremental_source_positions_normalized", False):
166
+ return
167
+ preprocessed = model.preprocessed
168
+ file_name = root.file_name
169
+
170
+ def mapped_position(line: int, col: int) -> tuple[int, int] | None:
171
+ if line < 1:
172
+ return None
173
+ mapped_file, mapped_line, mapped_col = preprocessed.map_position(line, col)
174
+ if mapped_file == file_name and mapped_line > 0:
175
+ return mapped_line, mapped_col
176
+ if line == len(preprocessed.source_map) + 1 and preprocessed.source_map:
177
+ last = preprocessed.source_map[-1]
178
+ if last.file_name == file_name:
179
+ return last.line + 1, col + last.col_offset
180
+ return None
181
+
182
+ def visit(node: SyntaxNode) -> None:
183
+ start = mapped_position(node.line, node.col)
184
+ if start is not None:
185
+ node.line, node.col = start
186
+ end_line = getattr(node, "end_line", 0)
187
+ end = mapped_position(end_line, getattr(node, "end_col", 0))
188
+ if end is not None:
189
+ cast(Any, node).end_line, cast(Any, node).end_col = end
190
+ token_end = mapped_position(node.token_end_line, node.token_end_col)
191
+ if token_end is not None:
192
+ node.token_end_line, node.token_end_col = token_end
193
+ if node.source_positions:
194
+ node.source_positions = (
195
+ mapped_position(line, col) or (line, col)
196
+ for line, col in node.source_positions
197
+ )
198
+ for child in node.child_nodes:
199
+ visit(child)
200
+
201
+ visit(root)
202
+ cast(Any, root)._incremental_source_positions_normalized = True
203
+
204
+
205
+ def apply_edit(source: str, span_index: SpanIndex, edit: TextEdit) -> EditPlan:
206
+ """Apply *edit* and classify it as one routine span or a full reparse."""
207
+
208
+ if edit.end > len(source):
209
+ raise ValueError("edit end exceeds source length")
210
+ updated_source = source[: edit.start] + edit.text + source[edit.end :]
211
+ character_delta = len(edit.text) - (edit.end - edit.start)
212
+ removed = source[edit.start : edit.end]
213
+ line_delta = _newline_count(edit.text) - _newline_count(removed)
214
+ if _digest(source) != span_index.source_hash or len(source) != span_index.source_length:
215
+ return _full_plan(
216
+ span_index,
217
+ edit,
218
+ updated_source,
219
+ "stale-span-index",
220
+ line_delta,
221
+ character_delta,
222
+ )
223
+ if _touches_conditional_directive(source, edit) or _contains_conditional_directive(
224
+ edit.text
225
+ ):
226
+ return _full_plan(
227
+ span_index,
228
+ edit,
229
+ updated_source,
230
+ "conditional-directive",
231
+ line_delta,
232
+ character_delta,
233
+ )
234
+
235
+ routine = next(
236
+ (
237
+ candidate
238
+ for candidate in span_index.routines
239
+ if candidate.body_start_offset <= edit.start
240
+ and edit.end <= candidate.body_end_offset
241
+ ),
242
+ None,
243
+ )
244
+ if routine is None:
245
+ return _full_plan(
246
+ span_index,
247
+ edit,
248
+ updated_source,
249
+ "outside-routine-body",
250
+ line_delta,
251
+ character_delta,
252
+ )
253
+
254
+ updated_index = _shift_index(
255
+ span_index,
256
+ updated_source,
257
+ routine,
258
+ edit,
259
+ line_delta,
260
+ character_delta,
261
+ counters=PathCounters(
262
+ incremental=span_index.counters.incremental + 1,
263
+ full_reparse=span_index.counters.full_reparse,
264
+ ),
265
+ )
266
+ updated_routine = next(
267
+ candidate
268
+ for candidate in updated_index.routines
269
+ if candidate.start_offset == routine.start_offset
270
+ )
271
+ return EditPlan(
272
+ kind=EditKind.INCREMENTAL,
273
+ reason="routine-body",
274
+ edit=edit,
275
+ updated_source=updated_source,
276
+ origin_index=span_index,
277
+ updated_index=updated_index,
278
+ counters=updated_index.counters,
279
+ routine=updated_routine,
280
+ previous_routine=routine,
281
+ line_delta=line_delta,
282
+ character_delta=character_delta,
283
+ )
284
+
285
+
286
+ def reparse_span(source: str, plan: EditPlan) -> SplicedModel:
287
+ """Execute an edit plan and return the updated parse result and span index."""
288
+
289
+ if source != plan.updated_source:
290
+ raise ValueError("source does not match the edit plan")
291
+ if plan.kind is EditKind.FULL_REPARSE:
292
+ return _execute_full_reparse(source, plan, plan.reason, plan.counters)
293
+ if (
294
+ plan.updated_index is None
295
+ or plan.routine is None
296
+ or plan.previous_routine is None
297
+ ):
298
+ return _execute_full_reparse(
299
+ source,
300
+ plan,
301
+ "incomplete-incremental-plan",
302
+ _escalated_counters(plan),
303
+ )
304
+ if _digest(source) != plan.updated_index.source_hash:
305
+ return _execute_full_reparse(
306
+ source,
307
+ plan,
308
+ "stale-updated-source",
309
+ _escalated_counters(plan),
310
+ )
311
+ routine = plan.routine
312
+ if _context_hash(source, routine.start_offset, routine.end_offset) != routine.context_hash:
313
+ return _execute_full_reparse(
314
+ source,
315
+ plan,
316
+ "routine-context-mismatch",
317
+ _escalated_counters(plan),
318
+ )
319
+
320
+ fragment_source = source[routine.start_offset : routine.end_offset]
321
+ wrapped = f"unit __Frag;\ninterface\nimplementation\n{fragment_source}\nend.\n"
322
+ fragment_result = plan.updated_index.parser.parse(
323
+ wrapped,
324
+ plan.updated_index.file_name,
325
+ build_semantic=False,
326
+ )
327
+ fragment_implementation = _implementation_node(fragment_result.root)
328
+ fragment_methods = (
329
+ [
330
+ child
331
+ for child in fragment_implementation.child_nodes
332
+ if child.typ is SyntaxNodeType.ntMethod
333
+ ]
334
+ if fragment_implementation is not None
335
+ else []
336
+ )
337
+ if len(fragment_methods) != 1:
338
+ return _execute_full_reparse(
339
+ source,
340
+ plan,
341
+ "fragment-resync-failed",
342
+ _escalated_counters(plan),
343
+ )
344
+ fragment_method = fragment_methods[0]
345
+ line_offset = routine.start_line - fragment_method.line
346
+ _shift_node(fragment_method, line_offset, plan.updated_index.file_name)
347
+ statements = next(
348
+ (
349
+ child
350
+ for child in fragment_method.child_nodes
351
+ if child.typ is SyntaxNodeType.ntStatements
352
+ ),
353
+ None,
354
+ )
355
+ if (
356
+ fragment_method.get_attribute(AttributeName.anName) != routine.name
357
+ or statements is None
358
+ or getattr(statements, "end_line", 0) != routine.end_line
359
+ ):
360
+ return _execute_full_reparse(
361
+ source,
362
+ plan,
363
+ "fragment-boundary-changed",
364
+ _escalated_counters(plan),
365
+ )
366
+
367
+ result = plan.updated_index.result
368
+ target_parent = _implementation_node(result.root)
369
+ target = _find_method_node(
370
+ target_parent,
371
+ plan.previous_routine.name,
372
+ plan.previous_routine.start_line,
373
+ )
374
+ if target_parent is None or target is None or fragment_implementation is None:
375
+ return _execute_full_reparse(
376
+ source,
377
+ plan,
378
+ "held-model-mismatch",
379
+ _escalated_counters(plan),
380
+ )
381
+
382
+ if result.semantic is not None and not _splice_semantic_model(
383
+ result,
384
+ fragment_result.root,
385
+ fragment_method,
386
+ plan.previous_routine,
387
+ routine,
388
+ plan.line_delta,
389
+ ):
390
+ return _execute_full_reparse(
391
+ source,
392
+ plan,
393
+ "semantic-splice-failed",
394
+ _escalated_counters(plan),
395
+ )
396
+
397
+ _shift_node_after(
398
+ result.root,
399
+ plan.previous_routine.end_line,
400
+ plan.line_delta,
401
+ skip=target,
402
+ )
403
+ target_index = target_parent.child_nodes.index(target)
404
+ target_parent.extract_child(target)
405
+ fragment_implementation.extract_child(fragment_method)
406
+ target_parent.add_child(fragment_method)
407
+ target_parent.child_nodes.insert(target_index, target_parent.child_nodes.pop())
408
+
409
+ shifted_fragment_problems = [
410
+ _shift_problem(problem, line_offset)
411
+ for problem in fragment_result.problems
412
+ if getattr(problem, "line", fragment_method.line) >= fragment_method.line - line_offset
413
+ ]
414
+ result.problems = [
415
+ shifted
416
+ for problem in result.problems
417
+ if (
418
+ shifted := _problem_after_edit(
419
+ problem,
420
+ plan.previous_routine,
421
+ plan.line_delta,
422
+ )
423
+ )
424
+ is not None
425
+ ]
426
+ result.problems.extend(shifted_fragment_problems)
427
+ result.comments = _splice_comments(
428
+ result.comments,
429
+ fragment_result.comments,
430
+ plan.previous_routine,
431
+ line_offset,
432
+ plan.line_delta,
433
+ routine,
434
+ )
435
+
436
+ updated_index = replace(
437
+ plan.updated_index,
438
+ result=result,
439
+ counters=plan.counters,
440
+ )
441
+ return SplicedModel(
442
+ result=result,
443
+ span_index=updated_index,
444
+ kind=EditKind.INCREMENTAL,
445
+ reason=plan.reason,
446
+ counters=plan.counters,
447
+ )
448
+
449
+
450
+ def _execute_full_reparse(
451
+ source: str,
452
+ plan: EditPlan,
453
+ reason: str,
454
+ counters: PathCounters,
455
+ ) -> SplicedModel:
456
+ previous_index = plan.updated_index or plan.origin_index
457
+ result = previous_index.parser.parse(
458
+ source,
459
+ previous_index.file_name,
460
+ build_semantic=previous_index.result.semantic is not None,
461
+ )
462
+ span_index = replace(
463
+ build_span_index(
464
+ source,
465
+ result,
466
+ parser=previous_index.parser,
467
+ ),
468
+ counters=counters,
469
+ )
470
+ return SplicedModel(
471
+ result=result,
472
+ span_index=span_index,
473
+ kind=EditKind.FULL_REPARSE,
474
+ reason=reason,
475
+ counters=counters,
476
+ )
477
+
478
+
479
+ def _escalated_counters(plan: EditPlan) -> PathCounters:
480
+ return PathCounters(
481
+ incremental=max(0, plan.counters.incremental - 1),
482
+ full_reparse=plan.counters.full_reparse + 1,
483
+ )
484
+
485
+
486
+ def _implementation_node(root: SyntaxNode) -> SyntaxNode | None:
487
+ return next(
488
+ (
489
+ child
490
+ for child in root.child_nodes
491
+ if child.typ is SyntaxNodeType.ntImplementation
492
+ ),
493
+ None,
494
+ )
495
+
496
+
497
+ def _find_method_node(
498
+ implementation: SyntaxNode | None,
499
+ name: str,
500
+ start_line: int,
501
+ ) -> SyntaxNode | None:
502
+ if implementation is None:
503
+ return None
504
+ return next(
505
+ (
506
+ child
507
+ for child in implementation.child_nodes
508
+ if child.typ is SyntaxNodeType.ntMethod
509
+ and child.line == start_line
510
+ and child.get_attribute(AttributeName.anName) == name
511
+ ),
512
+ None,
513
+ )
514
+
515
+
516
+ def _shift_node(node: SyntaxNode, line_delta: int, file_name: str) -> None:
517
+ node.file_name = file_name
518
+ if node.line > 0:
519
+ node.line += line_delta
520
+ end_line = getattr(node, "end_line", 0)
521
+ if end_line > 0:
522
+ cast(Any, node).end_line = end_line + line_delta
523
+ if node.token_end_line > 0:
524
+ node.token_end_line += line_delta
525
+ if node.source_positions:
526
+ node.source_positions = (
527
+ (line + line_delta if line > 0 else line, column)
528
+ for line, column in node.source_positions
529
+ )
530
+ for child in node.child_nodes:
531
+ _shift_node(child, line_delta, file_name)
532
+
533
+
534
+ def _shift_node_after(
535
+ node: SyntaxNode,
536
+ cutoff_line: int,
537
+ line_delta: int,
538
+ *,
539
+ skip: SyntaxNode,
540
+ ) -> None:
541
+ if line_delta == 0 or node is skip:
542
+ return
543
+ if node.line > cutoff_line:
544
+ _shift_node(node, line_delta, node.file_name)
545
+ return
546
+ end_line = getattr(node, "end_line", 0)
547
+ if end_line > cutoff_line:
548
+ cast(Any, node).end_line = end_line + line_delta
549
+ if node.token_end_line > cutoff_line:
550
+ node.token_end_line += line_delta
551
+ if node.source_positions:
552
+ node.source_positions = (
553
+ (line + line_delta if line > cutoff_line else line, column)
554
+ for line, column in node.source_positions
555
+ )
556
+ for child in node.child_nodes:
557
+ _shift_node_after(child, cutoff_line, line_delta, skip=skip)
558
+
559
+
560
+ def _shift_problem(problem: Any, line_delta: int) -> Any:
561
+ line = getattr(problem, "line", None)
562
+ if not isinstance(line, int) or line < 1 or not is_dataclass(problem):
563
+ return problem
564
+ values: dict[str, Any] = {"line": line + line_delta}
565
+ if hasattr(problem, "file_name"):
566
+ values["file_name"] = problem.file_name
567
+ try:
568
+ return replace(cast(Any, problem), **values)
569
+ except TypeError:
570
+ values.pop("file_name", None)
571
+ return replace(cast(Any, problem), **values)
572
+
573
+
574
+ def _problem_after_edit(
575
+ problem: Any,
576
+ previous: RoutineSpan,
577
+ line_delta: int,
578
+ ) -> Any | None:
579
+ line = getattr(problem, "line", None)
580
+ if not isinstance(line, int):
581
+ return problem
582
+ if previous.start_line <= line <= previous.end_line:
583
+ return None
584
+ if line > previous.end_line and line_delta:
585
+ return _shift_problem(problem, line_delta)
586
+ return problem
587
+
588
+
589
+ def _splice_comments(
590
+ old_comments: list,
591
+ fragment_comments: list,
592
+ previous: RoutineSpan,
593
+ fragment_line_offset: int,
594
+ line_delta: int,
595
+ routine: RoutineSpan,
596
+ ) -> list:
597
+ kept = []
598
+ for comment in old_comments:
599
+ if previous.start_line <= comment.line <= previous.end_line:
600
+ continue
601
+ if comment.line > previous.end_line and line_delta:
602
+ _shift_node(comment, line_delta, comment.file_name)
603
+ kept.append(comment)
604
+ fragment_start = routine.start_line - fragment_line_offset
605
+ fragment_end = routine.end_line - fragment_line_offset
606
+ for comment in fragment_comments:
607
+ if fragment_start <= comment.line <= fragment_end:
608
+ _shift_node(comment, fragment_line_offset, comment.file_name)
609
+ kept.append(comment)
610
+ kept.sort(key=lambda comment: (comment.line, comment.col))
611
+ return kept
612
+
613
+
614
+ def _splice_semantic_model(
615
+ result: ParseResult,
616
+ fragment_root: SyntaxNode,
617
+ fragment_method: SyntaxNode,
618
+ previous: RoutineSpan,
619
+ routine: RoutineSpan,
620
+ line_delta: int,
621
+ ) -> bool:
622
+ semantic = result.semantic
623
+ if semantic is None:
624
+ return True
625
+ unit_scope = semantic.unit_scope
626
+ key = normalize_name(previous.name)
627
+ candidates = unit_scope.symbols.get(key, [])
628
+ old_symbol = next(
629
+ (
630
+ symbol
631
+ for symbol in candidates
632
+ if symbol.name == previous.name
633
+ and symbol.decl_range.start_line == previous.start_line
634
+ ),
635
+ None,
636
+ )
637
+ if old_symbol is None:
638
+ return False
639
+ old_position = candidates.index(old_symbol)
640
+ candidates.remove(old_symbol)
641
+ if not candidates:
642
+ unit_scope.symbols.pop(key, None)
643
+ _remove_index_symbol(semantic.index, old_symbol)
644
+
645
+ _shift_scope_ranges(
646
+ unit_scope,
647
+ previous.end_line,
648
+ line_delta,
649
+ skip=old_symbol,
650
+ )
651
+ kept_references = []
652
+ for reference in semantic.references:
653
+ line = reference.ref_range.start_line
654
+ if previous.start_line <= line <= previous.end_line:
655
+ continue
656
+ reference.ref_range = _shift_source_range(
657
+ reference.ref_range,
658
+ previous.end_line,
659
+ line_delta,
660
+ )
661
+ kept_references.append(reference)
662
+ kept_problems = []
663
+ for problem in semantic.problems:
664
+ line = problem.range.start_line
665
+ if previous.start_line <= line <= previous.end_line:
666
+ continue
667
+ problem.range = _shift_source_range(
668
+ problem.range,
669
+ previous.end_line,
670
+ line_delta,
671
+ )
672
+ kept_problems.append(problem)
673
+
674
+ fragment_index = SymbolIndex(units=dict(semantic.index.units))
675
+ builder = SemanticBuilder()
676
+ builder.declare(
677
+ fragment_root,
678
+ index=fragment_index,
679
+ unit_scope=unit_scope,
680
+ reset_state=True,
681
+ )
682
+ fragment_model = builder.resolve(fragment_root, unit_scope)
683
+ new_candidates = unit_scope.symbols.get(normalize_name(routine.name), [])
684
+ new_symbol = next(
685
+ (
686
+ symbol
687
+ for symbol in reversed(new_candidates)
688
+ if symbol.name == routine.name
689
+ and symbol.decl_range.start_line == routine.start_line
690
+ ),
691
+ None,
692
+ )
693
+ if new_symbol is None:
694
+ return False
695
+ new_candidates.remove(new_symbol)
696
+ new_candidates.insert(min(old_position, len(new_candidates)), new_symbol)
697
+ _add_index_symbol(semantic.index, new_symbol)
698
+ semantic.references = [*kept_references, *fragment_model.references]
699
+ semantic.problems = [*kept_problems, *fragment_model.problems]
700
+ semantic.references.sort(
701
+ key=lambda reference: (
702
+ reference.ref_range.start_line,
703
+ reference.ref_range.start_col,
704
+ reference.name,
705
+ )
706
+ )
707
+ semantic.problems.sort(
708
+ key=lambda problem: (
709
+ problem.range.start_line,
710
+ problem.range.start_col,
711
+ problem.message,
712
+ )
713
+ )
714
+ return fragment_method.get_attribute(AttributeName.anName) == routine.name
715
+
716
+
717
+ def _shift_scope_ranges(
718
+ scope: Any,
719
+ cutoff_line: int,
720
+ line_delta: int,
721
+ *,
722
+ skip: Symbol,
723
+ ) -> None:
724
+ if line_delta == 0:
725
+ return
726
+ pending = [scope]
727
+ seen_scopes: set[int] = set()
728
+ seen_symbols: set[int] = set()
729
+ while pending:
730
+ current = pending.pop()
731
+ if id(current) in seen_scopes:
732
+ continue
733
+ seen_scopes.add(id(current))
734
+ if current.owner is not None and current.owner is not skip:
735
+ _shift_symbol_ranges(
736
+ current.owner,
737
+ cutoff_line,
738
+ line_delta,
739
+ seen_symbols,
740
+ )
741
+ for entries in current.symbols.values():
742
+ for symbol in entries:
743
+ if symbol is skip:
744
+ continue
745
+ _shift_symbol_ranges(
746
+ symbol,
747
+ cutoff_line,
748
+ line_delta,
749
+ seen_symbols,
750
+ )
751
+ if symbol.member_scope is not None:
752
+ pending.append(symbol.member_scope)
753
+
754
+
755
+ def _shift_symbol_ranges(
756
+ symbol: Symbol,
757
+ cutoff_line: int,
758
+ line_delta: int,
759
+ seen: set[int],
760
+ ) -> None:
761
+ if id(symbol) in seen:
762
+ return
763
+ seen.add(id(symbol))
764
+ symbol.decl_range = _shift_source_range(symbol.decl_range, cutoff_line, line_delta)
765
+ symbol.name_range = _shift_source_range(symbol.name_range, cutoff_line, line_delta)
766
+ for overload in symbol.overloads:
767
+ _shift_symbol_ranges(overload, cutoff_line, line_delta, seen)
768
+
769
+
770
+ def _shift_source_range(
771
+ value: SourceRange,
772
+ cutoff_line: int,
773
+ line_delta: int,
774
+ ) -> SourceRange:
775
+ if line_delta == 0:
776
+ return value
777
+ if value.start_line > cutoff_line:
778
+ return replace(
779
+ value,
780
+ start_line=value.start_line + line_delta,
781
+ end_line=value.end_line + line_delta,
782
+ )
783
+ if value.end_line > cutoff_line:
784
+ return replace(value, end_line=value.end_line + line_delta)
785
+ return value
786
+
787
+
788
+ def _remove_index_symbol(index: SymbolIndex, symbol: Symbol) -> None:
789
+ key = normalize_name(symbol.name)
790
+ entries = index.name_index.get(key)
791
+ if entries is None:
792
+ return
793
+ index.name_index[key] = [candidate for candidate in entries if candidate is not symbol]
794
+ if not index.name_index[key]:
795
+ index.name_index.pop(key, None)
796
+
797
+
798
+ def _add_index_symbol(index: SymbolIndex, symbol: Symbol) -> None:
799
+ key = normalize_name(symbol.name)
800
+ entries = index.name_index.setdefault(key, [])
801
+ if all(candidate is not symbol for candidate in entries):
802
+ entries.append(symbol)
803
+
804
+
805
+ def _routine_span(
806
+ source: str,
807
+ line_starts: tuple[int, ...],
808
+ node: SyntaxNode,
809
+ ) -> RoutineSpan | None:
810
+ if node.line < 1:
811
+ return None
812
+ statements = next(
813
+ (
814
+ child
815
+ for child in node.child_nodes
816
+ if child.typ is SyntaxNodeType.ntStatements
817
+ ),
818
+ None,
819
+ )
820
+ statements_end_line = getattr(statements, "end_line", 0)
821
+ if (
822
+ statements is None
823
+ or statements.line < 1
824
+ or statements_end_line < 1
825
+ or statements_end_line > len(line_starts)
826
+ ):
827
+ return None
828
+ start_line = node.line
829
+ end_line = statements_end_line
830
+ body_start_line = statements.line + 1
831
+ body_end_line = statements_end_line - 1
832
+ if body_end_line < body_start_line:
833
+ return None
834
+ start_offset = _line_start(line_starts, start_line)
835
+ end_offset = _line_start(line_starts, end_line + 1, default=len(source))
836
+ body_start_offset = _line_start(line_starts, body_start_line)
837
+ body_end_offset = _line_start(
838
+ line_starts,
839
+ body_end_line + 1,
840
+ default=len(source),
841
+ )
842
+ name = node.get_attribute(AttributeName.anName)
843
+ if not name or end_offset <= start_offset:
844
+ return None
845
+ return RoutineSpan(
846
+ name=name,
847
+ start_line=start_line,
848
+ end_line=end_line,
849
+ body_start_line=body_start_line,
850
+ body_end_line=body_end_line,
851
+ start_offset=start_offset,
852
+ end_offset=end_offset,
853
+ body_start_offset=body_start_offset,
854
+ body_end_offset=body_end_offset,
855
+ context_hash=_context_hash(source, start_offset, end_offset),
856
+ )
857
+
858
+
859
+ def _full_plan(
860
+ span_index: SpanIndex,
861
+ edit: TextEdit,
862
+ updated_source: str,
863
+ reason: str,
864
+ line_delta: int,
865
+ character_delta: int,
866
+ ) -> EditPlan:
867
+ counters = PathCounters(
868
+ incremental=span_index.counters.incremental,
869
+ full_reparse=span_index.counters.full_reparse + 1,
870
+ )
871
+ return EditPlan(
872
+ kind=EditKind.FULL_REPARSE,
873
+ reason=reason,
874
+ edit=edit,
875
+ updated_source=updated_source,
876
+ origin_index=span_index,
877
+ updated_index=None,
878
+ counters=counters,
879
+ line_delta=line_delta,
880
+ character_delta=character_delta,
881
+ )
882
+
883
+
884
+ def _shift_index(
885
+ span_index: SpanIndex,
886
+ updated_source: str,
887
+ edited_routine: RoutineSpan,
888
+ edit: TextEdit,
889
+ line_delta: int,
890
+ character_delta: int,
891
+ *,
892
+ counters: PathCounters,
893
+ ) -> SpanIndex:
894
+ line_starts = _shift_line_starts(span_index.line_starts, edit)
895
+ routines: list[RoutineSpan] = []
896
+ for routine in span_index.routines:
897
+ if routine is edited_routine:
898
+ shifted = replace(
899
+ routine,
900
+ end_line=routine.end_line + line_delta,
901
+ body_end_line=routine.body_end_line + line_delta,
902
+ end_offset=routine.end_offset + character_delta,
903
+ body_end_offset=routine.body_end_offset + character_delta,
904
+ )
905
+ elif routine.start_offset >= edit.end:
906
+ shifted = replace(
907
+ routine,
908
+ start_line=routine.start_line + line_delta,
909
+ end_line=routine.end_line + line_delta,
910
+ body_start_line=routine.body_start_line + line_delta,
911
+ body_end_line=routine.body_end_line + line_delta,
912
+ start_offset=routine.start_offset + character_delta,
913
+ end_offset=routine.end_offset + character_delta,
914
+ body_start_offset=routine.body_start_offset + character_delta,
915
+ body_end_offset=routine.body_end_offset + character_delta,
916
+ )
917
+ else:
918
+ shifted = routine
919
+ routines.append(shifted)
920
+ return replace(
921
+ span_index,
922
+ source_hash=_digest(updated_source),
923
+ source_length=len(updated_source),
924
+ line_starts=line_starts,
925
+ sections=_shift_boundary_spans(
926
+ span_index.sections,
927
+ edit,
928
+ line_delta,
929
+ character_delta,
930
+ ),
931
+ declarations=_shift_boundary_spans(
932
+ span_index.declarations,
933
+ edit,
934
+ line_delta,
935
+ character_delta,
936
+ ),
937
+ routines=tuple(routines),
938
+ counters=counters,
939
+ )
940
+
941
+
942
+ def _structural_spans(
943
+ source: str,
944
+ line_starts: tuple[int, ...],
945
+ root: SyntaxNode,
946
+ ) -> tuple[tuple[SectionSpan, ...], tuple[DeclarationSpan, ...]]:
947
+ section_kinds = {
948
+ SyntaxNodeType.ntInterface: "interface",
949
+ SyntaxNodeType.ntImplementation: "implementation",
950
+ SyntaxNodeType.ntInitialization: "initialization",
951
+ SyntaxNodeType.ntFinalization: "finalization",
952
+ }
953
+ declaration_kinds = {
954
+ SyntaxNodeType.ntTypeSection: "type",
955
+ SyntaxNodeType.ntConstants: "const",
956
+ SyntaxNodeType.ntVariables: "var",
957
+ SyntaxNodeType.ntUses: "uses",
958
+ SyntaxNodeType.ntExports: "exports",
959
+ }
960
+ section_nodes = [
961
+ child
962
+ for child in root.child_nodes
963
+ if child.typ in section_kinds and child.line > 0
964
+ ]
965
+ sections: list[SectionSpan] = []
966
+ declarations: list[DeclarationSpan] = []
967
+ source_end_line = len(line_starts) - (1 if line_starts[-1] == len(source) else 0)
968
+ root_end_line = min(
969
+ max(root.line, getattr(root, "end_line", 0)),
970
+ max(1, source_end_line),
971
+ )
972
+ for index, node in enumerate(section_nodes):
973
+ kind = section_kinds[node.typ]
974
+ end_line = (
975
+ section_nodes[index + 1].line - 1
976
+ if index + 1 < len(section_nodes)
977
+ else root_end_line
978
+ )
979
+ start_offset = _line_start(line_starts, node.line)
980
+ end_offset = _line_start(line_starts, end_line + 1, default=len(source))
981
+ sections.append(
982
+ SectionSpan(
983
+ kind=kind,
984
+ start_line=node.line,
985
+ end_line=end_line,
986
+ start_offset=start_offset,
987
+ end_offset=end_offset,
988
+ context_hash=_context_hash(source, start_offset, end_offset),
989
+ )
990
+ )
991
+ declaration_nodes = [
992
+ child
993
+ for child in node.child_nodes
994
+ if child.typ in declaration_kinds and child.line > 0
995
+ ]
996
+ for declaration_index, declaration in enumerate(declaration_nodes):
997
+ declaration_end_line = (
998
+ declaration_nodes[declaration_index + 1].line - 1
999
+ if declaration_index + 1 < len(declaration_nodes)
1000
+ else end_line
1001
+ )
1002
+ declaration_start = _line_start(line_starts, declaration.line)
1003
+ declaration_end = _line_start(
1004
+ line_starts,
1005
+ declaration_end_line + 1,
1006
+ default=len(source),
1007
+ )
1008
+ declarations.append(
1009
+ DeclarationSpan(
1010
+ section=kind,
1011
+ kind=declaration_kinds[declaration.typ],
1012
+ start_line=declaration.line,
1013
+ end_line=declaration_end_line,
1014
+ start_offset=declaration_start,
1015
+ end_offset=declaration_end,
1016
+ context_hash=_context_hash(
1017
+ source,
1018
+ declaration_start,
1019
+ declaration_end,
1020
+ ),
1021
+ )
1022
+ )
1023
+ return tuple(sections), tuple(declarations)
1024
+
1025
+
1026
+ def _shift_boundary_spans(
1027
+ spans: tuple,
1028
+ edit: TextEdit,
1029
+ line_delta: int,
1030
+ character_delta: int,
1031
+ ) -> tuple:
1032
+ shifted_spans = []
1033
+ for span in spans:
1034
+ if span.start_offset >= edit.end:
1035
+ shifted = replace(
1036
+ span,
1037
+ start_line=span.start_line + line_delta,
1038
+ end_line=span.end_line + line_delta,
1039
+ start_offset=span.start_offset + character_delta,
1040
+ end_offset=span.end_offset + character_delta,
1041
+ )
1042
+ elif span.end_offset >= edit.end:
1043
+ shifted = replace(
1044
+ span,
1045
+ end_line=span.end_line + line_delta,
1046
+ end_offset=span.end_offset + character_delta,
1047
+ )
1048
+ else:
1049
+ shifted = span
1050
+ shifted_spans.append(shifted)
1051
+ return tuple(shifted_spans)
1052
+
1053
+
1054
+ def _shift_line_starts(
1055
+ line_starts: tuple[int, ...],
1056
+ edit: TextEdit,
1057
+ ) -> tuple[int, ...]:
1058
+ prefix_end = bisect_right(line_starts, edit.start)
1059
+ suffix_start = bisect_right(line_starts, edit.end)
1060
+ character_delta = len(edit.text) - (edit.end - edit.start)
1061
+ inserted = tuple(
1062
+ edit.start + index + 1
1063
+ for index, char in enumerate(edit.text)
1064
+ if char == "\n"
1065
+ )
1066
+ shifted_suffix = tuple(
1067
+ offset + character_delta
1068
+ for offset in line_starts[suffix_start:]
1069
+ )
1070
+ return (*line_starts[:prefix_end], *inserted, *shifted_suffix)
1071
+
1072
+
1073
+ def _line_starts(source: str) -> tuple[int, ...]:
1074
+ starts = [0]
1075
+ starts.extend(index + 1 for index, char in enumerate(source) if char == "\n")
1076
+ return tuple(starts)
1077
+
1078
+
1079
+ def _line_start(
1080
+ line_starts: tuple[int, ...],
1081
+ line: int,
1082
+ *,
1083
+ default: int | None = None,
1084
+ ) -> int:
1085
+ index = line - 1
1086
+ if 0 <= index < len(line_starts):
1087
+ return line_starts[index]
1088
+ if default is not None and index == len(line_starts):
1089
+ return default
1090
+ raise ValueError(f"line {line} is outside the source")
1091
+
1092
+
1093
+ def _touches_conditional_directive(source: str, edit: TextEdit) -> bool:
1094
+ line_start = source.rfind("\n", 0, edit.start) + 1
1095
+ line_end = source.find("\n", edit.end)
1096
+ if line_end < 0:
1097
+ line_end = len(source)
1098
+ return _contains_conditional_directive(source[line_start:line_end])
1099
+
1100
+
1101
+ def _contains_conditional_directive(text: str) -> bool:
1102
+ folded = text.casefold()
1103
+ return any(
1104
+ marker in folded
1105
+ for marker in ("{$if", "(*$if", "{$else", "{$endif")
1106
+ )
1107
+
1108
+
1109
+ def _context_hash(source: str, start: int, end: int) -> str:
1110
+ header_end = source.find("\n", start, end)
1111
+ header = source[start : end if header_end < 0 else header_end]
1112
+ closing_start = source.rfind("\n", start, max(start, end - 1)) + 1
1113
+ closing = source[closing_start:end].rstrip("\r\n")
1114
+ return _digest(f"{header}\n{closing}")
1115
+
1116
+
1117
+ def _newline_count(text: str) -> int:
1118
+ return text.count("\n")
1119
+
1120
+
1121
+ def _digest(text: str) -> str:
1122
+ return sha256(text.encode("utf-8")).hexdigest()
1123
+
1124
+
1125
+ __all__ = [
1126
+ "FULL_REPARSE",
1127
+ "DeclarationSpan",
1128
+ "EditKind",
1129
+ "EditPlan",
1130
+ "PathCounters",
1131
+ "RoutineSpan",
1132
+ "SectionSpan",
1133
+ "SpanIndex",
1134
+ "SplicedModel",
1135
+ "TextEdit",
1136
+ "apply_edit",
1137
+ "build_span_index",
1138
+ "reparse_span",
1139
+ ]