agent-codinglanguage-mapper 1.0.0__py3-none-any.whl

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