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,860 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import hashlib
|
|
4
|
+
import json
|
|
5
|
+
import os
|
|
6
|
+
import sys
|
|
7
|
+
import unicodedata
|
|
8
|
+
from dataclasses import dataclass
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
|
|
11
|
+
from .agent_protocol import AgentProtocolError, Focus, make_target_id
|
|
12
|
+
from .consts import AttributeName, SyntaxNodeType
|
|
13
|
+
from .lsp_server import multiline_string_block_end, outline_source
|
|
14
|
+
from .project_config import ProjectPathConfig
|
|
15
|
+
from .project_discovery import (
|
|
16
|
+
SKIP_DIRS,
|
|
17
|
+
SOURCE_EXTENSIONS,
|
|
18
|
+
DelphiProjectDiscovery,
|
|
19
|
+
discover_delphi_project,
|
|
20
|
+
discover_workspace_sources,
|
|
21
|
+
populate_workspace_sources,
|
|
22
|
+
)
|
|
23
|
+
from .project_indexer import (
|
|
24
|
+
IncludeFileInfo,
|
|
25
|
+
ProjectIndexer,
|
|
26
|
+
ProjectIndexResult,
|
|
27
|
+
UnitInfo,
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
_PROJECT_SNAPSHOT_EXTENSIONS = (".pas", ".dpr", ".dpk", ".dproj", ".cfg", ".dof")
|
|
31
|
+
_UNIT_SNAPSHOT_EXTENSIONS = (".pas", ".dpr", ".dpk")
|
|
32
|
+
_RELATIVE_INCLUDE_EXTENSIONS = (".inc",)
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
@dataclass(frozen=True)
|
|
36
|
+
class AgentProject:
|
|
37
|
+
project_id: str
|
|
38
|
+
name: str
|
|
39
|
+
path: str
|
|
40
|
+
kind: str
|
|
41
|
+
|
|
42
|
+
def to_mapping(self) -> dict[str, str]:
|
|
43
|
+
return {
|
|
44
|
+
"project_id": self.project_id,
|
|
45
|
+
"name": self.name,
|
|
46
|
+
"path": self.path,
|
|
47
|
+
"kind": self.kind,
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
@dataclass(frozen=True)
|
|
52
|
+
class AgentUnit:
|
|
53
|
+
unit_id: str
|
|
54
|
+
name: str
|
|
55
|
+
path: str
|
|
56
|
+
has_error: bool
|
|
57
|
+
|
|
58
|
+
def to_mapping(self) -> dict[str, str | bool]:
|
|
59
|
+
return {
|
|
60
|
+
"unit_id": self.unit_id,
|
|
61
|
+
"name": self.name,
|
|
62
|
+
"path": self.path,
|
|
63
|
+
"has_error": self.has_error,
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
@dataclass(frozen=True)
|
|
68
|
+
class _ProjectCache:
|
|
69
|
+
result: ProjectIndexResult
|
|
70
|
+
fingerprint: str
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
@dataclass
|
|
74
|
+
class _DirectorySnapshotSpec:
|
|
75
|
+
path: Path
|
|
76
|
+
immediate_extensions: set[str]
|
|
77
|
+
recursive_extensions: set[str]
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
class AgentWorkspace:
|
|
81
|
+
def __init__(
|
|
82
|
+
self,
|
|
83
|
+
root: Path,
|
|
84
|
+
discovery: DelphiProjectDiscovery,
|
|
85
|
+
projects: tuple[AgentProject, ...],
|
|
86
|
+
project_paths: dict[str, Path | None],
|
|
87
|
+
) -> None:
|
|
88
|
+
self._root = root
|
|
89
|
+
self._discovery = discovery
|
|
90
|
+
self._projects = projects
|
|
91
|
+
self._project_paths = project_paths
|
|
92
|
+
self._active_project_id = ""
|
|
93
|
+
self._focus = Focus()
|
|
94
|
+
self._active_discovery: DelphiProjectDiscovery | None = None
|
|
95
|
+
self._active_result: ProjectIndexResult | None = None
|
|
96
|
+
self._units: tuple[AgentUnit, ...] = ()
|
|
97
|
+
self._include_files: tuple[dict[str, str], ...] = ()
|
|
98
|
+
self._project_cache: dict[str, _ProjectCache] = {}
|
|
99
|
+
self._current_revision = ""
|
|
100
|
+
|
|
101
|
+
@classmethod
|
|
102
|
+
def open(
|
|
103
|
+
cls,
|
|
104
|
+
root: str | Path,
|
|
105
|
+
project_file: str | Path | None = None,
|
|
106
|
+
) -> AgentWorkspace:
|
|
107
|
+
root_path = Path(root).expanduser().resolve()
|
|
108
|
+
resolved_project_file: Path | None = None
|
|
109
|
+
if project_file is not None:
|
|
110
|
+
resolved_project_file = Path(project_file).expanduser()
|
|
111
|
+
if not resolved_project_file.is_absolute():
|
|
112
|
+
resolved_project_file = root_path / resolved_project_file
|
|
113
|
+
resolved_project_file = resolved_project_file.resolve()
|
|
114
|
+
discovery = discover_delphi_project(
|
|
115
|
+
root_path,
|
|
116
|
+
project_file=resolved_project_file,
|
|
117
|
+
scan_workspace_sources=False,
|
|
118
|
+
)
|
|
119
|
+
projects: list[AgentProject] = []
|
|
120
|
+
project_paths: dict[str, Path | None] = {}
|
|
121
|
+
default_project_id = ""
|
|
122
|
+
if (
|
|
123
|
+
resolved_project_file is None
|
|
124
|
+
and len(discovery.project_files) > 1
|
|
125
|
+
):
|
|
126
|
+
default_project_id = make_target_id("project", "", "workspace")
|
|
127
|
+
projects.append(
|
|
128
|
+
AgentProject(
|
|
129
|
+
project_id=default_project_id,
|
|
130
|
+
name="Workspace",
|
|
131
|
+
path=".",
|
|
132
|
+
kind="workspace",
|
|
133
|
+
)
|
|
134
|
+
)
|
|
135
|
+
project_paths[default_project_id] = None
|
|
136
|
+
if discovery.project_files:
|
|
137
|
+
for value in discovery.project_files:
|
|
138
|
+
path = Path(value)
|
|
139
|
+
relative_path = _display_path(path, root_path)
|
|
140
|
+
name = path.stem
|
|
141
|
+
project_id = make_target_id("project", relative_path, name)
|
|
142
|
+
project = AgentProject(
|
|
143
|
+
project_id=project_id,
|
|
144
|
+
name=name,
|
|
145
|
+
path=relative_path,
|
|
146
|
+
kind=_project_kind(path),
|
|
147
|
+
)
|
|
148
|
+
projects.append(project)
|
|
149
|
+
project_paths[project_id] = path
|
|
150
|
+
else:
|
|
151
|
+
populate_workspace_sources(discovery)
|
|
152
|
+
project_id = make_target_id("project", "", "workspace")
|
|
153
|
+
projects.append(
|
|
154
|
+
AgentProject(
|
|
155
|
+
project_id=project_id,
|
|
156
|
+
name="Workspace",
|
|
157
|
+
path=".",
|
|
158
|
+
kind="workspace",
|
|
159
|
+
)
|
|
160
|
+
)
|
|
161
|
+
project_paths[project_id] = None
|
|
162
|
+
|
|
163
|
+
workspace = cls(root_path, discovery, tuple(projects), project_paths)
|
|
164
|
+
if default_project_id:
|
|
165
|
+
workspace.select_project(default_project_id)
|
|
166
|
+
elif len(projects) == 1:
|
|
167
|
+
workspace.select_project(projects[0].project_id)
|
|
168
|
+
return workspace
|
|
169
|
+
|
|
170
|
+
@property
|
|
171
|
+
def projects(self) -> tuple[AgentProject, ...]:
|
|
172
|
+
return self._projects
|
|
173
|
+
|
|
174
|
+
@property
|
|
175
|
+
def root(self) -> Path:
|
|
176
|
+
return self._root
|
|
177
|
+
|
|
178
|
+
@property
|
|
179
|
+
def search_paths(self) -> tuple[str, ...]:
|
|
180
|
+
discovery = self._active_discovery or self._discovery
|
|
181
|
+
return tuple(discovery.search_paths)
|
|
182
|
+
|
|
183
|
+
@property
|
|
184
|
+
def include_paths(self) -> tuple[str, ...]:
|
|
185
|
+
discovery = self._active_discovery or self._discovery
|
|
186
|
+
return tuple(discovery.include_paths)
|
|
187
|
+
|
|
188
|
+
@property
|
|
189
|
+
def defines(self) -> tuple[str, ...]:
|
|
190
|
+
discovery = self._active_discovery or self._discovery
|
|
191
|
+
return tuple(discovery.defines)
|
|
192
|
+
|
|
193
|
+
@property
|
|
194
|
+
def project_config(self) -> ProjectPathConfig | None:
|
|
195
|
+
discovery = self._active_discovery or self._discovery
|
|
196
|
+
return discovery.project_config
|
|
197
|
+
|
|
198
|
+
@property
|
|
199
|
+
def active_project(self) -> AgentProject | None:
|
|
200
|
+
return next(
|
|
201
|
+
(project for project in self._projects if project.project_id == self._active_project_id),
|
|
202
|
+
None,
|
|
203
|
+
)
|
|
204
|
+
|
|
205
|
+
@property
|
|
206
|
+
def active_project_id(self) -> str:
|
|
207
|
+
return self._active_project_id
|
|
208
|
+
|
|
209
|
+
def cache_roots(self) -> tuple[object, ...]:
|
|
210
|
+
return (self._project_cache, self._active_result)
|
|
211
|
+
|
|
212
|
+
@property
|
|
213
|
+
def estimated_cache_bytes(self) -> int:
|
|
214
|
+
retained = 512
|
|
215
|
+
seen_results: set[int] = set()
|
|
216
|
+
for cached in self._project_cache.values():
|
|
217
|
+
result = cached.result
|
|
218
|
+
if id(result) in seen_results:
|
|
219
|
+
continue
|
|
220
|
+
seen_results.add(id(result))
|
|
221
|
+
retained += _estimate_project_result_bytes(result)
|
|
222
|
+
if self._active_result is not None and id(self._active_result) not in seen_results:
|
|
223
|
+
retained += _estimate_project_result_bytes(self._active_result)
|
|
224
|
+
return retained
|
|
225
|
+
|
|
226
|
+
def evict_recomputable_caches(self) -> None:
|
|
227
|
+
self._project_cache.clear()
|
|
228
|
+
self._active_result = None
|
|
229
|
+
|
|
230
|
+
@property
|
|
231
|
+
def focus(self) -> Focus:
|
|
232
|
+
return self._focus
|
|
233
|
+
|
|
234
|
+
@property
|
|
235
|
+
def units(self) -> tuple[AgentUnit, ...]:
|
|
236
|
+
return self._units
|
|
237
|
+
|
|
238
|
+
@property
|
|
239
|
+
def include_files(self) -> tuple[dict[str, str], ...]:
|
|
240
|
+
return tuple(dict(item) for item in self._include_files)
|
|
241
|
+
|
|
242
|
+
def include_source_path(self, display_path: str) -> Path:
|
|
243
|
+
if self._active_result is not None:
|
|
244
|
+
for include in self._active_result.include_files:
|
|
245
|
+
source_path = Path(include.path).expanduser().resolve()
|
|
246
|
+
if _display_path(source_path, self._root) == display_path:
|
|
247
|
+
return source_path
|
|
248
|
+
path = Path(display_path).expanduser()
|
|
249
|
+
if not path.is_absolute():
|
|
250
|
+
path = self._root / path
|
|
251
|
+
return path.resolve()
|
|
252
|
+
|
|
253
|
+
def include_display_path(self, display_path: str) -> str:
|
|
254
|
+
source_path = self.include_source_path(display_path)
|
|
255
|
+
try:
|
|
256
|
+
return source_path.relative_to(self._root).as_posix()
|
|
257
|
+
except ValueError:
|
|
258
|
+
return f"@external/include/{_stable_path_component(source_path.name)}"
|
|
259
|
+
|
|
260
|
+
@property
|
|
261
|
+
def search_path_entries(self) -> tuple[dict[str, object], ...]:
|
|
262
|
+
discovery = self._active_discovery or self._discovery
|
|
263
|
+
return self._path_entries(discovery.search_paths, discovery.search_path_origins)
|
|
264
|
+
|
|
265
|
+
@property
|
|
266
|
+
def include_path_entries(self) -> tuple[dict[str, object], ...]:
|
|
267
|
+
discovery = self._active_discovery or self._discovery
|
|
268
|
+
return self._path_entries(discovery.include_paths, discovery.include_path_origins)
|
|
269
|
+
|
|
270
|
+
@property
|
|
271
|
+
def define_entries(self) -> tuple[dict[str, object], ...]:
|
|
272
|
+
discovery = self._active_discovery or self._discovery
|
|
273
|
+
return tuple(
|
|
274
|
+
{
|
|
275
|
+
"define": define,
|
|
276
|
+
"origins": [self._display_origin(origin) for origin in discovery.define_origins.get(define, [])],
|
|
277
|
+
}
|
|
278
|
+
for define in discovery.defines
|
|
279
|
+
)
|
|
280
|
+
|
|
281
|
+
@property
|
|
282
|
+
def problems(self) -> tuple[dict[str, str], ...]:
|
|
283
|
+
entries: list[dict[str, str]] = []
|
|
284
|
+
seen_discovery: set[tuple[str, str, str]] = set()
|
|
285
|
+
discovery = self._active_discovery or self._discovery
|
|
286
|
+
for problem in discovery.problems:
|
|
287
|
+
key = (problem.kind, problem.message, problem.origin)
|
|
288
|
+
if key in seen_discovery:
|
|
289
|
+
continue
|
|
290
|
+
seen_discovery.add(key)
|
|
291
|
+
entries.append(
|
|
292
|
+
{
|
|
293
|
+
"kind": problem.kind,
|
|
294
|
+
"message": problem.message,
|
|
295
|
+
"origin": self._display_origin(problem.origin),
|
|
296
|
+
}
|
|
297
|
+
)
|
|
298
|
+
|
|
299
|
+
active_project = self.active_project
|
|
300
|
+
if active_project is not None and self._active_result is not None:
|
|
301
|
+
seen_project: set[tuple[str, str, str]] = set()
|
|
302
|
+
for problem in self._active_result.problems:
|
|
303
|
+
key = (problem.problem_type.value, problem.file_name, problem.description)
|
|
304
|
+
if key in seen_project:
|
|
305
|
+
continue
|
|
306
|
+
seen_project.add(key)
|
|
307
|
+
entries.append(
|
|
308
|
+
{
|
|
309
|
+
"kind": problem.problem_type.value,
|
|
310
|
+
"message": problem.description,
|
|
311
|
+
"origin": active_project.path,
|
|
312
|
+
"path": self._display_problem_path(problem.file_name),
|
|
313
|
+
}
|
|
314
|
+
)
|
|
315
|
+
return tuple(entries)
|
|
316
|
+
|
|
317
|
+
@property
|
|
318
|
+
def workspace_revision(self) -> str:
|
|
319
|
+
return self.refresh_revision()
|
|
320
|
+
|
|
321
|
+
@property
|
|
322
|
+
def current_revision(self) -> str:
|
|
323
|
+
if not self._current_revision:
|
|
324
|
+
return self.refresh_revision()
|
|
325
|
+
return self._current_revision
|
|
326
|
+
|
|
327
|
+
def refresh_revision(self) -> str:
|
|
328
|
+
discovery = self._active_discovery or self._discovery
|
|
329
|
+
result = self._active_result
|
|
330
|
+
if self._active_project_id:
|
|
331
|
+
project_path = self._project_paths[self._active_project_id]
|
|
332
|
+
if project_path is None:
|
|
333
|
+
discovery = discover_workspace_sources(self._root)
|
|
334
|
+
result = _catalog_workspace_sources(discovery)
|
|
335
|
+
else:
|
|
336
|
+
discovery = discover_delphi_project(
|
|
337
|
+
self._root,
|
|
338
|
+
project_file=project_path,
|
|
339
|
+
scan_workspace_sources=False,
|
|
340
|
+
)
|
|
341
|
+
fingerprint = _selection_fingerprint(discovery, result, root=self._root)
|
|
342
|
+
self._current_revision = f"workspace_v2_{fingerprint}"
|
|
343
|
+
return self._current_revision
|
|
344
|
+
|
|
345
|
+
def select_project(self, project_id: str) -> None:
|
|
346
|
+
self._select_project_with_revision(project_id)
|
|
347
|
+
|
|
348
|
+
def _select_project_with_revision(self, project_id: str) -> str:
|
|
349
|
+
if project_id not in self._project_paths:
|
|
350
|
+
raise AgentProtocolError("project_not_found", f"Project not found: {project_id}.")
|
|
351
|
+
project_path = self._project_paths[project_id]
|
|
352
|
+
cached = self._project_cache.get(project_id)
|
|
353
|
+
if project_path is None:
|
|
354
|
+
discovery = (
|
|
355
|
+
self._discovery
|
|
356
|
+
if cached is None and self._discovery.source_files
|
|
357
|
+
else discover_workspace_sources(self._root)
|
|
358
|
+
)
|
|
359
|
+
else:
|
|
360
|
+
discovery = discover_delphi_project(
|
|
361
|
+
self._root,
|
|
362
|
+
project_file=project_path,
|
|
363
|
+
scan_workspace_sources=False,
|
|
364
|
+
)
|
|
365
|
+
if cached is not None:
|
|
366
|
+
fingerprint = _selection_fingerprint(discovery, cached.result, root=self._root)
|
|
367
|
+
if fingerprint == cached.fingerprint:
|
|
368
|
+
self._activate_project(project_id, discovery, cached.result)
|
|
369
|
+
self._current_revision = f"workspace_v2_{fingerprint}"
|
|
370
|
+
return self._current_revision
|
|
371
|
+
|
|
372
|
+
if project_path is None:
|
|
373
|
+
result = _catalog_workspace_sources(discovery)
|
|
374
|
+
else:
|
|
375
|
+
indexer = ProjectIndexer(
|
|
376
|
+
search_paths=discovery.search_paths,
|
|
377
|
+
include_paths=discovery.include_paths,
|
|
378
|
+
defines=discovery.defines,
|
|
379
|
+
source_transform=_outline_agent_source,
|
|
380
|
+
project_config=discovery.project_config,
|
|
381
|
+
)
|
|
382
|
+
result = indexer.index(str(project_path))
|
|
383
|
+
fingerprint = _selection_fingerprint(discovery, result, root=self._root)
|
|
384
|
+
self._project_cache[project_id] = _ProjectCache(
|
|
385
|
+
result=result,
|
|
386
|
+
fingerprint=fingerprint,
|
|
387
|
+
)
|
|
388
|
+
self._activate_project(project_id, discovery, result)
|
|
389
|
+
self._current_revision = f"workspace_v2_{fingerprint}"
|
|
390
|
+
return self._current_revision
|
|
391
|
+
|
|
392
|
+
def _activate_project(
|
|
393
|
+
self,
|
|
394
|
+
project_id: str,
|
|
395
|
+
discovery: DelphiProjectDiscovery,
|
|
396
|
+
result: ProjectIndexResult,
|
|
397
|
+
) -> None:
|
|
398
|
+
units = []
|
|
399
|
+
for item in result.parsed_units:
|
|
400
|
+
relative_path = _display_path(Path(item.path), self._root)
|
|
401
|
+
units.append(
|
|
402
|
+
AgentUnit(
|
|
403
|
+
unit_id=make_target_id("unit", relative_path, item.name),
|
|
404
|
+
name=item.name,
|
|
405
|
+
path=relative_path,
|
|
406
|
+
has_error=item.has_error,
|
|
407
|
+
)
|
|
408
|
+
)
|
|
409
|
+
units.sort(key=lambda item: (item.name.casefold(), item.path.casefold(), item.name, item.path))
|
|
410
|
+
include_files = tuple(
|
|
411
|
+
{
|
|
412
|
+
"name": item.name,
|
|
413
|
+
"path": _display_path(Path(item.path), self._root),
|
|
414
|
+
}
|
|
415
|
+
for item in sorted(
|
|
416
|
+
result.include_files,
|
|
417
|
+
key=lambda item: (item.name.casefold(), item.path.casefold(), item.name, item.path),
|
|
418
|
+
)
|
|
419
|
+
)
|
|
420
|
+
|
|
421
|
+
self._active_discovery = discovery
|
|
422
|
+
self._active_result = result
|
|
423
|
+
if self._project_paths[project_id] is None:
|
|
424
|
+
self._discovery = discovery
|
|
425
|
+
self._units = tuple(units)
|
|
426
|
+
self._include_files = include_files
|
|
427
|
+
self._active_project_id = project_id
|
|
428
|
+
self._focus = Focus(project_id=project_id, unit_id="", target_id="")
|
|
429
|
+
|
|
430
|
+
def _path_entries(
|
|
431
|
+
self,
|
|
432
|
+
paths: list[str],
|
|
433
|
+
origins: dict[str, list[str]],
|
|
434
|
+
) -> tuple[dict[str, object], ...]:
|
|
435
|
+
return tuple(
|
|
436
|
+
{
|
|
437
|
+
"path": _display_path(Path(path), self._root),
|
|
438
|
+
"origins": [self._display_origin(origin) for origin in origins.get(path, [])],
|
|
439
|
+
}
|
|
440
|
+
for path in paths
|
|
441
|
+
)
|
|
442
|
+
|
|
443
|
+
def _display_origin(self, origin: str) -> str:
|
|
444
|
+
path = Path(origin).expanduser()
|
|
445
|
+
if path.is_absolute():
|
|
446
|
+
return _display_path(path, self._root)
|
|
447
|
+
return origin.replace("\\", "/")
|
|
448
|
+
|
|
449
|
+
def _display_problem_path(self, value: str) -> str:
|
|
450
|
+
path = Path(value).expanduser()
|
|
451
|
+
if path.is_absolute():
|
|
452
|
+
return _display_path(path, self._root)
|
|
453
|
+
return value.replace("\\", "/")
|
|
454
|
+
|
|
455
|
+
|
|
456
|
+
def _display_path(path: Path, root: Path) -> str:
|
|
457
|
+
resolved = path.expanduser().resolve()
|
|
458
|
+
try:
|
|
459
|
+
return resolved.relative_to(root).as_posix()
|
|
460
|
+
except ValueError:
|
|
461
|
+
return resolved.as_posix()
|
|
462
|
+
|
|
463
|
+
|
|
464
|
+
def unit_source_path(root: Path, unit: AgentUnit) -> Path:
|
|
465
|
+
path = Path(unit.path)
|
|
466
|
+
if not path.is_absolute():
|
|
467
|
+
path = root / path
|
|
468
|
+
return path.expanduser().resolve()
|
|
469
|
+
|
|
470
|
+
|
|
471
|
+
def unit_display_path(root: Path, unit: AgentUnit) -> str:
|
|
472
|
+
source_path = unit_source_path(root, unit)
|
|
473
|
+
try:
|
|
474
|
+
return source_path.relative_to(root).as_posix()
|
|
475
|
+
except ValueError:
|
|
476
|
+
unit_component = _stable_path_component(unit.name or source_path.stem)
|
|
477
|
+
file_component = _stable_path_component(source_path.name)
|
|
478
|
+
return f"@external/{unit_component}/{file_component}"
|
|
479
|
+
|
|
480
|
+
|
|
481
|
+
def unit_target_id(root: Path, unit: AgentUnit) -> str:
|
|
482
|
+
return make_target_id("unit", unit_display_path(root, unit), unit.name)
|
|
483
|
+
|
|
484
|
+
|
|
485
|
+
def _stable_path_component(value: str) -> str:
|
|
486
|
+
normalized = unicodedata.normalize("NFC", value).replace("\\", "_").replace("/", "_")
|
|
487
|
+
return normalized or "unknown"
|
|
488
|
+
|
|
489
|
+
|
|
490
|
+
def _outline_agent_source(text: str) -> str:
|
|
491
|
+
return _compact_outline_whitespace(outline_source(text))
|
|
492
|
+
|
|
493
|
+
|
|
494
|
+
def _compact_outline_whitespace(text: str) -> str:
|
|
495
|
+
parts: list[str] = []
|
|
496
|
+
i = 0
|
|
497
|
+
n = len(text)
|
|
498
|
+
while i < n:
|
|
499
|
+
if text[i].isspace():
|
|
500
|
+
start = i
|
|
501
|
+
while i < n and text[i].isspace():
|
|
502
|
+
i += 1
|
|
503
|
+
if parts and i < n and not parts[-1][-1].isspace():
|
|
504
|
+
parts.append(_collapsed_whitespace(text, start, i))
|
|
505
|
+
continue
|
|
506
|
+
|
|
507
|
+
start = i
|
|
508
|
+
if text[i] == "'":
|
|
509
|
+
block_end = multiline_string_block_end(text, i)
|
|
510
|
+
i = block_end if block_end is not None else _quoted_string_end(text, i)
|
|
511
|
+
elif text.startswith("//", i):
|
|
512
|
+
i = _line_comment_end(text, i)
|
|
513
|
+
elif text[i] == "{":
|
|
514
|
+
close = text.find("}", i + 1)
|
|
515
|
+
i = n if close < 0 else close + 1
|
|
516
|
+
elif text.startswith("(*", i):
|
|
517
|
+
close = text.find("*)", i + 2)
|
|
518
|
+
i = n if close < 0 else close + 2
|
|
519
|
+
else:
|
|
520
|
+
i += 1
|
|
521
|
+
while i < n:
|
|
522
|
+
if text[i].isspace() or text[i] in {"'", "{"}:
|
|
523
|
+
break
|
|
524
|
+
if text.startswith("//", i) or text.startswith("(*", i):
|
|
525
|
+
break
|
|
526
|
+
i += 1
|
|
527
|
+
parts.append(text[start:i])
|
|
528
|
+
return "".join(parts)
|
|
529
|
+
|
|
530
|
+
|
|
531
|
+
def _collapsed_whitespace(text: str, start: int, end: int) -> str:
|
|
532
|
+
for index in range(start, end):
|
|
533
|
+
if text[index] == "\r":
|
|
534
|
+
if index + 1 < end and text[index + 1] == "\n":
|
|
535
|
+
return "\r\n"
|
|
536
|
+
return "\r"
|
|
537
|
+
if text[index] == "\n":
|
|
538
|
+
return "\n"
|
|
539
|
+
return " "
|
|
540
|
+
|
|
541
|
+
|
|
542
|
+
def _quoted_string_end(text: str, start: int) -> int:
|
|
543
|
+
i = start + 1
|
|
544
|
+
while i < len(text):
|
|
545
|
+
if text[i] != "'":
|
|
546
|
+
i += 1
|
|
547
|
+
continue
|
|
548
|
+
if i + 1 < len(text) and text[i + 1] == "'":
|
|
549
|
+
i += 2
|
|
550
|
+
continue
|
|
551
|
+
return i + 1
|
|
552
|
+
return len(text)
|
|
553
|
+
|
|
554
|
+
|
|
555
|
+
def _line_comment_end(text: str, start: int) -> int:
|
|
556
|
+
i = start + 2
|
|
557
|
+
while i < len(text) and text[i] not in {"\r", "\n"}:
|
|
558
|
+
i += 1
|
|
559
|
+
if i < len(text) and text[i] == "\r" and i + 1 < len(text) and text[i + 1] == "\n":
|
|
560
|
+
return i + 2
|
|
561
|
+
if i < len(text):
|
|
562
|
+
return i + 1
|
|
563
|
+
return i
|
|
564
|
+
|
|
565
|
+
|
|
566
|
+
def _project_kind(path: Path) -> str:
|
|
567
|
+
if path.suffix.casefold() == ".dpr":
|
|
568
|
+
return "program"
|
|
569
|
+
if path.suffix.casefold() == ".dpk":
|
|
570
|
+
return "package"
|
|
571
|
+
return "project"
|
|
572
|
+
|
|
573
|
+
|
|
574
|
+
def _catalog_workspace_sources(discovery: DelphiProjectDiscovery) -> ProjectIndexResult:
|
|
575
|
+
parsed_units: list[UnitInfo] = []
|
|
576
|
+
include_files: list[IncludeFileInfo] = []
|
|
577
|
+
for source in discovery.source_files:
|
|
578
|
+
path = Path(source)
|
|
579
|
+
if path.suffix.casefold() in {".pas", ".dpr", ".dpk"}:
|
|
580
|
+
parsed_units.append(UnitInfo(name=path.stem, path=source, syntax_tree=None))
|
|
581
|
+
elif path.suffix.casefold() == ".inc":
|
|
582
|
+
include_files.append(IncludeFileInfo(name=path.name, path=source))
|
|
583
|
+
|
|
584
|
+
return ProjectIndexResult(
|
|
585
|
+
parsed_units=sorted(
|
|
586
|
+
parsed_units,
|
|
587
|
+
key=lambda item: (item.name.casefold(), item.path.casefold(), item.name, item.path),
|
|
588
|
+
),
|
|
589
|
+
include_files=sorted(
|
|
590
|
+
include_files,
|
|
591
|
+
key=lambda item: (item.name.casefold(), item.path.casefold(), item.name, item.path),
|
|
592
|
+
),
|
|
593
|
+
problems=[],
|
|
594
|
+
not_found_units=[],
|
|
595
|
+
)
|
|
596
|
+
|
|
597
|
+
|
|
598
|
+
def _selection_fingerprint(
|
|
599
|
+
discovery: DelphiProjectDiscovery,
|
|
600
|
+
result: ProjectIndexResult | None,
|
|
601
|
+
*,
|
|
602
|
+
root: Path,
|
|
603
|
+
) -> str:
|
|
604
|
+
state = _selection_state(discovery, result, root=root)
|
|
605
|
+
encoded = json.dumps(
|
|
606
|
+
state,
|
|
607
|
+
ensure_ascii=False,
|
|
608
|
+
sort_keys=True,
|
|
609
|
+
separators=(",", ":"),
|
|
610
|
+
).encode("utf-8")
|
|
611
|
+
return hashlib.sha256(encoded).hexdigest()
|
|
612
|
+
|
|
613
|
+
|
|
614
|
+
def _estimate_project_result_bytes(result: ProjectIndexResult) -> int:
|
|
615
|
+
retained = 2048
|
|
616
|
+
retained += sum(
|
|
617
|
+
768 + sys.getsizeof(unit.name) + sys.getsizeof(unit.path)
|
|
618
|
+
for unit in result.parsed_units
|
|
619
|
+
)
|
|
620
|
+
retained += sum(
|
|
621
|
+
384 + sys.getsizeof(include.name) + sys.getsizeof(include.path)
|
|
622
|
+
for include in result.include_files
|
|
623
|
+
)
|
|
624
|
+
retained += sum(
|
|
625
|
+
512
|
|
626
|
+
+ sys.getsizeof(problem.file_name)
|
|
627
|
+
+ sys.getsizeof(problem.description)
|
|
628
|
+
for problem in result.problems
|
|
629
|
+
)
|
|
630
|
+
retained += sum(sys.getsizeof(name) + 96 for name in result.not_found_units)
|
|
631
|
+
return retained
|
|
632
|
+
|
|
633
|
+
|
|
634
|
+
def _selection_state(
|
|
635
|
+
discovery: DelphiProjectDiscovery,
|
|
636
|
+
result: ProjectIndexResult | None,
|
|
637
|
+
*,
|
|
638
|
+
root: Path,
|
|
639
|
+
) -> dict[str, object]:
|
|
640
|
+
return {
|
|
641
|
+
"files": _file_records(_selection_paths(discovery, result), root=root),
|
|
642
|
+
"search_paths": [_display_path(Path(path), root) for path in discovery.search_paths],
|
|
643
|
+
"include_paths": [_display_path(Path(path), root) for path in discovery.include_paths],
|
|
644
|
+
"defines": sorted(discovery.defines, key=lambda item: (item.casefold(), item)),
|
|
645
|
+
"directories": _directory_snapshots(
|
|
646
|
+
_selection_snapshot_specs(discovery, result),
|
|
647
|
+
root=root,
|
|
648
|
+
project_config=discovery.project_config,
|
|
649
|
+
),
|
|
650
|
+
}
|
|
651
|
+
|
|
652
|
+
|
|
653
|
+
def _selection_paths(
|
|
654
|
+
discovery: DelphiProjectDiscovery,
|
|
655
|
+
result: ProjectIndexResult | None,
|
|
656
|
+
) -> set[str]:
|
|
657
|
+
paths = {
|
|
658
|
+
*discovery.project_files,
|
|
659
|
+
*discovery.config_files,
|
|
660
|
+
*discovery.source_files,
|
|
661
|
+
}
|
|
662
|
+
if result is not None:
|
|
663
|
+
paths.update(unit.path for unit in result.parsed_units)
|
|
664
|
+
paths.update(include.path for include in result.include_files)
|
|
665
|
+
paths.update(_explicit_dependency_paths(result, discovery.project_config))
|
|
666
|
+
if discovery.project_config is None:
|
|
667
|
+
return paths
|
|
668
|
+
return {
|
|
669
|
+
path
|
|
670
|
+
for path in paths
|
|
671
|
+
if not discovery.project_config.excludes_workspace_path(path)
|
|
672
|
+
}
|
|
673
|
+
|
|
674
|
+
|
|
675
|
+
def _explicit_dependency_paths(
|
|
676
|
+
result: ProjectIndexResult,
|
|
677
|
+
project_config: ProjectPathConfig | None,
|
|
678
|
+
) -> set[str]:
|
|
679
|
+
paths: dict[str, str] = {}
|
|
680
|
+
for unit in sorted(
|
|
681
|
+
result.parsed_units,
|
|
682
|
+
key=lambda item: (item.path.casefold(), item.path, item.name.casefold(), item.name),
|
|
683
|
+
):
|
|
684
|
+
if unit.syntax_tree is None:
|
|
685
|
+
continue
|
|
686
|
+
declaring_folder = Path(unit.path).expanduser().resolve().parent
|
|
687
|
+
pending = [unit.syntax_tree]
|
|
688
|
+
while pending:
|
|
689
|
+
node = pending.pop()
|
|
690
|
+
if node.typ == SyntaxNodeType.ntUnit:
|
|
691
|
+
raw_path = node.get_attribute(AttributeName.anPath).strip()
|
|
692
|
+
if raw_path:
|
|
693
|
+
candidate = Path(raw_path.replace("\\", "/"))
|
|
694
|
+
if not candidate.is_absolute():
|
|
695
|
+
candidate = declaring_folder / candidate
|
|
696
|
+
resolved = str(candidate.resolve())
|
|
697
|
+
if (
|
|
698
|
+
project_config is None
|
|
699
|
+
or not project_config.excludes_workspace_path(resolved)
|
|
700
|
+
):
|
|
701
|
+
paths.setdefault(resolved.casefold(), resolved)
|
|
702
|
+
pending.extend(reversed(node.child_nodes))
|
|
703
|
+
return set(paths.values())
|
|
704
|
+
|
|
705
|
+
|
|
706
|
+
def _selection_snapshot_specs(
|
|
707
|
+
discovery: DelphiProjectDiscovery,
|
|
708
|
+
result: ProjectIndexResult | None,
|
|
709
|
+
) -> list[_DirectorySnapshotSpec]:
|
|
710
|
+
specs: dict[str, _DirectorySnapshotSpec] = {}
|
|
711
|
+
|
|
712
|
+
def add(
|
|
713
|
+
path: Path,
|
|
714
|
+
*,
|
|
715
|
+
immediate: tuple[str, ...] = (),
|
|
716
|
+
recursive: tuple[str, ...] = (),
|
|
717
|
+
) -> None:
|
|
718
|
+
resolved = path.expanduser().resolve()
|
|
719
|
+
if (
|
|
720
|
+
discovery.project_config is not None
|
|
721
|
+
and discovery.project_config.excludes_workspace_path(resolved)
|
|
722
|
+
):
|
|
723
|
+
return
|
|
724
|
+
key = str(resolved).casefold()
|
|
725
|
+
spec = specs.setdefault(
|
|
726
|
+
key,
|
|
727
|
+
_DirectorySnapshotSpec(
|
|
728
|
+
path=resolved,
|
|
729
|
+
immediate_extensions=set(),
|
|
730
|
+
recursive_extensions=set(),
|
|
731
|
+
),
|
|
732
|
+
)
|
|
733
|
+
spec.immediate_extensions.update(immediate)
|
|
734
|
+
spec.recursive_extensions.update(recursive)
|
|
735
|
+
|
|
736
|
+
for project in discovery.project_files:
|
|
737
|
+
add(
|
|
738
|
+
Path(project).parent,
|
|
739
|
+
immediate=_PROJECT_SNAPSHOT_EXTENSIONS,
|
|
740
|
+
recursive=_RELATIVE_INCLUDE_EXTENSIONS,
|
|
741
|
+
)
|
|
742
|
+
for path in discovery.search_paths:
|
|
743
|
+
add(
|
|
744
|
+
Path(path),
|
|
745
|
+
immediate=_UNIT_SNAPSHOT_EXTENSIONS,
|
|
746
|
+
recursive=_RELATIVE_INCLUDE_EXTENSIONS,
|
|
747
|
+
)
|
|
748
|
+
for path in discovery.include_paths:
|
|
749
|
+
add(Path(path), recursive=SOURCE_EXTENSIONS)
|
|
750
|
+
if result is not None:
|
|
751
|
+
for unit in result.parsed_units:
|
|
752
|
+
add(
|
|
753
|
+
Path(unit.path).parent,
|
|
754
|
+
immediate=_UNIT_SNAPSHOT_EXTENSIONS,
|
|
755
|
+
recursive=_RELATIVE_INCLUDE_EXTENSIONS,
|
|
756
|
+
)
|
|
757
|
+
for include in result.include_files:
|
|
758
|
+
add(
|
|
759
|
+
Path(include.path).parent,
|
|
760
|
+
immediate=_UNIT_SNAPSHOT_EXTENSIONS,
|
|
761
|
+
recursive=_RELATIVE_INCLUDE_EXTENSIONS,
|
|
762
|
+
)
|
|
763
|
+
return sorted(specs.values(), key=lambda item: (str(item.path).casefold(), str(item.path)))
|
|
764
|
+
|
|
765
|
+
|
|
766
|
+
def _directory_snapshots(
|
|
767
|
+
specs: list[_DirectorySnapshotSpec],
|
|
768
|
+
*,
|
|
769
|
+
root: Path,
|
|
770
|
+
project_config: ProjectPathConfig | None,
|
|
771
|
+
) -> list[dict[str, object]]:
|
|
772
|
+
snapshots: list[dict[str, object]] = []
|
|
773
|
+
for spec in specs:
|
|
774
|
+
directory = spec.path
|
|
775
|
+
entries: set[str] = set()
|
|
776
|
+
readable = True
|
|
777
|
+
try:
|
|
778
|
+
if spec.immediate_extensions:
|
|
779
|
+
for candidate in directory.iterdir():
|
|
780
|
+
if (
|
|
781
|
+
(
|
|
782
|
+
project_config is None
|
|
783
|
+
or not project_config.excludes_workspace_path(candidate)
|
|
784
|
+
)
|
|
785
|
+
and candidate.suffix.casefold() in spec.immediate_extensions
|
|
786
|
+
and candidate.is_file()
|
|
787
|
+
):
|
|
788
|
+
entries.add(str(candidate.resolve()))
|
|
789
|
+
if spec.recursive_extensions:
|
|
790
|
+
if project_config is None:
|
|
791
|
+
for candidate in directory.rglob("*"):
|
|
792
|
+
if any(part in SKIP_DIRS for part in candidate.parts):
|
|
793
|
+
continue
|
|
794
|
+
if (
|
|
795
|
+
candidate.suffix.casefold() in spec.recursive_extensions
|
|
796
|
+
and candidate.is_file()
|
|
797
|
+
):
|
|
798
|
+
entries.add(str(candidate.resolve()))
|
|
799
|
+
else:
|
|
800
|
+
for current_name, directory_names, file_names in os.walk(
|
|
801
|
+
directory,
|
|
802
|
+
followlinks=False,
|
|
803
|
+
):
|
|
804
|
+
current = Path(current_name)
|
|
805
|
+
directory_names[:] = [
|
|
806
|
+
name
|
|
807
|
+
for name in directory_names
|
|
808
|
+
if name not in SKIP_DIRS
|
|
809
|
+
and not project_config.excludes_workspace_path(
|
|
810
|
+
current / name
|
|
811
|
+
)
|
|
812
|
+
]
|
|
813
|
+
for name in file_names:
|
|
814
|
+
candidate = current / name
|
|
815
|
+
if (
|
|
816
|
+
candidate.suffix.casefold()
|
|
817
|
+
in spec.recursive_extensions
|
|
818
|
+
and not project_config.excludes_workspace_path(candidate)
|
|
819
|
+
and candidate.is_file()
|
|
820
|
+
):
|
|
821
|
+
entries.add(str(candidate.resolve()))
|
|
822
|
+
except OSError:
|
|
823
|
+
readable = False
|
|
824
|
+
snapshots.append(
|
|
825
|
+
{
|
|
826
|
+
"path": _display_path(directory, root),
|
|
827
|
+
"is_directory": directory.is_dir(),
|
|
828
|
+
"readable": readable,
|
|
829
|
+
"entries": _file_records(entries, root=root),
|
|
830
|
+
}
|
|
831
|
+
)
|
|
832
|
+
return snapshots
|
|
833
|
+
|
|
834
|
+
|
|
835
|
+
def _file_records(
|
|
836
|
+
paths: set[str],
|
|
837
|
+
*,
|
|
838
|
+
root: Path,
|
|
839
|
+
) -> list[tuple[str, int | None, int | None]]:
|
|
840
|
+
records: list[tuple[str, int | None, int | None]] = []
|
|
841
|
+
for value in sorted(paths, key=lambda item: (item.casefold(), item)):
|
|
842
|
+
path = Path(value).expanduser().resolve()
|
|
843
|
+
exposed_path = _display_path(path, root)
|
|
844
|
+
try:
|
|
845
|
+
stat = path.stat()
|
|
846
|
+
except OSError:
|
|
847
|
+
records.append((exposed_path, None, None))
|
|
848
|
+
else:
|
|
849
|
+
records.append((exposed_path, stat.st_size, stat.st_mtime_ns))
|
|
850
|
+
return records
|
|
851
|
+
|
|
852
|
+
|
|
853
|
+
__all__ = [
|
|
854
|
+
"AgentProject",
|
|
855
|
+
"AgentUnit",
|
|
856
|
+
"AgentWorkspace",
|
|
857
|
+
"unit_display_path",
|
|
858
|
+
"unit_source_path",
|
|
859
|
+
"unit_target_id",
|
|
860
|
+
]
|