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.
- delphi_lsp/__init__.py +184 -0
- delphi_lsp/_version.py +1 -0
- delphi_lsp/agent_cache.py +1574 -0
- delphi_lsp/agent_cli.py +902 -0
- delphi_lsp/agent_context.py +3907 -0
- delphi_lsp/agent_cpg.py +349 -0
- delphi_lsp/agent_cpg_builder.py +666 -0
- delphi_lsp/agent_layers.py +1410 -0
- delphi_lsp/agent_metrics.py +526 -0
- delphi_lsp/agent_protocol.py +445 -0
- delphi_lsp/agent_relations.py +1881 -0
- delphi_lsp/agent_templates.py +614 -0
- delphi_lsp/agent_wiki.py +2853 -0
- delphi_lsp/agent_workspace.py +860 -0
- delphi_lsp/ast_serialize.py +126 -0
- delphi_lsp/binary.py +250 -0
- delphi_lsp/comment_builder.py +28 -0
- delphi_lsp/consts.py +345 -0
- delphi_lsp/delphi_lsp_native.pyd +0 -0
- delphi_lsp/delphiast_lexer.py +399 -0
- delphi_lsp/delphiast_parser.py +2189 -0
- delphi_lsp/delphiast_tokens.py +41 -0
- delphi_lsp/grammar.py +459 -0
- delphi_lsp/incremental.py +1139 -0
- delphi_lsp/lark_builder.py +2684 -0
- delphi_lsp/lark_tokens.py +236 -0
- delphi_lsp/lexical_scanner.py +30 -0
- delphi_lsp/lsp_server.py +6698 -0
- delphi_lsp/metrics.py +1452 -0
- delphi_lsp/native_bridge.py +300 -0
- delphi_lsp/navigation_cache.py +189 -0
- delphi_lsp/nodes.py +462 -0
- delphi_lsp/parallel_outline.py +662 -0
- delphi_lsp/parse_artifacts.py +969 -0
- delphi_lsp/parser.py +342 -0
- delphi_lsp/parser_backend.py +139 -0
- delphi_lsp/preprocessor.py +1122 -0
- delphi_lsp/progress.py +26 -0
- delphi_lsp/project_config.py +285 -0
- delphi_lsp/project_discovery.py +932 -0
- delphi_lsp/project_indexer.py +470 -0
- delphi_lsp/py.typed +1 -0
- delphi_lsp/response_cache.py +465 -0
- delphi_lsp/semantic.py +394 -0
- delphi_lsp/semantic_builder.py +2302 -0
- delphi_lsp/source_reader.py +17 -0
- delphi_lsp/wiki_external_sort.py +392 -0
- delphi_lsp/wiki_memory.py +548 -0
- delphi_lsp/workspace.py +83 -0
- delphi_lsp/writer.py +73 -0
- python_delphi_lsp-3.6.0.dist-info/METADATA +766 -0
- python_delphi_lsp-3.6.0.dist-info/RECORD +56 -0
- python_delphi_lsp-3.6.0.dist-info/WHEEL +4 -0
- python_delphi_lsp-3.6.0.dist-info/entry_points.txt +3 -0
- python_delphi_lsp-3.6.0.dist-info/licenses/LICENSE +373 -0
- 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
|
+
]
|