agent-codinglanguage-mapper 1.0.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- agent_codinglanguage_mapper/__init__.py +20 -0
- agent_codinglanguage_mapper/__main__.py +4 -0
- agent_codinglanguage_mapper/_version.py +2 -0
- agent_codinglanguage_mapper/adapters/__init__.py +4 -0
- agent_codinglanguage_mapper/adapters/delphi.py +822 -0
- agent_codinglanguage_mapper/adapters/delphi_frontend/__init__.py +6 -0
- agent_codinglanguage_mapper/adapters/delphi_frontend/comment_builder.py +29 -0
- agent_codinglanguage_mapper/adapters/delphi_frontend/consts.py +345 -0
- agent_codinglanguage_mapper/adapters/delphi_frontend/grammar.py +460 -0
- agent_codinglanguage_mapper/adapters/delphi_frontend/lark_builder.py +2674 -0
- agent_codinglanguage_mapper/adapters/delphi_frontend/lark_tokens.py +237 -0
- agent_codinglanguage_mapper/adapters/delphi_frontend/lsp_server.py +1832 -0
- agent_codinglanguage_mapper/adapters/delphi_frontend/nodes.py +371 -0
- agent_codinglanguage_mapper/adapters/delphi_frontend/parser.py +193 -0
- agent_codinglanguage_mapper/adapters/delphi_frontend/preprocessor.py +997 -0
- agent_codinglanguage_mapper/adapters/delphi_frontend/project_discovery.py +518 -0
- agent_codinglanguage_mapper/adapters/delphi_frontend/project_indexer.py +319 -0
- agent_codinglanguage_mapper/adapters/delphi_frontend/semantic.py +375 -0
- agent_codinglanguage_mapper/adapters/delphi_frontend/semantic_builder.py +1384 -0
- agent_codinglanguage_mapper/adapters/delphi_frontend/source_reader.py +17 -0
- agent_codinglanguage_mapper/adapters/delphi_frontend/workspace.py +67 -0
- agent_codinglanguage_mapper/adapters/tree_sitter.py +1722 -0
- agent_codinglanguage_mapper/cli.py +138 -0
- agent_codinglanguage_mapper/discovery.py +1328 -0
- agent_codinglanguage_mapper/indexer.py +1102 -0
- agent_codinglanguage_mapper/integration.py +186 -0
- agent_codinglanguage_mapper/lsp_server.py +413 -0
- agent_codinglanguage_mapper/lsp_service.py +447 -0
- agent_codinglanguage_mapper/mapper.py +596 -0
- agent_codinglanguage_mapper/models.py +101 -0
- agent_codinglanguage_mapper/protocol.py +152 -0
- agent_codinglanguage_mapper/rendering.py +153 -0
- agent_codinglanguage_mapper/templates/opencode/plugins/codebase_map.ts +191 -0
- agent_codinglanguage_mapper/templates/skill/SKILL.md +52 -0
- agent_codinglanguage_mapper/worker.py +29 -0
- agent_codinglanguage_mapper/workspace.py +114 -0
- agent_codinglanguage_mapper-1.0.0.dist-info/METADATA +383 -0
- agent_codinglanguage_mapper-1.0.0.dist-info/RECORD +42 -0
- agent_codinglanguage_mapper-1.0.0.dist-info/WHEEL +5 -0
- agent_codinglanguage_mapper-1.0.0.dist-info/entry_points.txt +2 -0
- agent_codinglanguage_mapper-1.0.0.dist-info/licenses/LICENSE +373 -0
- agent_codinglanguage_mapper-1.0.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,1832 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass, field
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from typing import Iterable, Optional
|
|
6
|
+
from urllib.parse import unquote, urlparse
|
|
7
|
+
import os
|
|
8
|
+
import re
|
|
9
|
+
|
|
10
|
+
from .semantic import (
|
|
11
|
+
ReferenceKind,
|
|
12
|
+
Scope,
|
|
13
|
+
ScopeKind,
|
|
14
|
+
SourceRange,
|
|
15
|
+
Symbol,
|
|
16
|
+
SymbolIndex,
|
|
17
|
+
SymbolKind,
|
|
18
|
+
SymbolReference,
|
|
19
|
+
TypeRef,
|
|
20
|
+
UnknownTypeRef,
|
|
21
|
+
Visibility,
|
|
22
|
+
)
|
|
23
|
+
from .semantic_builder import SemanticBuilder, SemanticModel
|
|
24
|
+
from .source_reader import read_source_text
|
|
25
|
+
from .workspace import WorkspaceSemanticResult, build_workspace_semantics
|
|
26
|
+
from .project_discovery import discover_delphi_project
|
|
27
|
+
from ..._version import __version__
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
@dataclass
|
|
31
|
+
class DocumentSnapshot:
|
|
32
|
+
uri: str
|
|
33
|
+
file_name: str
|
|
34
|
+
text: str
|
|
35
|
+
semantic: Optional[SemanticModel] = None
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
@dataclass
|
|
39
|
+
class FileSnapshot:
|
|
40
|
+
path: str
|
|
41
|
+
text: str
|
|
42
|
+
mtime: float
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
@dataclass
|
|
46
|
+
class WorkspaceConfig:
|
|
47
|
+
roots: list[str] = field(default_factory=list)
|
|
48
|
+
include_paths: list[str] = field(default_factory=list)
|
|
49
|
+
search_paths: list[str] = field(default_factory=list)
|
|
50
|
+
defines: list[str] = field(default_factory=list)
|
|
51
|
+
extensions: tuple[str, ...] = ('.pas', '.dpr', '.dpk', '.inc')
|
|
52
|
+
eager_index: bool = False
|
|
53
|
+
auto_discover_paths: bool = True
|
|
54
|
+
discovered_include_paths: list[str] = field(default_factory=list)
|
|
55
|
+
discovered_search_paths: list[str] = field(default_factory=list)
|
|
56
|
+
discovered_defines: list[str] = field(default_factory=list)
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
@dataclass
|
|
60
|
+
class LspWorkspaceState:
|
|
61
|
+
config: WorkspaceConfig = field(default_factory=WorkspaceConfig)
|
|
62
|
+
documents: dict[str, DocumentSnapshot] = field(default_factory=dict)
|
|
63
|
+
workspace: Optional[WorkspaceSemanticResult] = None
|
|
64
|
+
workspace_symbol_index: Optional[WorkspaceSemanticResult] = None
|
|
65
|
+
workspace_symbol_query_cache: dict[str, WorkspaceSemanticResult] = field(default_factory=dict)
|
|
66
|
+
workspace_files: set[str] = field(default_factory=set)
|
|
67
|
+
file_cache: dict[str, FileSnapshot] = field(default_factory=dict)
|
|
68
|
+
|
|
69
|
+
def configure(self, config: WorkspaceConfig) -> None:
|
|
70
|
+
self.config = self._with_discovered_config(config)
|
|
71
|
+
self.workspace_files = set()
|
|
72
|
+
self.file_cache = {}
|
|
73
|
+
self.workspace = None
|
|
74
|
+
self.workspace_symbol_index = None
|
|
75
|
+
self.workspace_symbol_query_cache = {}
|
|
76
|
+
if config.eager_index:
|
|
77
|
+
self.index_workspace()
|
|
78
|
+
|
|
79
|
+
def _with_discovered_config(self, config: WorkspaceConfig) -> WorkspaceConfig:
|
|
80
|
+
if not config.auto_discover_paths or not config.roots:
|
|
81
|
+
return config
|
|
82
|
+
include_paths = list(config.include_paths)
|
|
83
|
+
search_paths = list(config.search_paths)
|
|
84
|
+
defines = list(config.defines)
|
|
85
|
+
discovered_include_paths: list[str] = []
|
|
86
|
+
discovered_search_paths: list[str] = []
|
|
87
|
+
discovered_defines: list[str] = []
|
|
88
|
+
|
|
89
|
+
def add_path(target: list[str], discovered: list[str], value: str) -> None:
|
|
90
|
+
normalized = str(Path(value).expanduser().resolve())
|
|
91
|
+
if normalized not in target:
|
|
92
|
+
target.append(normalized)
|
|
93
|
+
if normalized not in discovered:
|
|
94
|
+
discovered.append(normalized)
|
|
95
|
+
|
|
96
|
+
def add_define(value: str) -> None:
|
|
97
|
+
if value not in defines:
|
|
98
|
+
defines.append(value)
|
|
99
|
+
if value not in discovered_defines:
|
|
100
|
+
discovered_defines.append(value)
|
|
101
|
+
|
|
102
|
+
for root in config.roots:
|
|
103
|
+
discovery = discover_delphi_project(root, include_paths=config.include_paths, search_paths=config.search_paths, defines=config.defines)
|
|
104
|
+
for path in discovery.include_paths:
|
|
105
|
+
add_path(include_paths, discovered_include_paths, path)
|
|
106
|
+
for path in discovery.search_paths:
|
|
107
|
+
add_path(search_paths, discovered_search_paths, path)
|
|
108
|
+
for define in discovery.defines:
|
|
109
|
+
add_define(define)
|
|
110
|
+
return WorkspaceConfig(
|
|
111
|
+
roots=list(config.roots),
|
|
112
|
+
include_paths=include_paths,
|
|
113
|
+
search_paths=search_paths,
|
|
114
|
+
defines=defines,
|
|
115
|
+
extensions=config.extensions,
|
|
116
|
+
eager_index=config.eager_index,
|
|
117
|
+
auto_discover_paths=config.auto_discover_paths,
|
|
118
|
+
discovered_include_paths=discovered_include_paths,
|
|
119
|
+
discovered_search_paths=discovered_search_paths,
|
|
120
|
+
discovered_defines=discovered_defines,
|
|
121
|
+
)
|
|
122
|
+
|
|
123
|
+
def index_workspace(self) -> None:
|
|
124
|
+
self.workspace_files = set(self._scan_workspace_files())
|
|
125
|
+
self._refresh_file_cache()
|
|
126
|
+
self._rebuild()
|
|
127
|
+
|
|
128
|
+
def ensure_workspace_indexed(self) -> None:
|
|
129
|
+
if self.workspace is None and self.config.roots:
|
|
130
|
+
self.index_workspace()
|
|
131
|
+
|
|
132
|
+
def ensure_workspace_symbol_indexed(self) -> None:
|
|
133
|
+
if self.workspace_symbol_index is not None:
|
|
134
|
+
return
|
|
135
|
+
if not self.workspace_files and self.config.roots:
|
|
136
|
+
self.workspace_files = set(self._scan_workspace_files())
|
|
137
|
+
self._refresh_file_cache()
|
|
138
|
+
sources = {path: snapshot.text for path, snapshot in self.file_cache.items()}
|
|
139
|
+
for doc in self.documents.values():
|
|
140
|
+
sources[doc.file_name] = doc.text
|
|
141
|
+
self.workspace_symbol_index = self._outline_workspace_semantics(sources)
|
|
142
|
+
|
|
143
|
+
def workspace_symbols_for_query(self, query: str) -> Optional[WorkspaceSemanticResult]:
|
|
144
|
+
normalized_query = query.strip().casefold()
|
|
145
|
+
if not normalized_query:
|
|
146
|
+
self.ensure_workspace_symbol_indexed()
|
|
147
|
+
return self.workspace_symbol_index
|
|
148
|
+
if self.workspace_symbol_index is not None:
|
|
149
|
+
return self.workspace_symbol_index
|
|
150
|
+
cached = self.workspace_symbol_query_cache.get(normalized_query)
|
|
151
|
+
if cached is not None:
|
|
152
|
+
return cached
|
|
153
|
+
if not self.workspace_files and self.config.roots:
|
|
154
|
+
self.workspace_files = set(self._scan_workspace_files())
|
|
155
|
+
self._refresh_file_cache()
|
|
156
|
+
sources = {
|
|
157
|
+
path: snapshot.text
|
|
158
|
+
for path, snapshot in self.file_cache.items()
|
|
159
|
+
if normalized_query in snapshot.text.casefold()
|
|
160
|
+
}
|
|
161
|
+
for doc in self.documents.values():
|
|
162
|
+
if normalized_query in doc.text.casefold():
|
|
163
|
+
sources[doc.file_name] = doc.text
|
|
164
|
+
result = self._outline_workspace_semantics(sources)
|
|
165
|
+
if result is None:
|
|
166
|
+
result = WorkspaceSemanticResult(models={}, index=SymbolIndex())
|
|
167
|
+
self.workspace_symbol_query_cache[normalized_query] = result
|
|
168
|
+
return result
|
|
169
|
+
|
|
170
|
+
def update_document(self, uri: str, text: str) -> None:
|
|
171
|
+
file_name = uri_to_path(uri)
|
|
172
|
+
self.documents[uri] = DocumentSnapshot(uri=uri, file_name=file_name, text=text)
|
|
173
|
+
self.workspace_symbol_index = None
|
|
174
|
+
self.workspace_symbol_query_cache = {}
|
|
175
|
+
self._rebuild()
|
|
176
|
+
|
|
177
|
+
def remove_document(self, uri: str) -> None:
|
|
178
|
+
if uri in self.documents:
|
|
179
|
+
self.documents.pop(uri)
|
|
180
|
+
self.workspace_symbol_index = None
|
|
181
|
+
self.workspace_symbol_query_cache = {}
|
|
182
|
+
self._rebuild()
|
|
183
|
+
|
|
184
|
+
def _scan_workspace_files(self) -> list[str]:
|
|
185
|
+
files: list[str] = []
|
|
186
|
+
for root in self.config.roots:
|
|
187
|
+
root_path = Path(root)
|
|
188
|
+
if not root_path.exists():
|
|
189
|
+
continue
|
|
190
|
+
for ext in self.config.extensions:
|
|
191
|
+
files.extend(str(path) for path in root_path.rglob(f'*{ext}'))
|
|
192
|
+
return files
|
|
193
|
+
|
|
194
|
+
def _refresh_file_cache(self) -> None:
|
|
195
|
+
to_remove = [path for path in self.file_cache if path not in self.workspace_files]
|
|
196
|
+
for path in to_remove:
|
|
197
|
+
self.file_cache.pop(path, None)
|
|
198
|
+
for path in self.workspace_files:
|
|
199
|
+
try:
|
|
200
|
+
mtime = os.path.getmtime(path)
|
|
201
|
+
except OSError:
|
|
202
|
+
continue
|
|
203
|
+
cached = self.file_cache.get(path)
|
|
204
|
+
if cached is not None and cached.mtime == mtime:
|
|
205
|
+
continue
|
|
206
|
+
try:
|
|
207
|
+
text = read_source_text(Path(path))
|
|
208
|
+
except (OSError, UnicodeError):
|
|
209
|
+
continue
|
|
210
|
+
self.file_cache[path] = FileSnapshot(path=path, text=text, mtime=mtime)
|
|
211
|
+
|
|
212
|
+
def _collect_sources(self) -> dict[str, str]:
|
|
213
|
+
sources = {
|
|
214
|
+
path: snapshot.text
|
|
215
|
+
for path, snapshot in self.file_cache.items()
|
|
216
|
+
}
|
|
217
|
+
for doc in self.documents.values():
|
|
218
|
+
sources[doc.file_name] = doc.text
|
|
219
|
+
return sources
|
|
220
|
+
|
|
221
|
+
def _rebuild(self) -> None:
|
|
222
|
+
sources = self._collect_sources()
|
|
223
|
+
self.workspace_symbol_index = None
|
|
224
|
+
self.workspace_symbol_query_cache = {}
|
|
225
|
+
if not sources:
|
|
226
|
+
self.workspace = None
|
|
227
|
+
return
|
|
228
|
+
result = self._outline_workspace_semantics(sources)
|
|
229
|
+
self.workspace = result
|
|
230
|
+
for doc in self.documents.values():
|
|
231
|
+
doc.semantic = result.models.get(doc.file_name) if result is not None else None
|
|
232
|
+
|
|
233
|
+
def semantic_for_uri(self, uri: str) -> Optional[SemanticModel]:
|
|
234
|
+
doc = self.documents.get(uri)
|
|
235
|
+
if doc is not None:
|
|
236
|
+
return doc.semantic
|
|
237
|
+
file_name = uri_to_path(uri)
|
|
238
|
+
model = self.model_for_path(file_name)
|
|
239
|
+
if model is not None:
|
|
240
|
+
return model
|
|
241
|
+
if '://' in file_name:
|
|
242
|
+
return None
|
|
243
|
+
path = Path(file_name)
|
|
244
|
+
if not path.exists():
|
|
245
|
+
return None
|
|
246
|
+
try:
|
|
247
|
+
text = read_source_text(path)
|
|
248
|
+
except (OSError, UnicodeError):
|
|
249
|
+
return None
|
|
250
|
+
return build_outline_semantic_model(text, file_name)
|
|
251
|
+
|
|
252
|
+
def full_semantic_for_uri(self, uri: str) -> Optional[SemanticModel]:
|
|
253
|
+
file_name = uri_to_path(uri)
|
|
254
|
+
text = self.text_for_uri(uri)
|
|
255
|
+
if text is None:
|
|
256
|
+
return None
|
|
257
|
+
result = build_workspace_semantics(
|
|
258
|
+
{file_name: text},
|
|
259
|
+
include_paths=self.config.include_paths,
|
|
260
|
+
defines=self.config.defines,
|
|
261
|
+
)
|
|
262
|
+
return result.models.get(file_name)
|
|
263
|
+
|
|
264
|
+
def structure_semantic_for_uri(self, uri: str) -> Optional[SemanticModel]:
|
|
265
|
+
text = self.text_for_uri(uri)
|
|
266
|
+
if text is None:
|
|
267
|
+
return None
|
|
268
|
+
return build_outline_semantic_model(text, uri_to_path(uri))
|
|
269
|
+
|
|
270
|
+
def text_for_uri(self, uri: str) -> Optional[str]:
|
|
271
|
+
doc = self.documents.get(uri)
|
|
272
|
+
if doc is not None:
|
|
273
|
+
return doc.text
|
|
274
|
+
file_name = uri_to_path(uri)
|
|
275
|
+
cached = self.file_cache.get(file_name)
|
|
276
|
+
if cached is not None:
|
|
277
|
+
return cached.text
|
|
278
|
+
if '://' in file_name:
|
|
279
|
+
return None
|
|
280
|
+
path = Path(file_name)
|
|
281
|
+
if not path.exists():
|
|
282
|
+
return None
|
|
283
|
+
try:
|
|
284
|
+
return read_source_text(path)
|
|
285
|
+
except (OSError, UnicodeError):
|
|
286
|
+
return None
|
|
287
|
+
|
|
288
|
+
def _outline_workspace_semantics(
|
|
289
|
+
self,
|
|
290
|
+
sources: dict[str, str],
|
|
291
|
+
) -> Optional[WorkspaceSemanticResult]:
|
|
292
|
+
if not sources:
|
|
293
|
+
return None
|
|
294
|
+
models = {file_name: build_outline_semantic_model(text, file_name) for file_name, text in sources.items()}
|
|
295
|
+
index = SymbolIndex()
|
|
296
|
+
for model in models.values():
|
|
297
|
+
index.register_unit(model.unit_scope.name, model.unit_scope)
|
|
298
|
+
for model in models.values():
|
|
299
|
+
model.index = index
|
|
300
|
+
return WorkspaceSemanticResult(models=models, index=index)
|
|
301
|
+
|
|
302
|
+
def model_for_path(self, path: str) -> Optional[SemanticModel]:
|
|
303
|
+
if self.workspace is None:
|
|
304
|
+
return None
|
|
305
|
+
return self.workspace.models.get(path)
|
|
306
|
+
|
|
307
|
+
def iter_models(self) -> Iterable[SemanticModel]:
|
|
308
|
+
if self.workspace is None:
|
|
309
|
+
return []
|
|
310
|
+
return self.workspace.models.values()
|
|
311
|
+
|
|
312
|
+
def uri_for_file_name(self, file_name: str) -> Optional[str]:
|
|
313
|
+
for doc in self.documents.values():
|
|
314
|
+
if doc.file_name == file_name:
|
|
315
|
+
return doc.uri
|
|
316
|
+
return None
|
|
317
|
+
|
|
318
|
+
def diagnostics_for_uri(self, uri: str):
|
|
319
|
+
model = self.semantic_for_uri(uri)
|
|
320
|
+
return diagnostics_for_model(model) if model else []
|
|
321
|
+
|
|
322
|
+
|
|
323
|
+
BUILTIN_TYPES = {
|
|
324
|
+
'string', 'ansistring', 'widestring', 'unicodestring',
|
|
325
|
+
'char', 'widechar',
|
|
326
|
+
'integer', 'smallint', 'shortint', 'longint', 'int64', 'uint64',
|
|
327
|
+
'byte', 'word', 'cardinal', 'nativeint', 'nativeuint',
|
|
328
|
+
'boolean', 'bytebool', 'wordbool', 'longbool',
|
|
329
|
+
'single', 'double', 'extended', 'currency', 'real', 'real48',
|
|
330
|
+
'variant', 'olevariant', 'pointer', 'pchar', 'tobject', 'tclass',
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
|
|
334
|
+
def uri_to_path(uri: str) -> str:
|
|
335
|
+
parsed = urlparse(uri)
|
|
336
|
+
if parsed.scheme and parsed.scheme != 'file':
|
|
337
|
+
return uri
|
|
338
|
+
path = unquote(parsed.path)
|
|
339
|
+
if path.startswith('/') and len(path) > 3 and path[2] == ':':
|
|
340
|
+
path = path[1:]
|
|
341
|
+
return path or uri
|
|
342
|
+
|
|
343
|
+
|
|
344
|
+
def outline_source(text: str) -> str:
|
|
345
|
+
return _blank_compound_statement_bodies(text)
|
|
346
|
+
|
|
347
|
+
|
|
348
|
+
def multiline_string_block_end(text: str, start: int) -> int | None:
|
|
349
|
+
for quote_count in (5, 3):
|
|
350
|
+
delimiter = "'" * quote_count
|
|
351
|
+
if not text.startswith(delimiter, start):
|
|
352
|
+
continue
|
|
353
|
+
content_start = start + quote_count
|
|
354
|
+
if text.startswith('\r\n', content_start):
|
|
355
|
+
content_start += 2
|
|
356
|
+
elif content_start < len(text) and text[content_start] == '\n':
|
|
357
|
+
content_start += 1
|
|
358
|
+
else:
|
|
359
|
+
continue
|
|
360
|
+
close = text.find(delimiter, content_start)
|
|
361
|
+
return len(text) if close < 0 else close + quote_count
|
|
362
|
+
return None
|
|
363
|
+
|
|
364
|
+
|
|
365
|
+
# These are the statement grammar alternatives that close with END.
|
|
366
|
+
_END_TERMINATED_STATEMENTS = frozenset({'begin', 'asm', 'case', 'try'})
|
|
367
|
+
# Inline variable type_spec declarations can place these inside a body.
|
|
368
|
+
_END_TERMINATED_STRUCTURED_TYPES = frozenset(
|
|
369
|
+
{'class', 'record', 'object', 'interface', 'dispinterface'}
|
|
370
|
+
)
|
|
371
|
+
_CLASS_MEMBER_PREFIX_FOLLOWERS = frozenset(
|
|
372
|
+
{
|
|
373
|
+
'const',
|
|
374
|
+
'constructor',
|
|
375
|
+
'destructor',
|
|
376
|
+
'function',
|
|
377
|
+
'of',
|
|
378
|
+
'operator',
|
|
379
|
+
'procedure',
|
|
380
|
+
'property',
|
|
381
|
+
'threadvar',
|
|
382
|
+
'type',
|
|
383
|
+
'var',
|
|
384
|
+
}
|
|
385
|
+
)
|
|
386
|
+
_CLASS_TYPE_PREDECESSORS = frozenset({':', '=', 'of', 'packed', 'to', 'type', '^'})
|
|
387
|
+
_CLASS_MEMBER_BOUNDARIES = frozenset(
|
|
388
|
+
{
|
|
389
|
+
';',
|
|
390
|
+
')',
|
|
391
|
+
']',
|
|
392
|
+
'automated',
|
|
393
|
+
'class',
|
|
394
|
+
'dispinterface',
|
|
395
|
+
'interface',
|
|
396
|
+
'object',
|
|
397
|
+
'private',
|
|
398
|
+
'protected',
|
|
399
|
+
'public',
|
|
400
|
+
'published',
|
|
401
|
+
'record',
|
|
402
|
+
}
|
|
403
|
+
)
|
|
404
|
+
_GENERIC_ROUTINE_HEADINGS = frozenset(
|
|
405
|
+
{'constructor', 'destructor', 'function', 'operator', 'procedure'}
|
|
406
|
+
)
|
|
407
|
+
|
|
408
|
+
|
|
409
|
+
def _blank_compound_statement_bodies(text: str) -> str:
|
|
410
|
+
chars = list(text)
|
|
411
|
+
end_stack: list[str] = []
|
|
412
|
+
body_start: int | None = None
|
|
413
|
+
previous_token: str | None = None
|
|
414
|
+
previous_token_is_identifier = False
|
|
415
|
+
paren_depth = 0
|
|
416
|
+
bracket_depth = 0
|
|
417
|
+
angle_depth = 0
|
|
418
|
+
generic_owner_ready = False
|
|
419
|
+
routine_heading_active = False
|
|
420
|
+
type_constructors: dict[tuple[int, int], str] = {}
|
|
421
|
+
i = 0
|
|
422
|
+
n = len(text)
|
|
423
|
+
while i < n:
|
|
424
|
+
ch = text[i]
|
|
425
|
+
nxt = text[i + 1] if i + 1 < n else ''
|
|
426
|
+
|
|
427
|
+
if end_stack and (
|
|
428
|
+
(ch == '{' and nxt == '$')
|
|
429
|
+
or (ch == '(' and nxt == '*' and i + 2 < n and text[i + 2] == '$')
|
|
430
|
+
):
|
|
431
|
+
# Raw conditional branches can disagree on END nesting, so keep the file all-or-safe.
|
|
432
|
+
return text
|
|
433
|
+
|
|
434
|
+
if ch.isspace():
|
|
435
|
+
i += 1
|
|
436
|
+
while i < n and text[i].isspace():
|
|
437
|
+
i += 1
|
|
438
|
+
continue
|
|
439
|
+
|
|
440
|
+
if ch == "'":
|
|
441
|
+
block_end = multiline_string_block_end(text, i)
|
|
442
|
+
if block_end is not None:
|
|
443
|
+
i = block_end
|
|
444
|
+
previous_token = 'literal'
|
|
445
|
+
previous_token_is_identifier = False
|
|
446
|
+
generic_owner_ready = False
|
|
447
|
+
continue
|
|
448
|
+
i += 1
|
|
449
|
+
while i < n:
|
|
450
|
+
if text[i] == "'":
|
|
451
|
+
if i + 1 < n and text[i + 1] == "'":
|
|
452
|
+
i += 2
|
|
453
|
+
continue
|
|
454
|
+
i += 1
|
|
455
|
+
break
|
|
456
|
+
i += 1
|
|
457
|
+
previous_token = 'literal'
|
|
458
|
+
previous_token_is_identifier = False
|
|
459
|
+
generic_owner_ready = False
|
|
460
|
+
continue
|
|
461
|
+
|
|
462
|
+
if ch == '/' and nxt == '/':
|
|
463
|
+
i += 2
|
|
464
|
+
while i < n and text[i] not in {'\n', '\r'}:
|
|
465
|
+
i += 1
|
|
466
|
+
continue
|
|
467
|
+
|
|
468
|
+
if ch == '{':
|
|
469
|
+
i += 1
|
|
470
|
+
while i < n and text[i] != '}':
|
|
471
|
+
i += 1
|
|
472
|
+
if i < n:
|
|
473
|
+
i += 1
|
|
474
|
+
continue
|
|
475
|
+
|
|
476
|
+
if ch == '(' and nxt == '*':
|
|
477
|
+
i += 2
|
|
478
|
+
while i + 1 < n and not (text[i] == '*' and text[i + 1] == ')'):
|
|
479
|
+
i += 1
|
|
480
|
+
if i + 1 < n:
|
|
481
|
+
i += 2
|
|
482
|
+
continue
|
|
483
|
+
|
|
484
|
+
if ch == '&' and (nxt.isalpha() or nxt == '_'):
|
|
485
|
+
start = i
|
|
486
|
+
i += 2
|
|
487
|
+
while i < n and (text[i].isalnum() or text[i] == '_'):
|
|
488
|
+
i += 1
|
|
489
|
+
previous_token = text[start:i].casefold()
|
|
490
|
+
previous_token_is_identifier = True
|
|
491
|
+
generic_owner_ready = False
|
|
492
|
+
continue
|
|
493
|
+
|
|
494
|
+
if ch.isalpha() or ch == '_':
|
|
495
|
+
start = i
|
|
496
|
+
i += 1
|
|
497
|
+
while i < n and (text[i].isalnum() or text[i] == '_'):
|
|
498
|
+
i += 1
|
|
499
|
+
word = text[start:i].casefold()
|
|
500
|
+
generic_owner_ready = False
|
|
501
|
+
opens_construct: bool | None = False
|
|
502
|
+
if not end_stack:
|
|
503
|
+
if word in {'begin', 'asm'}:
|
|
504
|
+
body_start = i
|
|
505
|
+
end_stack.append(word)
|
|
506
|
+
elif word == 'end':
|
|
507
|
+
end_stack.pop()
|
|
508
|
+
if not end_stack and body_start is not None:
|
|
509
|
+
_blank_preserving_newlines(chars, body_start, start)
|
|
510
|
+
body_start = None
|
|
511
|
+
else:
|
|
512
|
+
if angle_depth > 0 and word in _END_TERMINATED_STRUCTURED_TYPES:
|
|
513
|
+
# Constraint keywords are names inside a confirmed generic angle.
|
|
514
|
+
opens_construct = False
|
|
515
|
+
else:
|
|
516
|
+
opens_construct = _opens_end_terminated_construct(
|
|
517
|
+
word,
|
|
518
|
+
text=text,
|
|
519
|
+
word_end=i,
|
|
520
|
+
previous_token=previous_token,
|
|
521
|
+
end_stack=end_stack,
|
|
522
|
+
type_constructor=type_constructors.get((paren_depth, bracket_depth)),
|
|
523
|
+
)
|
|
524
|
+
if opens_construct is None:
|
|
525
|
+
return text
|
|
526
|
+
if opens_construct:
|
|
527
|
+
end_stack.append(word)
|
|
528
|
+
if word == 'class' and opens_construct:
|
|
529
|
+
generic_owner_ready = True
|
|
530
|
+
if word in _GENERIC_ROUTINE_HEADINGS:
|
|
531
|
+
routine_heading_active = True
|
|
532
|
+
if word in {'begin', 'asm'}:
|
|
533
|
+
type_constructors.clear()
|
|
534
|
+
routine_heading_active = False
|
|
535
|
+
elif word in {'procedure', 'function', 'array'}:
|
|
536
|
+
type_constructors[(paren_depth, bracket_depth)] = word
|
|
537
|
+
elif word == 'var':
|
|
538
|
+
type_constructors.pop((paren_depth, bracket_depth), None)
|
|
539
|
+
previous_token = word
|
|
540
|
+
previous_token_is_identifier = True
|
|
541
|
+
continue
|
|
542
|
+
|
|
543
|
+
context_key = (paren_depth, bracket_depth)
|
|
544
|
+
if ch == '<' and nxt not in {'=', '>'}:
|
|
545
|
+
if angle_depth > 0:
|
|
546
|
+
if not previous_token_is_identifier:
|
|
547
|
+
return text
|
|
548
|
+
angle_depth += 1
|
|
549
|
+
elif generic_owner_ready or (
|
|
550
|
+
routine_heading_active
|
|
551
|
+
and previous_token_is_identifier
|
|
552
|
+
and previous_token != 'operator'
|
|
553
|
+
):
|
|
554
|
+
angle_depth = 1
|
|
555
|
+
generic_owner_ready = False
|
|
556
|
+
elif ch == '>' and nxt != '=' and angle_depth > 0:
|
|
557
|
+
angle_depth -= 1
|
|
558
|
+
generic_owner_ready = False
|
|
559
|
+
elif ch == '(':
|
|
560
|
+
paren_depth += 1
|
|
561
|
+
generic_owner_ready = False
|
|
562
|
+
routine_heading_active = False
|
|
563
|
+
elif ch == ')':
|
|
564
|
+
type_constructors.pop(context_key, None)
|
|
565
|
+
paren_depth = max(0, paren_depth - 1)
|
|
566
|
+
generic_owner_ready = False
|
|
567
|
+
elif ch == '[':
|
|
568
|
+
bracket_depth += 1
|
|
569
|
+
generic_owner_ready = False
|
|
570
|
+
elif ch == ']':
|
|
571
|
+
type_constructors.pop(context_key, None)
|
|
572
|
+
bracket_depth = max(0, bracket_depth - 1)
|
|
573
|
+
generic_owner_ready = False
|
|
574
|
+
elif ch == ';':
|
|
575
|
+
type_constructors.pop(context_key, None)
|
|
576
|
+
generic_owner_ready = False
|
|
577
|
+
routine_heading_active = False
|
|
578
|
+
elif ch not in {'<', '>'}:
|
|
579
|
+
generic_owner_ready = False
|
|
580
|
+
previous_token = ch
|
|
581
|
+
previous_token_is_identifier = False
|
|
582
|
+
i += 1
|
|
583
|
+
return text if angle_depth > 0 else ''.join(chars)
|
|
584
|
+
|
|
585
|
+
|
|
586
|
+
def _opens_end_terminated_construct(
|
|
587
|
+
word: str,
|
|
588
|
+
*,
|
|
589
|
+
text: str,
|
|
590
|
+
word_end: int,
|
|
591
|
+
previous_token: str | None,
|
|
592
|
+
end_stack: list[str],
|
|
593
|
+
type_constructor: str | None,
|
|
594
|
+
) -> bool | None:
|
|
595
|
+
if word in _END_TERMINATED_STRUCTURED_TYPES:
|
|
596
|
+
if word == 'object' and previous_token == 'of':
|
|
597
|
+
if type_constructor in {'procedure', 'function'}:
|
|
598
|
+
return False
|
|
599
|
+
if type_constructor == 'array':
|
|
600
|
+
return True
|
|
601
|
+
return None
|
|
602
|
+
if word == 'class':
|
|
603
|
+
next_word = _next_code_word(text, word_end)
|
|
604
|
+
if next_word == 'of':
|
|
605
|
+
return False
|
|
606
|
+
if (
|
|
607
|
+
end_stack[-1] in _END_TERMINATED_STRUCTURED_TYPES
|
|
608
|
+
and next_word in _CLASS_MEMBER_PREFIX_FOLLOWERS
|
|
609
|
+
):
|
|
610
|
+
if previous_token in _CLASS_TYPE_PREDECESSORS:
|
|
611
|
+
return True
|
|
612
|
+
if previous_token in _CLASS_MEMBER_BOUNDARIES:
|
|
613
|
+
return False
|
|
614
|
+
return None
|
|
615
|
+
return True
|
|
616
|
+
if end_stack[-1] in _END_TERMINATED_STRUCTURED_TYPES:
|
|
617
|
+
return False
|
|
618
|
+
return word in _END_TERMINATED_STATEMENTS
|
|
619
|
+
|
|
620
|
+
|
|
621
|
+
def _next_code_word(text: str, start: int) -> str | None:
|
|
622
|
+
i = start
|
|
623
|
+
n = len(text)
|
|
624
|
+
while i < n:
|
|
625
|
+
if text[i].isspace():
|
|
626
|
+
i += 1
|
|
627
|
+
continue
|
|
628
|
+
if text.startswith('//', i):
|
|
629
|
+
i += 2
|
|
630
|
+
while i < n and text[i] not in {'\r', '\n'}:
|
|
631
|
+
i += 1
|
|
632
|
+
if i < n and text[i] == '\r' and i + 1 < n and text[i + 1] == '\n':
|
|
633
|
+
i += 2
|
|
634
|
+
elif i < n:
|
|
635
|
+
i += 1
|
|
636
|
+
continue
|
|
637
|
+
if text[i] == '{':
|
|
638
|
+
close = text.find('}', i + 1)
|
|
639
|
+
i = n if close < 0 else close + 1
|
|
640
|
+
continue
|
|
641
|
+
if text.startswith('(*', i):
|
|
642
|
+
close = text.find('*)', i + 2)
|
|
643
|
+
i = n if close < 0 else close + 2
|
|
644
|
+
continue
|
|
645
|
+
break
|
|
646
|
+
if i >= n or not (text[i].isalpha() or text[i] == '_'):
|
|
647
|
+
return None
|
|
648
|
+
end = i + 1
|
|
649
|
+
while end < n and (text[end].isalnum() or text[end] == '_'):
|
|
650
|
+
end += 1
|
|
651
|
+
return text[i:end].casefold()
|
|
652
|
+
|
|
653
|
+
|
|
654
|
+
def _blank_preserving_newlines(chars: list[str], start: int, end: int) -> None:
|
|
655
|
+
for index in range(start, end):
|
|
656
|
+
if chars[index] not in {'\n', '\r'}:
|
|
657
|
+
chars[index] = ' '
|
|
658
|
+
|
|
659
|
+
|
|
660
|
+
_IDENTIFIER = r'[A-Za-z_][A-Za-z0-9_]*'
|
|
661
|
+
_QUALIFIED_IDENTIFIER = rf'{_IDENTIFIER}(?:\s*\.\s*{_IDENTIFIER})*'
|
|
662
|
+
_UNIT_RE = re.compile(rf'^\s*(?:unit|program|library|package)\s+({_QUALIFIED_IDENTIFIER})\b', re.IGNORECASE)
|
|
663
|
+
_TYPE_RE = re.compile(rf'^\s*({_IDENTIFIER})(?:\s*<[^;=]+>)?\s*=\s*(.*)', re.IGNORECASE)
|
|
664
|
+
_ROUTINE_RE = re.compile(
|
|
665
|
+
rf'^\s*(?:class\s+)?(procedure|function|constructor|destructor|operator)\s+({_QUALIFIED_IDENTIFIER})\b',
|
|
666
|
+
re.IGNORECASE,
|
|
667
|
+
)
|
|
668
|
+
_VARIABLE_RE = re.compile(rf'^\s*({_IDENTIFIER})\s*:\s*([^;=]+)', re.IGNORECASE)
|
|
669
|
+
_PROPERTY_RE = re.compile(rf'^\s*property\s+({_IDENTIFIER})\b(?:[^:;]*:\s*([^;]+))?', re.IGNORECASE)
|
|
670
|
+
_CONSTANT_RE = re.compile(rf'^\s*({_IDENTIFIER})\s*(?::[^=]+)?=', re.IGNORECASE)
|
|
671
|
+
_SECTION_RE = re.compile(
|
|
672
|
+
r'^\s*(type|var|const|threadvar|resourcestring|implementation|initialization|finalization|begin|end)\b',
|
|
673
|
+
re.IGNORECASE,
|
|
674
|
+
)
|
|
675
|
+
_VISIBILITY_RE = re.compile(
|
|
676
|
+
r'^\s*(strict\s+private|strict\s+protected|private|protected|public|published)\b',
|
|
677
|
+
re.IGNORECASE,
|
|
678
|
+
)
|
|
679
|
+
_TYPE_REF_STOP_RE = re.compile(
|
|
680
|
+
r'\b(read|write|add|remove|stored|default|nodefault|index|implements)\b',
|
|
681
|
+
re.IGNORECASE,
|
|
682
|
+
)
|
|
683
|
+
|
|
684
|
+
|
|
685
|
+
def build_outline_semantic_model(text: str, file_name: str) -> SemanticModel:
|
|
686
|
+
unit_name = _outline_unit_name(text, file_name)
|
|
687
|
+
unit_scope = Scope(kind=ScopeKind.UNIT, name=unit_name)
|
|
688
|
+
unit_range = SourceRange(file_name, 1, 1, 1, max(2, len(unit_name) + 1))
|
|
689
|
+
unit_symbol = Symbol(
|
|
690
|
+
name=unit_name,
|
|
691
|
+
kind=SymbolKind.UNIT,
|
|
692
|
+
decl_range=unit_range,
|
|
693
|
+
name_range=unit_range,
|
|
694
|
+
scope=unit_scope,
|
|
695
|
+
)
|
|
696
|
+
unit_scope.owner = unit_symbol
|
|
697
|
+
unit_scope.define(unit_symbol)
|
|
698
|
+
|
|
699
|
+
comment_state: str | None = None
|
|
700
|
+
section = ''
|
|
701
|
+
current_routine: Symbol | None = None
|
|
702
|
+
current_routine_scope: Scope | None = None
|
|
703
|
+
routine_var_section = False
|
|
704
|
+
current_type: Symbol | None = None
|
|
705
|
+
current_type_scope: Scope | None = None
|
|
706
|
+
type_visibility = Visibility.PUBLIC
|
|
707
|
+
|
|
708
|
+
for line_number, raw_line in enumerate(text.splitlines(), start=1):
|
|
709
|
+
line, comment_state = _mask_delphi_comments_and_strings(raw_line, comment_state)
|
|
710
|
+
stripped = line.strip()
|
|
711
|
+
if not stripped:
|
|
712
|
+
continue
|
|
713
|
+
|
|
714
|
+
if section == 'type' and current_type_scope is not None:
|
|
715
|
+
section_match = _SECTION_RE.match(line)
|
|
716
|
+
if section_match and section_match.group(1).casefold() == 'end':
|
|
717
|
+
if current_type is not None:
|
|
718
|
+
current_type.decl_range = SourceRange(
|
|
719
|
+
file_name,
|
|
720
|
+
current_type.decl_range.start_line,
|
|
721
|
+
current_type.decl_range.start_col,
|
|
722
|
+
line_number,
|
|
723
|
+
max(1, len(raw_line) + 1),
|
|
724
|
+
)
|
|
725
|
+
current_type = None
|
|
726
|
+
current_type_scope = None
|
|
727
|
+
type_visibility = Visibility.PUBLIC
|
|
728
|
+
continue
|
|
729
|
+
|
|
730
|
+
visibility_match = _VISIBILITY_RE.match(line)
|
|
731
|
+
if visibility_match:
|
|
732
|
+
type_visibility = _outline_visibility(visibility_match.group(1))
|
|
733
|
+
continue
|
|
734
|
+
|
|
735
|
+
routine_match = _ROUTINE_RE.match(line)
|
|
736
|
+
if routine_match:
|
|
737
|
+
keyword = routine_match.group(1).casefold()
|
|
738
|
+
_define_outline_symbol(
|
|
739
|
+
current_type_scope,
|
|
740
|
+
file_name,
|
|
741
|
+
_normalize_outline_member_name(routine_match.group(2)),
|
|
742
|
+
_routine_symbol_kind(keyword),
|
|
743
|
+
line_number,
|
|
744
|
+
routine_match.start(2),
|
|
745
|
+
routine_match.end(2),
|
|
746
|
+
visibility=type_visibility,
|
|
747
|
+
)
|
|
748
|
+
continue
|
|
749
|
+
|
|
750
|
+
property_match = _PROPERTY_RE.match(line)
|
|
751
|
+
if property_match:
|
|
752
|
+
_define_outline_symbol(
|
|
753
|
+
current_type_scope,
|
|
754
|
+
file_name,
|
|
755
|
+
property_match.group(1),
|
|
756
|
+
SymbolKind.PROPERTY,
|
|
757
|
+
line_number,
|
|
758
|
+
property_match.start(1),
|
|
759
|
+
property_match.end(1),
|
|
760
|
+
visibility=type_visibility,
|
|
761
|
+
type_ref=_outline_type_ref(property_match.group(2) or ''),
|
|
762
|
+
)
|
|
763
|
+
continue
|
|
764
|
+
|
|
765
|
+
field_match = _VARIABLE_RE.match(line)
|
|
766
|
+
if field_match:
|
|
767
|
+
_define_outline_symbol(
|
|
768
|
+
current_type_scope,
|
|
769
|
+
file_name,
|
|
770
|
+
field_match.group(1),
|
|
771
|
+
SymbolKind.FIELD,
|
|
772
|
+
line_number,
|
|
773
|
+
field_match.start(1),
|
|
774
|
+
field_match.end(1),
|
|
775
|
+
visibility=type_visibility,
|
|
776
|
+
type_ref=_outline_type_ref(field_match.group(2)),
|
|
777
|
+
)
|
|
778
|
+
continue
|
|
779
|
+
|
|
780
|
+
routine_match = _ROUTINE_RE.match(line) if section != 'type' else None
|
|
781
|
+
if routine_match:
|
|
782
|
+
keyword = routine_match.group(1).casefold()
|
|
783
|
+
name = _normalize_outline_name(routine_match.group(2))
|
|
784
|
+
current_routine = _define_outline_symbol(
|
|
785
|
+
unit_scope,
|
|
786
|
+
file_name,
|
|
787
|
+
name,
|
|
788
|
+
_routine_symbol_kind(keyword),
|
|
789
|
+
line_number,
|
|
790
|
+
routine_match.start(2),
|
|
791
|
+
routine_match.end(2),
|
|
792
|
+
)
|
|
793
|
+
current_routine_scope = Scope(
|
|
794
|
+
kind=ScopeKind.ROUTINE,
|
|
795
|
+
name=name,
|
|
796
|
+
parent=unit_scope,
|
|
797
|
+
owner=current_routine,
|
|
798
|
+
)
|
|
799
|
+
current_routine.member_scope = current_routine_scope
|
|
800
|
+
routine_var_section = False
|
|
801
|
+
section = ''
|
|
802
|
+
continue
|
|
803
|
+
|
|
804
|
+
section_match = _SECTION_RE.match(line)
|
|
805
|
+
if section_match:
|
|
806
|
+
keyword = section_match.group(1).casefold()
|
|
807
|
+
if current_routine is not None:
|
|
808
|
+
if keyword in {'var', 'threadvar'}:
|
|
809
|
+
routine_var_section = True
|
|
810
|
+
continue
|
|
811
|
+
if keyword == 'begin':
|
|
812
|
+
routine_var_section = False
|
|
813
|
+
continue
|
|
814
|
+
if keyword == 'end':
|
|
815
|
+
current_routine.decl_range = SourceRange(
|
|
816
|
+
file_name,
|
|
817
|
+
current_routine.decl_range.start_line,
|
|
818
|
+
current_routine.decl_range.start_col,
|
|
819
|
+
line_number,
|
|
820
|
+
max(1, len(raw_line) + 1),
|
|
821
|
+
)
|
|
822
|
+
current_routine = None
|
|
823
|
+
current_routine_scope = None
|
|
824
|
+
routine_var_section = False
|
|
825
|
+
continue
|
|
826
|
+
if keyword == 'type':
|
|
827
|
+
section = 'type'
|
|
828
|
+
elif keyword in {'var', 'threadvar', 'const', 'resourcestring'}:
|
|
829
|
+
section = keyword
|
|
830
|
+
elif keyword in {'implementation', 'initialization', 'finalization', 'begin'}:
|
|
831
|
+
section = ''
|
|
832
|
+
continue
|
|
833
|
+
|
|
834
|
+
if current_routine_scope is not None and routine_var_section:
|
|
835
|
+
var_match = _VARIABLE_RE.match(line)
|
|
836
|
+
if var_match:
|
|
837
|
+
_define_outline_symbol(
|
|
838
|
+
current_routine_scope,
|
|
839
|
+
file_name,
|
|
840
|
+
var_match.group(1),
|
|
841
|
+
SymbolKind.VARIABLE,
|
|
842
|
+
line_number,
|
|
843
|
+
var_match.start(1),
|
|
844
|
+
var_match.end(1),
|
|
845
|
+
type_ref=_outline_type_ref(var_match.group(2)),
|
|
846
|
+
)
|
|
847
|
+
continue
|
|
848
|
+
|
|
849
|
+
if section == 'type':
|
|
850
|
+
type_match = _TYPE_RE.match(line)
|
|
851
|
+
if type_match:
|
|
852
|
+
kind = _type_symbol_kind(type_match.group(2))
|
|
853
|
+
type_symbol = _define_outline_symbol(
|
|
854
|
+
unit_scope,
|
|
855
|
+
file_name,
|
|
856
|
+
type_match.group(1),
|
|
857
|
+
kind,
|
|
858
|
+
line_number,
|
|
859
|
+
type_match.start(1),
|
|
860
|
+
type_match.end(1),
|
|
861
|
+
)
|
|
862
|
+
if kind in {SymbolKind.CLASS, SymbolKind.RECORD, SymbolKind.INTERFACE} and not _is_forward_outline_type(type_match.group(2)):
|
|
863
|
+
current_type = type_symbol
|
|
864
|
+
current_type_scope = Scope(
|
|
865
|
+
kind=ScopeKind.TYPE,
|
|
866
|
+
name=type_symbol.name,
|
|
867
|
+
parent=unit_scope,
|
|
868
|
+
owner=type_symbol,
|
|
869
|
+
)
|
|
870
|
+
type_symbol.member_scope = current_type_scope
|
|
871
|
+
type_visibility = Visibility.PUBLIC
|
|
872
|
+
continue
|
|
873
|
+
|
|
874
|
+
if section in {'const', 'resourcestring'}:
|
|
875
|
+
const_match = _CONSTANT_RE.match(line)
|
|
876
|
+
if const_match:
|
|
877
|
+
_define_outline_symbol(
|
|
878
|
+
unit_scope,
|
|
879
|
+
file_name,
|
|
880
|
+
const_match.group(1),
|
|
881
|
+
SymbolKind.CONSTANT,
|
|
882
|
+
line_number,
|
|
883
|
+
const_match.start(1),
|
|
884
|
+
const_match.end(1),
|
|
885
|
+
)
|
|
886
|
+
continue
|
|
887
|
+
|
|
888
|
+
if section in {'var', 'threadvar'}:
|
|
889
|
+
var_match = _VARIABLE_RE.match(line)
|
|
890
|
+
if var_match:
|
|
891
|
+
_define_outline_symbol(
|
|
892
|
+
unit_scope,
|
|
893
|
+
file_name,
|
|
894
|
+
var_match.group(1),
|
|
895
|
+
SymbolKind.VARIABLE,
|
|
896
|
+
line_number,
|
|
897
|
+
var_match.start(1),
|
|
898
|
+
var_match.end(1),
|
|
899
|
+
type_ref=_outline_type_ref(var_match.group(2)),
|
|
900
|
+
)
|
|
901
|
+
|
|
902
|
+
index = SymbolIndex()
|
|
903
|
+
index.register_unit(unit_name, unit_scope)
|
|
904
|
+
return SemanticModel(unit_scope=unit_scope, index=index)
|
|
905
|
+
|
|
906
|
+
|
|
907
|
+
def _outline_unit_name(text: str, file_name: str) -> str:
|
|
908
|
+
comment_state: str | None = None
|
|
909
|
+
for raw_line in text.splitlines()[:200]:
|
|
910
|
+
line, comment_state = _mask_delphi_comments_and_strings(raw_line, comment_state)
|
|
911
|
+
match = _UNIT_RE.match(line)
|
|
912
|
+
if match:
|
|
913
|
+
return _normalize_outline_name(match.group(1))
|
|
914
|
+
return Path(file_name).stem or 'unit'
|
|
915
|
+
|
|
916
|
+
|
|
917
|
+
def _define_outline_symbol(
|
|
918
|
+
scope: Scope,
|
|
919
|
+
file_name: str,
|
|
920
|
+
name: str,
|
|
921
|
+
kind: SymbolKind,
|
|
922
|
+
line_number: int,
|
|
923
|
+
start_col_0: int,
|
|
924
|
+
end_col_0: int,
|
|
925
|
+
*,
|
|
926
|
+
visibility: Visibility = Visibility.UNKNOWN,
|
|
927
|
+
type_ref: TypeRef | None = None,
|
|
928
|
+
) -> Symbol:
|
|
929
|
+
symbol_range = SourceRange(
|
|
930
|
+
file_name,
|
|
931
|
+
line_number,
|
|
932
|
+
start_col_0 + 1,
|
|
933
|
+
line_number,
|
|
934
|
+
max(start_col_0 + 2, end_col_0 + 1),
|
|
935
|
+
)
|
|
936
|
+
symbol = Symbol(
|
|
937
|
+
name=name,
|
|
938
|
+
kind=kind,
|
|
939
|
+
decl_range=symbol_range,
|
|
940
|
+
name_range=symbol_range,
|
|
941
|
+
scope=scope,
|
|
942
|
+
visibility=visibility,
|
|
943
|
+
type_ref=type_ref or UnknownTypeRef(),
|
|
944
|
+
)
|
|
945
|
+
scope.define(symbol)
|
|
946
|
+
return symbol
|
|
947
|
+
|
|
948
|
+
|
|
949
|
+
def _normalize_outline_name(name: str) -> str:
|
|
950
|
+
return re.sub(r'\s*\.\s*', '.', name.strip())
|
|
951
|
+
|
|
952
|
+
|
|
953
|
+
def _normalize_outline_member_name(name: str) -> str:
|
|
954
|
+
return _normalize_outline_name(name).split('.')[-1]
|
|
955
|
+
|
|
956
|
+
|
|
957
|
+
def _outline_visibility(name: str) -> Visibility:
|
|
958
|
+
normalized = name.casefold().replace(' ', '_')
|
|
959
|
+
mapping = {
|
|
960
|
+
'private': Visibility.PRIVATE,
|
|
961
|
+
'protected': Visibility.PROTECTED,
|
|
962
|
+
'public': Visibility.PUBLIC,
|
|
963
|
+
'published': Visibility.PUBLISHED,
|
|
964
|
+
'strict_private': Visibility.STRICT_PRIVATE,
|
|
965
|
+
'strict_protected': Visibility.STRICT_PROTECTED,
|
|
966
|
+
}
|
|
967
|
+
return mapping.get(normalized, Visibility.UNKNOWN)
|
|
968
|
+
|
|
969
|
+
|
|
970
|
+
def _outline_type_ref(type_text: str) -> TypeRef:
|
|
971
|
+
cleaned = type_text.strip()
|
|
972
|
+
if not cleaned:
|
|
973
|
+
return UnknownTypeRef()
|
|
974
|
+
stop_match = _TYPE_REF_STOP_RE.search(cleaned)
|
|
975
|
+
if stop_match:
|
|
976
|
+
cleaned = cleaned[:stop_match.start()].strip()
|
|
977
|
+
cleaned = cleaned.rstrip(';').strip()
|
|
978
|
+
if not cleaned:
|
|
979
|
+
return UnknownTypeRef()
|
|
980
|
+
return SemanticBuilder()._type_ref_from_name(cleaned)
|
|
981
|
+
|
|
982
|
+
|
|
983
|
+
def _is_forward_outline_type(type_text: str) -> bool:
|
|
984
|
+
rhs = type_text.strip().casefold()
|
|
985
|
+
return rhs in {'class;', 'record;', 'interface;'} or rhs.endswith(' class;') or rhs.endswith(' interface;')
|
|
986
|
+
|
|
987
|
+
|
|
988
|
+
def _routine_symbol_kind(keyword: str) -> SymbolKind:
|
|
989
|
+
if keyword == 'procedure':
|
|
990
|
+
return SymbolKind.PROCEDURE
|
|
991
|
+
if keyword == 'constructor':
|
|
992
|
+
return SymbolKind.CONSTRUCTOR
|
|
993
|
+
if keyword == 'destructor':
|
|
994
|
+
return SymbolKind.DESTRUCTOR
|
|
995
|
+
return SymbolKind.FUNCTION
|
|
996
|
+
|
|
997
|
+
|
|
998
|
+
def _type_symbol_kind(type_text: str) -> SymbolKind:
|
|
999
|
+
rhs = type_text.lstrip().casefold()
|
|
1000
|
+
if rhs.startswith('packed '):
|
|
1001
|
+
rhs = rhs[7:].lstrip()
|
|
1002
|
+
if rhs.startswith('class') and not rhs.startswith('class of'):
|
|
1003
|
+
return SymbolKind.CLASS
|
|
1004
|
+
if rhs.startswith('record'):
|
|
1005
|
+
return SymbolKind.RECORD
|
|
1006
|
+
if rhs.startswith('interface'):
|
|
1007
|
+
return SymbolKind.INTERFACE
|
|
1008
|
+
if rhs.startswith('('):
|
|
1009
|
+
return SymbolKind.ENUM
|
|
1010
|
+
return SymbolKind.TYPE
|
|
1011
|
+
|
|
1012
|
+
|
|
1013
|
+
def _mask_delphi_comments_and_strings(line: str, state: str | None) -> tuple[str, str | None]:
|
|
1014
|
+
chars = list(line)
|
|
1015
|
+
i = 0
|
|
1016
|
+
n = len(line)
|
|
1017
|
+
while i < n:
|
|
1018
|
+
ch = line[i]
|
|
1019
|
+
nxt = line[i + 1] if i + 1 < n else ''
|
|
1020
|
+
|
|
1021
|
+
if state == 'brace':
|
|
1022
|
+
chars[i] = ' '
|
|
1023
|
+
if ch == '}':
|
|
1024
|
+
state = None
|
|
1025
|
+
i += 1
|
|
1026
|
+
continue
|
|
1027
|
+
|
|
1028
|
+
if state == 'paren':
|
|
1029
|
+
chars[i] = ' '
|
|
1030
|
+
if ch == '*' and nxt == ')':
|
|
1031
|
+
chars[i + 1] = ' '
|
|
1032
|
+
state = None
|
|
1033
|
+
i += 2
|
|
1034
|
+
else:
|
|
1035
|
+
i += 1
|
|
1036
|
+
continue
|
|
1037
|
+
|
|
1038
|
+
if ch == "'":
|
|
1039
|
+
chars[i] = ' '
|
|
1040
|
+
i += 1
|
|
1041
|
+
while i < n:
|
|
1042
|
+
chars[i] = ' '
|
|
1043
|
+
if line[i] == "'":
|
|
1044
|
+
if i + 1 < n and line[i + 1] == "'":
|
|
1045
|
+
chars[i + 1] = ' '
|
|
1046
|
+
i += 2
|
|
1047
|
+
continue
|
|
1048
|
+
i += 1
|
|
1049
|
+
break
|
|
1050
|
+
i += 1
|
|
1051
|
+
continue
|
|
1052
|
+
|
|
1053
|
+
if ch == '/' and nxt == '/':
|
|
1054
|
+
for index in range(i, n):
|
|
1055
|
+
chars[index] = ' '
|
|
1056
|
+
break
|
|
1057
|
+
|
|
1058
|
+
if ch == '{':
|
|
1059
|
+
chars[i] = ' '
|
|
1060
|
+
state = 'brace'
|
|
1061
|
+
i += 1
|
|
1062
|
+
continue
|
|
1063
|
+
|
|
1064
|
+
if ch == '(' and nxt == '*':
|
|
1065
|
+
chars[i] = ' '
|
|
1066
|
+
chars[i + 1] = ' '
|
|
1067
|
+
state = 'paren'
|
|
1068
|
+
i += 2
|
|
1069
|
+
continue
|
|
1070
|
+
|
|
1071
|
+
i += 1
|
|
1072
|
+
return ''.join(chars), state
|
|
1073
|
+
|
|
1074
|
+
|
|
1075
|
+
def path_to_uri(path: str) -> str:
|
|
1076
|
+
if '://' in path:
|
|
1077
|
+
return path
|
|
1078
|
+
return Path(path).absolute().as_uri()
|
|
1079
|
+
|
|
1080
|
+
|
|
1081
|
+
def find_reference_at_position(
|
|
1082
|
+
model: SemanticModel,
|
|
1083
|
+
*,
|
|
1084
|
+
line: int,
|
|
1085
|
+
character: int,
|
|
1086
|
+
) -> Optional[SymbolReference]:
|
|
1087
|
+
line_1 = line + 1
|
|
1088
|
+
col_1 = character + 1
|
|
1089
|
+
candidates = [
|
|
1090
|
+
ref for ref in model.references if ref.ref_range.contains(line_1, col_1)
|
|
1091
|
+
]
|
|
1092
|
+
if not candidates:
|
|
1093
|
+
same_line = [
|
|
1094
|
+
ref for ref in model.references if ref.ref_range.start_line == line_1
|
|
1095
|
+
]
|
|
1096
|
+
if not same_line:
|
|
1097
|
+
return None
|
|
1098
|
+
return min(
|
|
1099
|
+
same_line,
|
|
1100
|
+
key=lambda ref: abs(ref.ref_range.start_col - col_1),
|
|
1101
|
+
)
|
|
1102
|
+
return min(candidates, key=_reference_span)
|
|
1103
|
+
|
|
1104
|
+
|
|
1105
|
+
def find_symbol_at_position(
|
|
1106
|
+
model: SemanticModel,
|
|
1107
|
+
*,
|
|
1108
|
+
line: int,
|
|
1109
|
+
character: int,
|
|
1110
|
+
) -> Optional[Symbol]:
|
|
1111
|
+
line_1 = line + 1
|
|
1112
|
+
col_1 = character + 1
|
|
1113
|
+
candidates: list[Symbol] = []
|
|
1114
|
+
for symbol in iter_symbols(model.unit_scope):
|
|
1115
|
+
if symbol.name_range.contains(line_1, col_1):
|
|
1116
|
+
candidates.append(symbol)
|
|
1117
|
+
if not candidates:
|
|
1118
|
+
return None
|
|
1119
|
+
return min(candidates, key=lambda sym: _range_span(sym.name_range))
|
|
1120
|
+
|
|
1121
|
+
|
|
1122
|
+
def find_identifier_symbol_at_position(
|
|
1123
|
+
model: SemanticModel,
|
|
1124
|
+
text: str,
|
|
1125
|
+
*,
|
|
1126
|
+
line: int,
|
|
1127
|
+
character: int,
|
|
1128
|
+
) -> Optional[Symbol]:
|
|
1129
|
+
identifier = identifier_at_position(text, line, character)
|
|
1130
|
+
if identifier is None:
|
|
1131
|
+
return None
|
|
1132
|
+
scope = scope_at_line(model.unit_scope, line + 1)
|
|
1133
|
+
if scope is None:
|
|
1134
|
+
return None
|
|
1135
|
+
resolved = scope.resolve(identifier)
|
|
1136
|
+
if resolved:
|
|
1137
|
+
return resolved[0]
|
|
1138
|
+
indexed = model.index.lookup(identifier)
|
|
1139
|
+
return indexed[0] if indexed else None
|
|
1140
|
+
|
|
1141
|
+
|
|
1142
|
+
def identifier_at_position(text: str, line: int, character: int) -> Optional[str]:
|
|
1143
|
+
lines = text.splitlines()
|
|
1144
|
+
if line < 0 or line >= len(lines):
|
|
1145
|
+
return None
|
|
1146
|
+
line_text = lines[line]
|
|
1147
|
+
if not line_text:
|
|
1148
|
+
return None
|
|
1149
|
+
idx = min(max(character, 0), len(line_text) - 1)
|
|
1150
|
+
if not _is_identifier_char(line_text[idx]) and idx > 0 and _is_identifier_char(line_text[idx - 1]):
|
|
1151
|
+
idx -= 1
|
|
1152
|
+
if not _is_identifier_char(line_text[idx]):
|
|
1153
|
+
return None
|
|
1154
|
+
start = idx
|
|
1155
|
+
while start > 0 and _is_identifier_char(line_text[start - 1]):
|
|
1156
|
+
start -= 1
|
|
1157
|
+
end = idx + 1
|
|
1158
|
+
while end < len(line_text) and _is_identifier_char(line_text[end]):
|
|
1159
|
+
end += 1
|
|
1160
|
+
return line_text[start:end]
|
|
1161
|
+
|
|
1162
|
+
|
|
1163
|
+
def scope_at_line(scope: Scope, line_1: int) -> Optional[Scope]:
|
|
1164
|
+
best_scope = scope
|
|
1165
|
+
best_span = float('inf')
|
|
1166
|
+
for symbol in iter_symbols(scope):
|
|
1167
|
+
member_scope = symbol.member_scope
|
|
1168
|
+
if member_scope is None:
|
|
1169
|
+
continue
|
|
1170
|
+
if not (symbol.decl_range.start_line <= line_1 <= symbol.decl_range.end_line):
|
|
1171
|
+
continue
|
|
1172
|
+
span = symbol.decl_range.end_line - symbol.decl_range.start_line
|
|
1173
|
+
if span < best_span:
|
|
1174
|
+
best_span = span
|
|
1175
|
+
best_scope = member_scope
|
|
1176
|
+
return best_scope
|
|
1177
|
+
|
|
1178
|
+
|
|
1179
|
+
def _is_identifier_char(ch: str) -> bool:
|
|
1180
|
+
return ch.isalnum() or ch == '_'
|
|
1181
|
+
|
|
1182
|
+
|
|
1183
|
+
def _range_span(range_value: SourceRange) -> tuple[int, int]:
|
|
1184
|
+
return (
|
|
1185
|
+
range_value.end_line - range_value.start_line,
|
|
1186
|
+
range_value.end_col - range_value.start_col,
|
|
1187
|
+
)
|
|
1188
|
+
|
|
1189
|
+
|
|
1190
|
+
def _reference_span(reference: SymbolReference) -> tuple[int, int]:
|
|
1191
|
+
return _range_span(reference.ref_range)
|
|
1192
|
+
|
|
1193
|
+
|
|
1194
|
+
def iter_symbols(scope) -> Iterable[Symbol]:
|
|
1195
|
+
for symbols in scope.symbols.values():
|
|
1196
|
+
for symbol in symbols:
|
|
1197
|
+
yield symbol
|
|
1198
|
+
if symbol.member_scope is not None:
|
|
1199
|
+
yield from iter_symbols(symbol.member_scope)
|
|
1200
|
+
|
|
1201
|
+
|
|
1202
|
+
def hover_text(symbol: Symbol) -> str:
|
|
1203
|
+
type_desc = symbol.type_ref.display_name()
|
|
1204
|
+
return f'{symbol.kind.value} {symbol.name}: {type_desc}'
|
|
1205
|
+
|
|
1206
|
+
|
|
1207
|
+
def source_range_to_lsp(range_value: SourceRange) -> tuple[int, int, int, int]:
|
|
1208
|
+
return (
|
|
1209
|
+
range_value.start_line - 1,
|
|
1210
|
+
range_value.start_col - 1,
|
|
1211
|
+
range_value.end_line - 1,
|
|
1212
|
+
range_value.end_col - 1,
|
|
1213
|
+
)
|
|
1214
|
+
|
|
1215
|
+
|
|
1216
|
+
def resolve_reference(model: SemanticModel, name: str, scope: Scope | None = None) -> Optional[Symbol]:
|
|
1217
|
+
builder = SemanticBuilder()
|
|
1218
|
+
builder._index = model.index
|
|
1219
|
+
return builder._resolve_reference(name, ReferenceKind.VALUE, scope or model.unit_scope)
|
|
1220
|
+
|
|
1221
|
+
|
|
1222
|
+
def split_reference_parts(name: str) -> list[str]:
|
|
1223
|
+
builder = SemanticBuilder()
|
|
1224
|
+
return builder._normalized_reference_parts(name)
|
|
1225
|
+
|
|
1226
|
+
|
|
1227
|
+
def resolve_base_for_member_completion(model: SemanticModel, name: str, scope: Scope | None = None) -> Optional[Symbol]:
|
|
1228
|
+
lookup_scope = scope or model.unit_scope
|
|
1229
|
+
parts = split_reference_parts(name)
|
|
1230
|
+
if len(parts) <= 1:
|
|
1231
|
+
symbol = resolve_reference(model, name, lookup_scope)
|
|
1232
|
+
if symbol is not None:
|
|
1233
|
+
return symbol
|
|
1234
|
+
symbols = model.index.lookup(name)
|
|
1235
|
+
return symbols[0] if symbols else None
|
|
1236
|
+
base_name = '.'.join(parts[:-1])
|
|
1237
|
+
symbol = resolve_reference(model, base_name, lookup_scope)
|
|
1238
|
+
if symbol is not None:
|
|
1239
|
+
return symbol
|
|
1240
|
+
symbols = model.index.lookup(base_name)
|
|
1241
|
+
return symbols[0] if symbols else None
|
|
1242
|
+
|
|
1243
|
+
|
|
1244
|
+
def iter_member_symbols(model: SemanticModel, symbol: Symbol) -> Iterable[Symbol]:
|
|
1245
|
+
builder = SemanticBuilder()
|
|
1246
|
+
builder._index = model.index
|
|
1247
|
+
for scope in builder._iter_member_scopes(symbol, model.unit_scope):
|
|
1248
|
+
for symbols in scope.symbols.values():
|
|
1249
|
+
for member in symbols:
|
|
1250
|
+
yield member
|
|
1251
|
+
|
|
1252
|
+
|
|
1253
|
+
def completion_items_for_scope(scope) -> list[Symbol]:
|
|
1254
|
+
seen: set[str] = set()
|
|
1255
|
+
items: list[Symbol] = []
|
|
1256
|
+
for symbols in scope.symbols.values():
|
|
1257
|
+
for symbol in symbols:
|
|
1258
|
+
key = symbol.name.casefold()
|
|
1259
|
+
if key not in seen:
|
|
1260
|
+
seen.add(key)
|
|
1261
|
+
items.append(symbol)
|
|
1262
|
+
for imported in scope.imports:
|
|
1263
|
+
for symbols in imported.symbols.values():
|
|
1264
|
+
for symbol in symbols:
|
|
1265
|
+
key = symbol.name.casefold()
|
|
1266
|
+
if key not in seen:
|
|
1267
|
+
seen.add(key)
|
|
1268
|
+
items.append(symbol)
|
|
1269
|
+
return items
|
|
1270
|
+
|
|
1271
|
+
|
|
1272
|
+
def references_for_symbol(workspace: WorkspaceSemanticResult, symbol: Symbol) -> list[SymbolReference]:
|
|
1273
|
+
refs: list[SymbolReference] = []
|
|
1274
|
+
for model in workspace.models.values():
|
|
1275
|
+
for ref in model.references:
|
|
1276
|
+
if ref.resolved is symbol:
|
|
1277
|
+
refs.append(ref)
|
|
1278
|
+
return refs
|
|
1279
|
+
|
|
1280
|
+
|
|
1281
|
+
def text_references_for_symbol(
|
|
1282
|
+
text: str,
|
|
1283
|
+
symbol: Symbol,
|
|
1284
|
+
*,
|
|
1285
|
+
include_declaration: bool,
|
|
1286
|
+
) -> list[SourceRange]:
|
|
1287
|
+
search_range = _text_reference_search_range(symbol)
|
|
1288
|
+
lines = text.splitlines()
|
|
1289
|
+
if search_range.start_line < 1 or search_range.start_line > len(lines):
|
|
1290
|
+
return []
|
|
1291
|
+
end_line = min(search_range.end_line, len(lines))
|
|
1292
|
+
pattern = re.compile(rf'\b{re.escape(symbol.name)}\b', re.IGNORECASE)
|
|
1293
|
+
ranges: list[SourceRange] = []
|
|
1294
|
+
for line_number in range(search_range.start_line, end_line + 1):
|
|
1295
|
+
line_text = lines[line_number - 1]
|
|
1296
|
+
for match in pattern.finditer(line_text):
|
|
1297
|
+
ref_range = SourceRange(
|
|
1298
|
+
symbol.decl_range.file_name,
|
|
1299
|
+
line_number,
|
|
1300
|
+
match.start() + 1,
|
|
1301
|
+
line_number,
|
|
1302
|
+
match.end() + 1,
|
|
1303
|
+
)
|
|
1304
|
+
if not include_declaration and _same_range(ref_range, symbol.name_range):
|
|
1305
|
+
continue
|
|
1306
|
+
ranges.append(ref_range)
|
|
1307
|
+
return ranges
|
|
1308
|
+
|
|
1309
|
+
|
|
1310
|
+
def _text_reference_search_range(symbol: Symbol) -> SourceRange:
|
|
1311
|
+
owner = symbol.scope.owner
|
|
1312
|
+
if owner is not None and owner is not symbol:
|
|
1313
|
+
return owner.decl_range
|
|
1314
|
+
return SourceRange(
|
|
1315
|
+
symbol.decl_range.file_name,
|
|
1316
|
+
1,
|
|
1317
|
+
1,
|
|
1318
|
+
max(symbol.decl_range.end_line, 1_000_000_000),
|
|
1319
|
+
1,
|
|
1320
|
+
)
|
|
1321
|
+
|
|
1322
|
+
|
|
1323
|
+
def _same_range(left: SourceRange, right: SourceRange) -> bool:
|
|
1324
|
+
return (
|
|
1325
|
+
left.file_name == right.file_name
|
|
1326
|
+
and left.start_line == right.start_line
|
|
1327
|
+
and left.start_col == right.start_col
|
|
1328
|
+
and left.end_line == right.end_line
|
|
1329
|
+
and left.end_col == right.end_col
|
|
1330
|
+
)
|
|
1331
|
+
|
|
1332
|
+
|
|
1333
|
+
def diagnostics_for_model(model: SemanticModel):
|
|
1334
|
+
try:
|
|
1335
|
+
from lsprotocol.types import Diagnostic, DiagnosticSeverity, Range, Position
|
|
1336
|
+
except ImportError:
|
|
1337
|
+
return []
|
|
1338
|
+
diagnostics = []
|
|
1339
|
+
for problem in model.problems:
|
|
1340
|
+
start_line, start_col, end_line, end_col = source_range_to_lsp(problem.range)
|
|
1341
|
+
diagnostics.append(
|
|
1342
|
+
Diagnostic(
|
|
1343
|
+
range=Range(
|
|
1344
|
+
start=Position(line=start_line, character=start_col),
|
|
1345
|
+
end=Position(line=end_line, character=end_col),
|
|
1346
|
+
),
|
|
1347
|
+
message=problem.message,
|
|
1348
|
+
severity=DiagnosticSeverity.Error,
|
|
1349
|
+
source='agent-codinglanguage-mapper',
|
|
1350
|
+
)
|
|
1351
|
+
)
|
|
1352
|
+
return diagnostics
|
|
1353
|
+
|
|
1354
|
+
|
|
1355
|
+
def extract_completion_base(text: str, line: int, character: int) -> Optional[str]:
|
|
1356
|
+
lines = text.splitlines()
|
|
1357
|
+
if line < 0 or line >= len(lines):
|
|
1358
|
+
return None
|
|
1359
|
+
line_text = lines[line]
|
|
1360
|
+
if character <= 0 or character > len(line_text):
|
|
1361
|
+
return None
|
|
1362
|
+
if line_text[character - 1] != '.':
|
|
1363
|
+
return None
|
|
1364
|
+
idx = character - 2
|
|
1365
|
+
if idx < 0:
|
|
1366
|
+
return None
|
|
1367
|
+
allowed = set('abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_<>.,&')
|
|
1368
|
+
buf = []
|
|
1369
|
+
while idx >= 0:
|
|
1370
|
+
ch = line_text[idx]
|
|
1371
|
+
if ch.isspace():
|
|
1372
|
+
break
|
|
1373
|
+
if ch in allowed:
|
|
1374
|
+
buf.append(ch)
|
|
1375
|
+
idx -= 1
|
|
1376
|
+
continue
|
|
1377
|
+
break
|
|
1378
|
+
if not buf:
|
|
1379
|
+
return None
|
|
1380
|
+
return ''.join(reversed(buf)).strip()
|
|
1381
|
+
|
|
1382
|
+
|
|
1383
|
+
# LSP server entrypoint
|
|
1384
|
+
|
|
1385
|
+
def create_server():
|
|
1386
|
+
try:
|
|
1387
|
+
from pygls.server import LanguageServer
|
|
1388
|
+
from lsprotocol.types import (
|
|
1389
|
+
CompletionItem,
|
|
1390
|
+
CompletionItemKind,
|
|
1391
|
+
CompletionList,
|
|
1392
|
+
CompletionOptions,
|
|
1393
|
+
CompletionParams,
|
|
1394
|
+
DefinitionParams,
|
|
1395
|
+
Hover,
|
|
1396
|
+
HoverParams,
|
|
1397
|
+
InitializeParams,
|
|
1398
|
+
InitializeResult,
|
|
1399
|
+
Location,
|
|
1400
|
+
Position,
|
|
1401
|
+
Range,
|
|
1402
|
+
ReferenceParams,
|
|
1403
|
+
RenameParams,
|
|
1404
|
+
ServerCapabilities,
|
|
1405
|
+
SymbolInformation,
|
|
1406
|
+
SymbolKind as LspSymbolKind,
|
|
1407
|
+
TextDocumentSyncKind,
|
|
1408
|
+
TextDocumentSyncOptions,
|
|
1409
|
+
TextEdit,
|
|
1410
|
+
WorkspaceEdit,
|
|
1411
|
+
WorkspaceSymbolParams,
|
|
1412
|
+
DocumentSymbolParams,
|
|
1413
|
+
DocumentSymbol,
|
|
1414
|
+
TEXT_DOCUMENT_COMPLETION,
|
|
1415
|
+
TEXT_DOCUMENT_DEFINITION,
|
|
1416
|
+
TEXT_DOCUMENT_DID_CHANGE,
|
|
1417
|
+
TEXT_DOCUMENT_DID_CLOSE,
|
|
1418
|
+
TEXT_DOCUMENT_DID_OPEN,
|
|
1419
|
+
TEXT_DOCUMENT_HOVER,
|
|
1420
|
+
TEXT_DOCUMENT_REFERENCES,
|
|
1421
|
+
TEXT_DOCUMENT_RENAME,
|
|
1422
|
+
TEXT_DOCUMENT_DOCUMENT_SYMBOL,
|
|
1423
|
+
WORKSPACE_SYMBOL,
|
|
1424
|
+
INITIALIZE,
|
|
1425
|
+
)
|
|
1426
|
+
except ImportError as exc: # pragma: no cover - optional dependency
|
|
1427
|
+
raise RuntimeError('pygls and lsprotocol are required for the LSP server') from exc
|
|
1428
|
+
|
|
1429
|
+
server = LanguageServer('agent-codinglanguage-mapper', __version__)
|
|
1430
|
+
state = LspWorkspaceState()
|
|
1431
|
+
|
|
1432
|
+
def _symbol_kind(symbol: Symbol) -> LspSymbolKind:
|
|
1433
|
+
mapping = {
|
|
1434
|
+
SymbolKind.CLASS: LspSymbolKind.Class,
|
|
1435
|
+
SymbolKind.RECORD: LspSymbolKind.Struct,
|
|
1436
|
+
SymbolKind.INTERFACE: LspSymbolKind.Interface,
|
|
1437
|
+
SymbolKind.ENUM: LspSymbolKind.Enum,
|
|
1438
|
+
SymbolKind.ENUM_VALUE: LspSymbolKind.EnumMember,
|
|
1439
|
+
SymbolKind.FIELD: LspSymbolKind.Field,
|
|
1440
|
+
SymbolKind.PROPERTY: LspSymbolKind.Property,
|
|
1441
|
+
SymbolKind.METHOD: LspSymbolKind.Method,
|
|
1442
|
+
SymbolKind.FUNCTION: LspSymbolKind.Function,
|
|
1443
|
+
SymbolKind.PROCEDURE: LspSymbolKind.Function,
|
|
1444
|
+
SymbolKind.CONSTRUCTOR: LspSymbolKind.Constructor,
|
|
1445
|
+
SymbolKind.DESTRUCTOR: LspSymbolKind.Method,
|
|
1446
|
+
SymbolKind.VARIABLE: LspSymbolKind.Variable,
|
|
1447
|
+
SymbolKind.PARAMETER: LspSymbolKind.Variable,
|
|
1448
|
+
SymbolKind.CONSTANT: LspSymbolKind.Constant,
|
|
1449
|
+
SymbolKind.TYPE: LspSymbolKind.Class,
|
|
1450
|
+
}
|
|
1451
|
+
return mapping.get(symbol.kind, LspSymbolKind.String)
|
|
1452
|
+
|
|
1453
|
+
def _completion_kind(symbol: Symbol) -> CompletionItemKind:
|
|
1454
|
+
mapping = {
|
|
1455
|
+
SymbolKind.CLASS: CompletionItemKind.Class,
|
|
1456
|
+
SymbolKind.RECORD: CompletionItemKind.Struct,
|
|
1457
|
+
SymbolKind.INTERFACE: CompletionItemKind.Interface,
|
|
1458
|
+
SymbolKind.ENUM: CompletionItemKind.Enum,
|
|
1459
|
+
SymbolKind.ENUM_VALUE: CompletionItemKind.EnumMember,
|
|
1460
|
+
SymbolKind.FIELD: CompletionItemKind.Field,
|
|
1461
|
+
SymbolKind.PROPERTY: CompletionItemKind.Property,
|
|
1462
|
+
SymbolKind.METHOD: CompletionItemKind.Method,
|
|
1463
|
+
SymbolKind.FUNCTION: CompletionItemKind.Function,
|
|
1464
|
+
SymbolKind.PROCEDURE: CompletionItemKind.Function,
|
|
1465
|
+
SymbolKind.CONSTRUCTOR: CompletionItemKind.Constructor,
|
|
1466
|
+
SymbolKind.DESTRUCTOR: CompletionItemKind.Method,
|
|
1467
|
+
SymbolKind.VARIABLE: CompletionItemKind.Variable,
|
|
1468
|
+
SymbolKind.PARAMETER: CompletionItemKind.Variable,
|
|
1469
|
+
SymbolKind.CONSTANT: CompletionItemKind.Constant,
|
|
1470
|
+
SymbolKind.TYPE: CompletionItemKind.Class,
|
|
1471
|
+
}
|
|
1472
|
+
return mapping.get(symbol.kind, CompletionItemKind.Text)
|
|
1473
|
+
|
|
1474
|
+
def _document_symbols_for_scope(scope) -> list[DocumentSymbol]:
|
|
1475
|
+
items: list[DocumentSymbol] = []
|
|
1476
|
+
for symbols in scope.symbols.values():
|
|
1477
|
+
for symbol in symbols:
|
|
1478
|
+
if symbol.kind == SymbolKind.UNIT:
|
|
1479
|
+
continue
|
|
1480
|
+
start_line, start_col, end_line, end_col = source_range_to_lsp(symbol.decl_range)
|
|
1481
|
+
sel_start, sel_col, sel_end, sel_end_col = source_range_to_lsp(symbol.name_range)
|
|
1482
|
+
children = []
|
|
1483
|
+
if symbol.member_scope is not None:
|
|
1484
|
+
children = _document_symbols_for_scope(symbol.member_scope)
|
|
1485
|
+
items.append(
|
|
1486
|
+
DocumentSymbol(
|
|
1487
|
+
name=symbol.name,
|
|
1488
|
+
kind=_symbol_kind(symbol),
|
|
1489
|
+
range=Range(
|
|
1490
|
+
start=Position(line=start_line, character=start_col),
|
|
1491
|
+
end=Position(line=end_line, character=end_col),
|
|
1492
|
+
),
|
|
1493
|
+
selection_range=Range(
|
|
1494
|
+
start=Position(line=sel_start, character=sel_col),
|
|
1495
|
+
end=Position(line=sel_end, character=sel_end_col),
|
|
1496
|
+
),
|
|
1497
|
+
children=children or None,
|
|
1498
|
+
)
|
|
1499
|
+
)
|
|
1500
|
+
return items
|
|
1501
|
+
|
|
1502
|
+
@server.feature(INITIALIZE)
|
|
1503
|
+
def initialize(ls: LanguageServer, params: InitializeParams) -> InitializeResult:
|
|
1504
|
+
roots: list[str] = []
|
|
1505
|
+
if params.workspace_folders:
|
|
1506
|
+
roots.extend(uri_to_path(folder.uri) for folder in params.workspace_folders)
|
|
1507
|
+
elif params.root_uri:
|
|
1508
|
+
roots.append(uri_to_path(params.root_uri))
|
|
1509
|
+
init_opts = params.initialization_options or {}
|
|
1510
|
+
include_paths = [uri_to_path(path) for path in init_opts.get('includePaths', [])]
|
|
1511
|
+
search_paths = [uri_to_path(path) for path in init_opts.get('searchPaths', [])]
|
|
1512
|
+
defines = init_opts.get('defines', [])
|
|
1513
|
+
auto_discover_paths = init_opts.get('autoDiscoverPaths', True)
|
|
1514
|
+
config = WorkspaceConfig(
|
|
1515
|
+
roots=roots,
|
|
1516
|
+
include_paths=include_paths,
|
|
1517
|
+
search_paths=search_paths,
|
|
1518
|
+
defines=defines,
|
|
1519
|
+
auto_discover_paths=bool(auto_discover_paths),
|
|
1520
|
+
)
|
|
1521
|
+
state.configure(config)
|
|
1522
|
+
|
|
1523
|
+
capabilities = ServerCapabilities(
|
|
1524
|
+
text_document_sync=TextDocumentSyncOptions(
|
|
1525
|
+
open_close=True,
|
|
1526
|
+
change=TextDocumentSyncKind.Full,
|
|
1527
|
+
),
|
|
1528
|
+
definition_provider=True,
|
|
1529
|
+
references_provider=True,
|
|
1530
|
+
hover_provider=True,
|
|
1531
|
+
completion_provider=CompletionOptions(trigger_characters=['.']),
|
|
1532
|
+
rename_provider=True,
|
|
1533
|
+
document_symbol_provider=True,
|
|
1534
|
+
workspace_symbol_provider=True,
|
|
1535
|
+
)
|
|
1536
|
+
return InitializeResult(capabilities=capabilities)
|
|
1537
|
+
|
|
1538
|
+
def _publish_diagnostics(ls: LanguageServer, uri: str) -> None:
|
|
1539
|
+
diagnostics = state.diagnostics_for_uri(uri)
|
|
1540
|
+
ls.publish_diagnostics(uri, diagnostics)
|
|
1541
|
+
|
|
1542
|
+
def _symbol_at_position_in_model(
|
|
1543
|
+
model: SemanticModel,
|
|
1544
|
+
uri: str,
|
|
1545
|
+
position: Position,
|
|
1546
|
+
text: str | None = None,
|
|
1547
|
+
) -> Optional[Symbol]:
|
|
1548
|
+
if text is None:
|
|
1549
|
+
text = state.text_for_uri(uri)
|
|
1550
|
+
ref = find_reference_at_position(
|
|
1551
|
+
model,
|
|
1552
|
+
line=position.line,
|
|
1553
|
+
character=position.character,
|
|
1554
|
+
)
|
|
1555
|
+
symbol = ref.resolved if ref and ref.resolved else find_symbol_at_position(
|
|
1556
|
+
model,
|
|
1557
|
+
line=position.line,
|
|
1558
|
+
character=position.character,
|
|
1559
|
+
)
|
|
1560
|
+
if symbol is None and text is not None:
|
|
1561
|
+
symbol = find_identifier_symbol_at_position(
|
|
1562
|
+
model,
|
|
1563
|
+
text,
|
|
1564
|
+
line=position.line,
|
|
1565
|
+
character=position.character,
|
|
1566
|
+
)
|
|
1567
|
+
return symbol
|
|
1568
|
+
|
|
1569
|
+
def _symbol_at_position(uri: str, position: Position) -> Optional[Symbol]:
|
|
1570
|
+
text = state.text_for_uri(uri)
|
|
1571
|
+
model = state.semantic_for_uri(uri)
|
|
1572
|
+
if model is not None:
|
|
1573
|
+
symbol = _symbol_at_position_in_model(model, uri, position, text)
|
|
1574
|
+
if symbol is not None:
|
|
1575
|
+
return symbol
|
|
1576
|
+
model = state.full_semantic_for_uri(uri)
|
|
1577
|
+
if model is not None:
|
|
1578
|
+
symbol = _symbol_at_position_in_model(model, uri, position, text)
|
|
1579
|
+
if symbol is not None:
|
|
1580
|
+
return symbol
|
|
1581
|
+
return None
|
|
1582
|
+
|
|
1583
|
+
def _add_rename_edit(
|
|
1584
|
+
edits: dict[str, list[TextEdit]],
|
|
1585
|
+
seen: set[tuple[str, int, int, int, int]],
|
|
1586
|
+
ref_range: SourceRange,
|
|
1587
|
+
new_text: str,
|
|
1588
|
+
) -> None:
|
|
1589
|
+
uri = state.uri_for_file_name(ref_range.file_name) or path_to_uri(ref_range.file_name)
|
|
1590
|
+
key = (uri, ref_range.start_line, ref_range.start_col, ref_range.end_line, ref_range.end_col)
|
|
1591
|
+
if key in seen:
|
|
1592
|
+
return
|
|
1593
|
+
seen.add(key)
|
|
1594
|
+
start_line, start_col, end_line, end_col = source_range_to_lsp(ref_range)
|
|
1595
|
+
edits.setdefault(uri, []).append(
|
|
1596
|
+
TextEdit(
|
|
1597
|
+
range=Range(
|
|
1598
|
+
start=Position(line=start_line, character=start_col),
|
|
1599
|
+
end=Position(line=end_line, character=end_col),
|
|
1600
|
+
),
|
|
1601
|
+
new_text=new_text,
|
|
1602
|
+
)
|
|
1603
|
+
)
|
|
1604
|
+
|
|
1605
|
+
@server.feature(TEXT_DOCUMENT_DID_OPEN)
|
|
1606
|
+
def did_open(ls: LanguageServer, params) -> None:
|
|
1607
|
+
state.update_document(params.text_document.uri, params.text_document.text)
|
|
1608
|
+
_publish_diagnostics(ls, params.text_document.uri)
|
|
1609
|
+
|
|
1610
|
+
@server.feature(TEXT_DOCUMENT_DID_CHANGE)
|
|
1611
|
+
def did_change(ls: LanguageServer, params) -> None:
|
|
1612
|
+
if not params.content_changes:
|
|
1613
|
+
return
|
|
1614
|
+
text = params.content_changes[-1].text
|
|
1615
|
+
state.update_document(params.text_document.uri, text)
|
|
1616
|
+
_publish_diagnostics(ls, params.text_document.uri)
|
|
1617
|
+
|
|
1618
|
+
@server.feature(TEXT_DOCUMENT_DID_CLOSE)
|
|
1619
|
+
def did_close(ls: LanguageServer, params) -> None:
|
|
1620
|
+
state.remove_document(params.text_document.uri)
|
|
1621
|
+
ls.publish_diagnostics(params.text_document.uri, [])
|
|
1622
|
+
|
|
1623
|
+
@server.feature(TEXT_DOCUMENT_DEFINITION)
|
|
1624
|
+
def definition(ls: LanguageServer, params: DefinitionParams):
|
|
1625
|
+
symbol = _symbol_at_position(params.text_document.uri, params.position)
|
|
1626
|
+
if symbol is None:
|
|
1627
|
+
return None
|
|
1628
|
+
file_name = symbol.decl_range.file_name
|
|
1629
|
+
uri = state.uri_for_file_name(file_name) or path_to_uri(file_name)
|
|
1630
|
+
start_line, start_col, end_line, end_col = source_range_to_lsp(symbol.decl_range)
|
|
1631
|
+
return Location(
|
|
1632
|
+
uri=uri,
|
|
1633
|
+
range=Range(
|
|
1634
|
+
start=Position(line=start_line, character=start_col),
|
|
1635
|
+
end=Position(line=end_line, character=end_col),
|
|
1636
|
+
),
|
|
1637
|
+
)
|
|
1638
|
+
|
|
1639
|
+
@server.feature(TEXT_DOCUMENT_HOVER)
|
|
1640
|
+
def hover(ls: LanguageServer, params: HoverParams) -> Optional[Hover]:
|
|
1641
|
+
symbol = _symbol_at_position(params.text_document.uri, params.position)
|
|
1642
|
+
if symbol is None:
|
|
1643
|
+
return None
|
|
1644
|
+
return Hover(contents=hover_text(symbol))
|
|
1645
|
+
|
|
1646
|
+
@server.feature(TEXT_DOCUMENT_REFERENCES)
|
|
1647
|
+
def references(ls: LanguageServer, params: ReferenceParams):
|
|
1648
|
+
symbol = _symbol_at_position(params.text_document.uri, params.position)
|
|
1649
|
+
if symbol is None:
|
|
1650
|
+
return []
|
|
1651
|
+
locations = []
|
|
1652
|
+
if state.workspace is not None:
|
|
1653
|
+
for ref in references_for_symbol(state.workspace, symbol):
|
|
1654
|
+
file_name = ref.ref_range.file_name
|
|
1655
|
+
uri = state.uri_for_file_name(file_name) or path_to_uri(file_name)
|
|
1656
|
+
start_line, start_col, end_line, end_col = source_range_to_lsp(ref.ref_range)
|
|
1657
|
+
locations.append(
|
|
1658
|
+
Location(
|
|
1659
|
+
uri=uri,
|
|
1660
|
+
range=Range(
|
|
1661
|
+
start=Position(line=start_line, character=start_col),
|
|
1662
|
+
end=Position(line=end_line, character=end_col),
|
|
1663
|
+
),
|
|
1664
|
+
)
|
|
1665
|
+
)
|
|
1666
|
+
if locations:
|
|
1667
|
+
return locations
|
|
1668
|
+
text = state.text_for_uri(params.text_document.uri)
|
|
1669
|
+
if text is None:
|
|
1670
|
+
return []
|
|
1671
|
+
include_declaration = getattr(params.context, 'include_declaration', True)
|
|
1672
|
+
for ref_range in text_references_for_symbol(
|
|
1673
|
+
text,
|
|
1674
|
+
symbol,
|
|
1675
|
+
include_declaration=include_declaration,
|
|
1676
|
+
):
|
|
1677
|
+
uri = state.uri_for_file_name(ref_range.file_name) or path_to_uri(ref_range.file_name)
|
|
1678
|
+
start_line, start_col, end_line, end_col = source_range_to_lsp(ref_range)
|
|
1679
|
+
locations.append(
|
|
1680
|
+
Location(
|
|
1681
|
+
uri=uri,
|
|
1682
|
+
range=Range(
|
|
1683
|
+
start=Position(line=start_line, character=start_col),
|
|
1684
|
+
end=Position(line=end_line, character=end_col),
|
|
1685
|
+
),
|
|
1686
|
+
)
|
|
1687
|
+
)
|
|
1688
|
+
return locations
|
|
1689
|
+
|
|
1690
|
+
@server.feature(TEXT_DOCUMENT_RENAME)
|
|
1691
|
+
def rename(ls: LanguageServer, params: RenameParams) -> Optional[WorkspaceEdit]:
|
|
1692
|
+
symbol = _symbol_at_position(params.text_document.uri, params.position)
|
|
1693
|
+
if symbol is None:
|
|
1694
|
+
return None
|
|
1695
|
+
|
|
1696
|
+
text = state.text_for_uri(params.text_document.uri)
|
|
1697
|
+
semantic_symbol: Symbol | None = None
|
|
1698
|
+
semantic_model = state.semantic_for_uri(params.text_document.uri)
|
|
1699
|
+
if semantic_model is not None:
|
|
1700
|
+
semantic_symbol = _symbol_at_position_in_model(
|
|
1701
|
+
semantic_model,
|
|
1702
|
+
params.text_document.uri,
|
|
1703
|
+
params.position,
|
|
1704
|
+
text,
|
|
1705
|
+
)
|
|
1706
|
+
|
|
1707
|
+
edits: dict[str, list[TextEdit]] = {}
|
|
1708
|
+
seen: set[tuple[str, int, int, int, int]] = set()
|
|
1709
|
+
workspace_symbol = semantic_symbol or symbol
|
|
1710
|
+
if state.workspace is not None and semantic_symbol is not None:
|
|
1711
|
+
for ref in references_for_symbol(state.workspace, semantic_symbol):
|
|
1712
|
+
if '.' in ref.name:
|
|
1713
|
+
continue
|
|
1714
|
+
_add_rename_edit(edits, seen, ref.ref_range, params.new_name)
|
|
1715
|
+
|
|
1716
|
+
if not edits and text is not None:
|
|
1717
|
+
for ref_range in text_references_for_symbol(
|
|
1718
|
+
text,
|
|
1719
|
+
symbol,
|
|
1720
|
+
include_declaration=True,
|
|
1721
|
+
):
|
|
1722
|
+
_add_rename_edit(edits, seen, ref_range, params.new_name)
|
|
1723
|
+
|
|
1724
|
+
_add_rename_edit(
|
|
1725
|
+
edits,
|
|
1726
|
+
seen,
|
|
1727
|
+
workspace_symbol.name_range,
|
|
1728
|
+
params.new_name,
|
|
1729
|
+
)
|
|
1730
|
+
return WorkspaceEdit(changes=edits)
|
|
1731
|
+
|
|
1732
|
+
@server.feature(TEXT_DOCUMENT_DOCUMENT_SYMBOL)
|
|
1733
|
+
def document_symbols(ls: LanguageServer, params: DocumentSymbolParams):
|
|
1734
|
+
model = state.structure_semantic_for_uri(params.text_document.uri)
|
|
1735
|
+
if model is None:
|
|
1736
|
+
return []
|
|
1737
|
+
return _document_symbols_for_scope(model.unit_scope)
|
|
1738
|
+
|
|
1739
|
+
@server.feature(WORKSPACE_SYMBOL)
|
|
1740
|
+
def workspace_symbols(ls: LanguageServer, params: WorkspaceSymbolParams):
|
|
1741
|
+
query = (params.query or '').strip().casefold()
|
|
1742
|
+
workspace = state.workspace_symbols_for_query(query)
|
|
1743
|
+
if workspace is None:
|
|
1744
|
+
return []
|
|
1745
|
+
items: list[SymbolInformation] = []
|
|
1746
|
+
for model in workspace.models.values():
|
|
1747
|
+
for symbol in iter_symbols(model.unit_scope):
|
|
1748
|
+
if symbol.kind == SymbolKind.UNIT:
|
|
1749
|
+
continue
|
|
1750
|
+
if query and query not in symbol.name.casefold():
|
|
1751
|
+
continue
|
|
1752
|
+
file_name = symbol.decl_range.file_name
|
|
1753
|
+
uri = state.uri_for_file_name(file_name) or path_to_uri(file_name)
|
|
1754
|
+
start_line, start_col, end_line, end_col = source_range_to_lsp(symbol.decl_range)
|
|
1755
|
+
items.append(
|
|
1756
|
+
SymbolInformation(
|
|
1757
|
+
name=symbol.name,
|
|
1758
|
+
kind=_symbol_kind(symbol),
|
|
1759
|
+
location=Location(
|
|
1760
|
+
uri=uri,
|
|
1761
|
+
range=Range(
|
|
1762
|
+
start=Position(line=start_line, character=start_col),
|
|
1763
|
+
end=Position(line=end_line, character=end_col),
|
|
1764
|
+
),
|
|
1765
|
+
),
|
|
1766
|
+
)
|
|
1767
|
+
)
|
|
1768
|
+
return items
|
|
1769
|
+
|
|
1770
|
+
@server.feature(TEXT_DOCUMENT_COMPLETION)
|
|
1771
|
+
def completion(ls: LanguageServer, params: CompletionParams):
|
|
1772
|
+
model = state.structure_semantic_for_uri(params.text_document.uri)
|
|
1773
|
+
if model is None:
|
|
1774
|
+
return CompletionList(is_incomplete=True, items=[])
|
|
1775
|
+
doc = state.documents.get(params.text_document.uri)
|
|
1776
|
+
base_expr = None
|
|
1777
|
+
if doc is not None:
|
|
1778
|
+
base_expr = extract_completion_base(doc.text, params.position.line, params.position.character)
|
|
1779
|
+
active_scope = scope_at_line(model.unit_scope, params.position.line + 1) or model.unit_scope
|
|
1780
|
+
symbols: list[Symbol]
|
|
1781
|
+
if base_expr:
|
|
1782
|
+
base_symbol = resolve_reference(model, base_expr, active_scope)
|
|
1783
|
+
if base_symbol is None:
|
|
1784
|
+
base_symbol = resolve_base_for_member_completion(model, base_expr, active_scope)
|
|
1785
|
+
if base_symbol is not None:
|
|
1786
|
+
symbols = list(iter_member_symbols(model, base_symbol))
|
|
1787
|
+
else:
|
|
1788
|
+
symbols = completion_items_for_scope(active_scope)
|
|
1789
|
+
if not symbols:
|
|
1790
|
+
full_model = state.full_semantic_for_uri(params.text_document.uri)
|
|
1791
|
+
if full_model is not None and full_model is not model:
|
|
1792
|
+
full_scope = scope_at_line(full_model.unit_scope, params.position.line + 1) or full_model.unit_scope
|
|
1793
|
+
full_base_symbol = resolve_reference(full_model, base_expr, full_scope)
|
|
1794
|
+
if full_base_symbol is None:
|
|
1795
|
+
full_base_symbol = resolve_base_for_member_completion(full_model, base_expr, full_scope)
|
|
1796
|
+
if full_base_symbol is not None:
|
|
1797
|
+
symbols = list(iter_member_symbols(full_model, full_base_symbol))
|
|
1798
|
+
else:
|
|
1799
|
+
full_model = state.full_semantic_for_uri(params.text_document.uri)
|
|
1800
|
+
if full_model is not None:
|
|
1801
|
+
full_scope = scope_at_line(full_model.unit_scope, params.position.line + 1) or full_model.unit_scope
|
|
1802
|
+
symbols = completion_items_for_scope(full_scope)
|
|
1803
|
+
else:
|
|
1804
|
+
symbols = completion_items_for_scope(active_scope)
|
|
1805
|
+
items = [CompletionItem(label=symbol.name, kind=_completion_kind(symbol)) for symbol in symbols]
|
|
1806
|
+
return CompletionList(is_incomplete=False, items=items)
|
|
1807
|
+
|
|
1808
|
+
return server
|
|
1809
|
+
|
|
1810
|
+
|
|
1811
|
+
def main() -> None:
|
|
1812
|
+
server = create_server()
|
|
1813
|
+
server.start_io()
|
|
1814
|
+
|
|
1815
|
+
|
|
1816
|
+
__all__ = [
|
|
1817
|
+
'create_server',
|
|
1818
|
+
'LspWorkspaceState',
|
|
1819
|
+
'WorkspaceConfig',
|
|
1820
|
+
'DocumentSnapshot',
|
|
1821
|
+
'find_reference_at_position',
|
|
1822
|
+
'find_symbol_at_position',
|
|
1823
|
+
'resolve_reference',
|
|
1824
|
+
'extract_completion_base',
|
|
1825
|
+
'outline_source',
|
|
1826
|
+
'multiline_string_block_end',
|
|
1827
|
+
'main',
|
|
1828
|
+
]
|
|
1829
|
+
|
|
1830
|
+
|
|
1831
|
+
if __name__ == '__main__':
|
|
1832
|
+
main()
|