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.
@@ -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()