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
delphi_lsp/progress.py
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from collections.abc import Callable
|
|
4
|
+
from dataclasses import dataclass
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
@dataclass(frozen=True)
|
|
8
|
+
class ProgressEvent:
|
|
9
|
+
"""A package-controlled snapshot of Delphi indexing progress."""
|
|
10
|
+
|
|
11
|
+
phase: str
|
|
12
|
+
language: str
|
|
13
|
+
path: str
|
|
14
|
+
files_discovered: int
|
|
15
|
+
files_completed: int
|
|
16
|
+
files_total: int | None
|
|
17
|
+
lines_processed: int
|
|
18
|
+
symbols_discovered: int
|
|
19
|
+
cached_files: int
|
|
20
|
+
detail: str
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
ProgressCallback = Callable[[ProgressEvent], None]
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
__all__ = ["ProgressCallback", "ProgressEvent"]
|
|
@@ -0,0 +1,285 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
import re
|
|
5
|
+
from collections.abc import Iterable, Mapping
|
|
6
|
+
from dataclasses import dataclass
|
|
7
|
+
from functools import lru_cache
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
from typing import Any
|
|
10
|
+
|
|
11
|
+
from .preprocessor import IncludeLoader
|
|
12
|
+
from .source_reader import read_source_text
|
|
13
|
+
|
|
14
|
+
try:
|
|
15
|
+
import tomllib
|
|
16
|
+
except ModuleNotFoundError: # pragma: no cover - exercised by Python 3.10 CI
|
|
17
|
+
import tomli as tomllib # type: ignore[import-not-found,no-redef]
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
CONFIG_FILE_NAME = ".delphi-lsp.toml"
|
|
21
|
+
_PROJECT_KEYS = frozenset({"include", "exclude"})
|
|
22
|
+
_WORKSPACE_KEYS = frozenset({"exclude"})
|
|
23
|
+
_WINDOWS_ABSOLUTE_RE = re.compile(r"^[A-Za-z]:/")
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class ProjectConfigError(ValueError):
|
|
27
|
+
"""Raised when repository project selection configuration is invalid."""
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
@dataclass(frozen=True, slots=True)
|
|
31
|
+
class ProjectPathConfig:
|
|
32
|
+
root: Path
|
|
33
|
+
source: Path
|
|
34
|
+
include: tuple[str, ...] = ()
|
|
35
|
+
exclude: tuple[str, ...] = ()
|
|
36
|
+
workspace_exclude: tuple[str, ...] = ()
|
|
37
|
+
|
|
38
|
+
@property
|
|
39
|
+
def has_filters(self) -> bool:
|
|
40
|
+
return bool(self.include or self.exclude)
|
|
41
|
+
|
|
42
|
+
def selects(self, *paths: str | Path) -> bool:
|
|
43
|
+
relative_paths = tuple(
|
|
44
|
+
relative
|
|
45
|
+
for path in paths
|
|
46
|
+
if (relative := _repository_relative_path(path, self.root)) is not None
|
|
47
|
+
)
|
|
48
|
+
if not relative_paths:
|
|
49
|
+
return False
|
|
50
|
+
if self.include and not _matches_any(relative_paths, self.include):
|
|
51
|
+
return False
|
|
52
|
+
return not _matches_any(relative_paths, self.exclude)
|
|
53
|
+
|
|
54
|
+
def excludes_workspace_path(self, path: str | Path) -> bool:
|
|
55
|
+
if not self.workspace_exclude:
|
|
56
|
+
return False
|
|
57
|
+
lexical = _lexical_repository_relative_path(path, self.root)
|
|
58
|
+
resolved = _repository_relative_path(path, self.root)
|
|
59
|
+
if lexical is None or resolved is None:
|
|
60
|
+
return True
|
|
61
|
+
return _matches_any(
|
|
62
|
+
(lexical, resolved),
|
|
63
|
+
self.workspace_exclude,
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def load_project_path_config(root: str | Path) -> ProjectPathConfig | None:
|
|
68
|
+
root_path = Path(root).expanduser().resolve()
|
|
69
|
+
config_path = root_path / CONFIG_FILE_NAME
|
|
70
|
+
if not config_path.is_file():
|
|
71
|
+
return None
|
|
72
|
+
try:
|
|
73
|
+
with config_path.open("rb") as stream:
|
|
74
|
+
document = tomllib.load(stream)
|
|
75
|
+
except (OSError, tomllib.TOMLDecodeError) as exc:
|
|
76
|
+
raise ProjectConfigError(f"{config_path}: Invalid TOML: {exc}") from exc
|
|
77
|
+
|
|
78
|
+
projects = _load_table(document, "projects", _PROJECT_KEYS, config_path)
|
|
79
|
+
workspace = _load_table(document, "workspace", _WORKSPACE_KEYS, config_path)
|
|
80
|
+
include = _load_patterns(projects, "include", config_path, section="projects")
|
|
81
|
+
exclude = _load_patterns(projects, "exclude", config_path, section="projects")
|
|
82
|
+
workspace_exclude = _load_patterns(
|
|
83
|
+
workspace,
|
|
84
|
+
"exclude",
|
|
85
|
+
config_path,
|
|
86
|
+
section="workspace",
|
|
87
|
+
)
|
|
88
|
+
return ProjectPathConfig(
|
|
89
|
+
root=root_path,
|
|
90
|
+
source=config_path.resolve(),
|
|
91
|
+
include=include,
|
|
92
|
+
exclude=exclude,
|
|
93
|
+
workspace_exclude=workspace_exclude,
|
|
94
|
+
)
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def workspace_include_loader(
|
|
98
|
+
project_config: ProjectPathConfig | None,
|
|
99
|
+
include_paths: Iterable[str | Path] = (),
|
|
100
|
+
) -> IncludeLoader | None:
|
|
101
|
+
"""Build an include loader that enforces the repository's hard path boundary."""
|
|
102
|
+
|
|
103
|
+
enforce_boundary = (
|
|
104
|
+
project_config is not None
|
|
105
|
+
and bool(project_config.workspace_exclude)
|
|
106
|
+
)
|
|
107
|
+
search_paths = tuple(
|
|
108
|
+
Path(path).expanduser().resolve()
|
|
109
|
+
for path in include_paths
|
|
110
|
+
)
|
|
111
|
+
|
|
112
|
+
def load(parent_file: str, include_name: str) -> tuple[str, str] | None:
|
|
113
|
+
include_path = Path(include_name.replace("\\", "/"))
|
|
114
|
+
parent = Path(parent_file).expanduser().resolve().parent
|
|
115
|
+
namespaced_source = (
|
|
116
|
+
parent.parent / "src"
|
|
117
|
+
if parent.name.casefold() == "namespaced"
|
|
118
|
+
else None
|
|
119
|
+
)
|
|
120
|
+
bases = tuple(
|
|
121
|
+
dict.fromkeys(
|
|
122
|
+
base
|
|
123
|
+
for base in (parent, namespaced_source, *search_paths)
|
|
124
|
+
if base is not None
|
|
125
|
+
)
|
|
126
|
+
)
|
|
127
|
+
for base in bases:
|
|
128
|
+
candidate = (base / include_path).resolve()
|
|
129
|
+
if (
|
|
130
|
+
enforce_boundary
|
|
131
|
+
and project_config is not None
|
|
132
|
+
and project_config.excludes_workspace_path(candidate)
|
|
133
|
+
):
|
|
134
|
+
continue
|
|
135
|
+
try:
|
|
136
|
+
if candidate.is_file():
|
|
137
|
+
return read_source_text(candidate), str(candidate)
|
|
138
|
+
except (OSError, UnicodeError):
|
|
139
|
+
continue
|
|
140
|
+
return None
|
|
141
|
+
|
|
142
|
+
return load
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
def _load_table(
|
|
146
|
+
document: Mapping[str, Any],
|
|
147
|
+
name: str,
|
|
148
|
+
supported_keys: frozenset[str],
|
|
149
|
+
config_path: Path,
|
|
150
|
+
) -> Mapping[str, Any]:
|
|
151
|
+
table = document.get(name, {})
|
|
152
|
+
if not isinstance(table, Mapping):
|
|
153
|
+
raise ProjectConfigError(f"{config_path}: [{name}] must be a TOML table.")
|
|
154
|
+
unknown = sorted(set(table) - supported_keys)
|
|
155
|
+
if unknown:
|
|
156
|
+
rendered = ", ".join(str(key) for key in unknown)
|
|
157
|
+
raise ProjectConfigError(
|
|
158
|
+
f"{config_path}: Unsupported key in [{name}]: {rendered}."
|
|
159
|
+
)
|
|
160
|
+
return table
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
def _load_patterns(
|
|
164
|
+
projects: Mapping[str, Any],
|
|
165
|
+
name: str,
|
|
166
|
+
config_path: Path,
|
|
167
|
+
*,
|
|
168
|
+
section: str,
|
|
169
|
+
) -> tuple[str, ...]:
|
|
170
|
+
values = projects.get(name, [])
|
|
171
|
+
if not isinstance(values, list):
|
|
172
|
+
raise ProjectConfigError(
|
|
173
|
+
f"{config_path}: {section}.{name} must be an array of strings."
|
|
174
|
+
)
|
|
175
|
+
if any(not isinstance(value, str) for value in values):
|
|
176
|
+
raise ProjectConfigError(
|
|
177
|
+
f"{config_path}: {section}.{name} must contain only strings."
|
|
178
|
+
)
|
|
179
|
+
normalized = tuple(
|
|
180
|
+
_normalize_pattern(
|
|
181
|
+
value,
|
|
182
|
+
config_path=config_path,
|
|
183
|
+
setting=f"{section}.{name}",
|
|
184
|
+
)
|
|
185
|
+
for value in values
|
|
186
|
+
)
|
|
187
|
+
return tuple(dict.fromkeys(normalized))
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
def _normalize_pattern(value: str, *, config_path: Path, setting: str) -> str:
|
|
191
|
+
pattern = value.strip().replace("\\", "/")
|
|
192
|
+
while pattern.startswith("./"):
|
|
193
|
+
pattern = pattern[2:]
|
|
194
|
+
pattern = pattern.rstrip("/")
|
|
195
|
+
pattern = re.sub(r"(?:\*\*/)+", "**/", pattern)
|
|
196
|
+
if not pattern:
|
|
197
|
+
raise ProjectConfigError(
|
|
198
|
+
f"{config_path}: {setting} patterns must not be empty."
|
|
199
|
+
)
|
|
200
|
+
if pattern.startswith("/") or _WINDOWS_ABSOLUTE_RE.match(pattern):
|
|
201
|
+
raise ProjectConfigError(
|
|
202
|
+
f"{config_path}: {setting} patterns must be repository-relative."
|
|
203
|
+
)
|
|
204
|
+
if any(part == ".." for part in pattern.split("/")):
|
|
205
|
+
raise ProjectConfigError(
|
|
206
|
+
f"{config_path}: {setting} patterns must not contain '..'."
|
|
207
|
+
)
|
|
208
|
+
return pattern
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
def _repository_relative_path(path: str | Path, root: Path) -> str | None:
|
|
212
|
+
candidate = Path(path).expanduser()
|
|
213
|
+
if not candidate.is_absolute():
|
|
214
|
+
candidate = root / candidate
|
|
215
|
+
try:
|
|
216
|
+
relative = candidate.resolve().relative_to(root)
|
|
217
|
+
except (OSError, ValueError):
|
|
218
|
+
return None
|
|
219
|
+
return relative.as_posix()
|
|
220
|
+
|
|
221
|
+
|
|
222
|
+
def _lexical_repository_relative_path(
|
|
223
|
+
path: str | Path,
|
|
224
|
+
root: Path,
|
|
225
|
+
) -> str | None:
|
|
226
|
+
candidate = Path(path).expanduser()
|
|
227
|
+
if not candidate.is_absolute():
|
|
228
|
+
candidate = root / candidate
|
|
229
|
+
try:
|
|
230
|
+
absolute = Path(os.path.abspath(os.path.normpath(candidate)))
|
|
231
|
+
relative = absolute.relative_to(root)
|
|
232
|
+
except (OSError, ValueError):
|
|
233
|
+
return None
|
|
234
|
+
return relative.as_posix()
|
|
235
|
+
|
|
236
|
+
|
|
237
|
+
def _matches_any(paths: tuple[str, ...], patterns: tuple[str, ...]) -> bool:
|
|
238
|
+
return any(
|
|
239
|
+
_path_matches_pattern(path.casefold(), pattern.casefold())
|
|
240
|
+
for path in paths
|
|
241
|
+
for pattern in patterns
|
|
242
|
+
)
|
|
243
|
+
|
|
244
|
+
|
|
245
|
+
def _path_matches_pattern(path: str, pattern: str) -> bool:
|
|
246
|
+
if not any(token in pattern for token in ("*", "?")):
|
|
247
|
+
return path == pattern or path.startswith(f"{pattern}/")
|
|
248
|
+
if pattern.endswith("/**"):
|
|
249
|
+
directory_pattern = pattern[:-3].rstrip("/")
|
|
250
|
+
if _compile_pattern(directory_pattern).fullmatch(path):
|
|
251
|
+
return True
|
|
252
|
+
return bool(_compile_pattern(pattern).fullmatch(path))
|
|
253
|
+
|
|
254
|
+
|
|
255
|
+
@lru_cache(maxsize=256)
|
|
256
|
+
def _compile_pattern(pattern: str) -> re.Pattern[str]:
|
|
257
|
+
translated: list[str] = []
|
|
258
|
+
index = 0
|
|
259
|
+
while index < len(pattern):
|
|
260
|
+
if pattern.startswith("**/", index):
|
|
261
|
+
translated.append("(?:.*/)?")
|
|
262
|
+
index += 3
|
|
263
|
+
continue
|
|
264
|
+
if pattern.startswith("**", index):
|
|
265
|
+
translated.append(".*")
|
|
266
|
+
index += 2
|
|
267
|
+
continue
|
|
268
|
+
character = pattern[index]
|
|
269
|
+
if character == "*":
|
|
270
|
+
translated.append("[^/]*")
|
|
271
|
+
elif character == "?":
|
|
272
|
+
translated.append("[^/]")
|
|
273
|
+
else:
|
|
274
|
+
translated.append(re.escape(character))
|
|
275
|
+
index += 1
|
|
276
|
+
return re.compile("".join(translated))
|
|
277
|
+
|
|
278
|
+
|
|
279
|
+
__all__ = [
|
|
280
|
+
"CONFIG_FILE_NAME",
|
|
281
|
+
"ProjectConfigError",
|
|
282
|
+
"ProjectPathConfig",
|
|
283
|
+
"load_project_path_config",
|
|
284
|
+
"workspace_include_loader",
|
|
285
|
+
]
|