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