pseudocode-i18n-languageserver 0.9.1__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,3 @@
1
+ """Language Server Protocol implementation for pseudocode-i18n."""
2
+
3
+ __version__ = "0.9.1"
@@ -0,0 +1,48 @@
1
+ """Command-line entry point for ``pseudo-lsp``."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import argparse
6
+ import logging
7
+ from pathlib import Path
8
+
9
+ from . import __version__
10
+ from .server import server
11
+
12
+
13
+ def _parser() -> argparse.ArgumentParser:
14
+ parser = argparse.ArgumentParser(
15
+ prog="pseudo-lsp",
16
+ description="Language Server Protocol server for pseudocode-i18n.",
17
+ )
18
+ transport = parser.add_mutually_exclusive_group()
19
+ transport.add_argument("--stdio", action="store_true", help="Use stdin/stdout (default).")
20
+ transport.add_argument(
21
+ "--tcp",
22
+ metavar="HOST:PORT",
23
+ help="Listen on TCP instead of stdin/stdout; useful for debugging and some IDEs.",
24
+ )
25
+ parser.add_argument("--log-file", type=Path, help="Write server logs to this file.")
26
+ parser.add_argument("--version", action="version", version=f"%(prog)s {__version__}")
27
+ return parser
28
+
29
+
30
+ def main() -> None:
31
+ args = _parser().parse_args()
32
+ if args.log_file:
33
+ logging.basicConfig(
34
+ filename=args.log_file,
35
+ level=logging.INFO,
36
+ format="%(asctime)s %(levelname)s %(name)s: %(message)s",
37
+ )
38
+ if args.tcp:
39
+ host, separator, port_text = args.tcp.rpartition(":")
40
+ if not separator or not host or not port_text.isdigit():
41
+ _parser().error("--tcp must use HOST:PORT, for example 127.0.0.1:2087")
42
+ server.start_tcp(host, int(port_text))
43
+ return
44
+ server.start_io()
45
+
46
+
47
+ if __name__ == "__main__":
48
+ main()
@@ -0,0 +1,22 @@
1
+ """Compatibility helpers for third-party runtime dependencies."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import asyncio
6
+ import inspect
7
+ import sys
8
+
9
+
10
+ def prepare_pygls_runtime() -> None:
11
+ """Keep pygls 2.1.x compatible with Python 3.14+ coroutine introspection.
12
+
13
+ Python 3.14 deprecates ``asyncio.iscoroutinefunction`` and Python 3.16
14
+ removes it. pygls 2.1.x still calls that alias while registering feature
15
+ handlers. Redirecting the alias to its stdlib replacement avoids the
16
+ deprecation today and preserves the expected API for pygls on Python 3.16.
17
+
18
+ Remove this shim once the minimum supported pygls release no longer uses
19
+ ``asyncio.iscoroutinefunction``.
20
+ """
21
+ if sys.version_info >= (3, 14):
22
+ asyncio.iscoroutinefunction = inspect.iscoroutinefunction
@@ -0,0 +1,229 @@
1
+ """Editor-independent document model used by the LSP transport layer."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import re
6
+ from dataclasses import dataclass
7
+ from pathlib import Path
8
+
9
+ from pseudocode_i18n import (
10
+ SemanticIndex,
11
+ build_semantic_index,
12
+ help_at_position,
13
+ help_for_token,
14
+ lint,
15
+ snippets_for_language,
16
+ )
17
+ from pseudocode_i18n.config import (
18
+ fallback_language,
19
+ find_config,
20
+ language_overrides,
21
+ load_config,
22
+ project_language,
23
+ )
24
+ from pseudocode_i18n.diagnostics import LintResult
25
+ from pseudocode_i18n.formatter import format_pseudocode
26
+ from pseudocode_i18n.language import load_language, resolve_language
27
+
28
+ _CALL_RE = re.compile(r"(?u)(?P<name>(?:[^\W\d]|_)\w*)\s*\((?P<args>[^()]*)$")
29
+ _SNIPPET_DEFAULT_RE = re.compile(r"\$\{\d+:([^}]*)\}")
30
+ _SNIPPET_EMPTY_RE = re.compile(r"\$\{\d+\}|\$\d+")
31
+
32
+
33
+ def snippet_to_plain_text(text: str) -> str:
34
+ """Return a readable insertion for LSP clients without snippet support."""
35
+ text = _SNIPPET_DEFAULT_RE.sub(lambda match: match.group(1), text)
36
+ return _SNIPPET_EMPTY_RE.sub("", text)
37
+
38
+
39
+ @dataclass(frozen=True, slots=True)
40
+ class CompletionCandidate:
41
+ label: str
42
+ kind: str
43
+ insert_text: str | None = None
44
+ detail: str | None = None
45
+ documentation: str | None = None
46
+ snippet: bool = False
47
+
48
+
49
+ @dataclass(frozen=True, slots=True)
50
+ class SignatureContext:
51
+ name: str
52
+ signature: str
53
+ parameters: tuple[str, ...]
54
+ active_parameter: int
55
+
56
+
57
+ @dataclass(slots=True)
58
+ class DocumentModel:
59
+ source: str
60
+ path: Path | None
61
+ language: str
62
+ config: dict
63
+ index: SemanticIndex
64
+ lint_result: LintResult
65
+
66
+ @classmethod
67
+ def build(cls, source: str, path: Path | None = None) -> DocumentModel:
68
+ config_path = find_config(path.parent if path is not None else None)
69
+ config = load_config(config_path)
70
+ configured = project_language(config)
71
+ fallback = fallback_language(config)
72
+ overrides = config.get("languages") if isinstance(config.get("languages"), dict) else {}
73
+ language = resolve_language(
74
+ source,
75
+ configured=configured,
76
+ fallback=fallback,
77
+ overrides_by_language=overrides,
78
+ )
79
+ override = language_overrides(config, language)
80
+ lint_result = lint(
81
+ source,
82
+ language,
83
+ override,
84
+ fallback_language=fallback,
85
+ source_path=path,
86
+ project_root=config_path.parent if config_path is not None else None,
87
+ )
88
+ try:
89
+ index = build_semantic_index(
90
+ source,
91
+ language=language,
92
+ configured=configured,
93
+ fallback=fallback,
94
+ overrides_by_language=overrides,
95
+ )
96
+ except (SyntaxError, ValueError):
97
+ index = SemanticIndex(language=language, symbols=(), references=())
98
+ return cls(source, path, language, config, index, lint_result)
99
+
100
+ def formatted(self) -> str:
101
+ override = language_overrides(self.config, self.language)
102
+ return format_pseudocode(self.source, self.language, override, self.config)
103
+
104
+ def completions(self) -> tuple[CompletionCandidate, ...]:
105
+ override = language_overrides(self.config, self.language)
106
+ spec = load_language(self.language, override)
107
+ items: list[CompletionCandidate] = []
108
+ for snippet in snippets_for_language(self.language, override):
109
+ items.append(
110
+ CompletionCandidate(
111
+ label=snippet.label,
112
+ kind="snippet",
113
+ insert_text=snippet.insert_text,
114
+ detail=f"Pseudocode ({self.language}) · {snippet.key}",
115
+ documentation=(
116
+ help_for_token(snippet.label.split()[0], self.language, override).markdown
117
+ if help_for_token(snippet.label.split()[0], self.language, override)
118
+ else None
119
+ ),
120
+ snippet=True,
121
+ )
122
+ )
123
+ seen = {item.label for item in items}
124
+ for symbol in self.index.symbols:
125
+ if symbol.name in seen:
126
+ continue
127
+ seen.add(symbol.name)
128
+ items.append(
129
+ CompletionCandidate(
130
+ label=symbol.name,
131
+ kind=symbol.kind,
132
+ detail=symbol.signature,
133
+ documentation=(
134
+ f"**{symbol.kind}** `{symbol.signature}`"
135
+ + (f"\n\nContainer: `{symbol.container}`" if symbol.container else "")
136
+ ),
137
+ )
138
+ )
139
+ for name, semantic in spec.semantic_functions.items():
140
+ if name in seen:
141
+ continue
142
+ seen.add(name)
143
+ items.append(
144
+ CompletionCandidate(
145
+ label=name,
146
+ kind="function",
147
+ detail=f"built-in · {semantic}",
148
+ documentation=(
149
+ help_for_token(name, self.language, override).markdown
150
+ if help_for_token(name, self.language, override)
151
+ else None
152
+ ),
153
+ )
154
+ )
155
+ for key in spec.types:
156
+ name = spec.canonical_type(key)
157
+ if name in seen:
158
+ continue
159
+ seen.add(name)
160
+ help_item = help_for_token(name, self.language, override)
161
+ items.append(
162
+ CompletionCandidate(
163
+ label=name,
164
+ kind="type",
165
+ detail=f"type · {key}",
166
+ documentation=help_item.markdown if help_item else None,
167
+ )
168
+ )
169
+ for name in spec.values:
170
+ if name in seen:
171
+ continue
172
+ seen.add(name)
173
+ help_item = help_for_token(name, self.language, override)
174
+ items.append(
175
+ CompletionCandidate(
176
+ label=name,
177
+ kind="constant",
178
+ detail="constant",
179
+ documentation=help_item.markdown if help_item else None,
180
+ )
181
+ )
182
+ return tuple(items)
183
+
184
+ def help_at(self, line: int, column: int):
185
+ override = language_overrides(self.config, self.language)
186
+ return help_at_position(self.source, self.language, line, column, override)
187
+
188
+ def signature_at(self, line: int, column: int) -> SignatureContext | None:
189
+ lines = self.source.splitlines()
190
+ if line < 1 or line > len(lines):
191
+ return None
192
+ prefix = lines[line - 1][: max(0, column - 1)]
193
+ match = _CALL_RE.search(prefix)
194
+ if match is None:
195
+ return None
196
+ name = match.group("name")
197
+ candidates = [
198
+ item
199
+ for item in self.index.symbols
200
+ if item.name == name
201
+ and item.kind in {"function", "method", "constructor", "process", "subprocess"}
202
+ ]
203
+ active_parameter = 0 if not match.group("args").strip() else match.group("args").count(",")
204
+ if candidates:
205
+ symbol = candidates[0]
206
+ parameters = tuple(item.display for item in symbol.parameters)
207
+ return SignatureContext(
208
+ name=name,
209
+ signature=symbol.signature,
210
+ parameters=parameters,
211
+ active_parameter=min(active_parameter, max(0, len(parameters) - 1)),
212
+ )
213
+
214
+ override = language_overrides(self.config, self.language)
215
+ help_item = help_for_token(name, self.language, override)
216
+ if help_item is None or help_item.kind != "builtin" or not help_item.syntax:
217
+ return None
218
+ signature = help_item.syntax[0]
219
+ match_params = re.search(r"\((.*)\)$", signature)
220
+ if match_params is None or "…" in match_params.group(1):
221
+ return None
222
+ raw = match_params.group(1).strip()
223
+ parameters = tuple(item.strip() for item in raw.split(",") if item.strip())
224
+ return SignatureContext(
225
+ name=name,
226
+ signature=signature,
227
+ parameters=parameters,
228
+ active_parameter=min(active_parameter, max(0, len(parameters) - 1)),
229
+ )
@@ -0,0 +1,346 @@
1
+ """pygls transport layer for the Pseudocode language server."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from pathlib import Path
6
+
7
+ from lsprotocol import types
8
+ from pseudocode_i18n import SourceRange
9
+ from pseudocode_i18n.diagnostics import DiagnosticSeverity as CoreSeverity
10
+ from pygls.lsp.server import LanguageServer
11
+ from pygls.uris import to_fs_path
12
+
13
+ from .compat import prepare_pygls_runtime
14
+ from .model import DocumentModel, snippet_to_plain_text
15
+
16
+ prepare_pygls_runtime()
17
+
18
+ SERVER_NAME = "pseudocode-i18n-languageserver"
19
+ SERVER_VERSION = "0.9.1"
20
+
21
+
22
+ class PseudocodeLanguageServer(LanguageServer):
23
+ def __init__(self) -> None:
24
+ super().__init__(SERVER_NAME, SERVER_VERSION)
25
+ self.models: dict[str, DocumentModel] = {}
26
+
27
+ def model(self, uri: str, *, refresh: bool = False) -> DocumentModel:
28
+ if not refresh and uri in self.models:
29
+ return self.models[uri]
30
+ document = self.workspace.get_text_document(uri)
31
+ model = DocumentModel.build(document.source, _path_from_uri(uri))
32
+ self.models[uri] = model
33
+ return model
34
+
35
+ def refresh_and_publish(self, uri: str, version: int | None = None) -> None:
36
+ document = self.workspace.get_text_document(uri)
37
+ model = self.model(uri, refresh=True)
38
+ diagnostics = [_diagnostic(item, document) for item in model.lint_result.diagnostics]
39
+ self.text_document_publish_diagnostics(
40
+ types.PublishDiagnosticsParams(uri=uri, version=version, diagnostics=diagnostics)
41
+ )
42
+
43
+
44
+ server = PseudocodeLanguageServer()
45
+
46
+
47
+ def _path_from_uri(uri: str) -> Path | None:
48
+ value = to_fs_path(uri)
49
+ return Path(value) if value is not None else None
50
+
51
+
52
+ def _source_offset(source: str, line: int, column: int) -> int:
53
+ """Convert a one-based core position into a Python code-point offset."""
54
+ lines = source.splitlines(keepends=True)
55
+ if not lines:
56
+ return 0
57
+ line_index = min(max(line - 1, 0), len(lines) - 1)
58
+ prefix = sum(len(item) for item in lines[:line_index])
59
+ text = lines[line_index]
60
+ visible_length = len(text.rstrip("\r\n"))
61
+ column_index = min(max(column - 1, 0), visible_length)
62
+ return prefix + column_index
63
+
64
+
65
+ def _core_position(document, position: types.Position) -> tuple[int, int]:
66
+ """Convert an LSP client position into one-based Python code-point units."""
67
+ offset = document.offset_at_position(position)
68
+ server_position = document.server_position_at_offset(offset)
69
+ return server_position.line + 1, server_position.character + 1
70
+
71
+
72
+ def _range(document, value: SourceRange) -> types.Range:
73
+ """Convert a core range into the position encoding negotiated with the client."""
74
+ return types.Range(
75
+ start=document.client_position_at_offset(
76
+ _source_offset(document.source, value.start_line, value.start_column)
77
+ ),
78
+ end=document.client_position_at_offset(
79
+ _source_offset(document.source, value.end_line, value.end_column)
80
+ ),
81
+ )
82
+
83
+
84
+ def _diagnostic(item, document) -> types.Diagnostic:
85
+ severity = {
86
+ CoreSeverity.ERROR: types.DiagnosticSeverity.Error,
87
+ CoreSeverity.WARNING: types.DiagnosticSeverity.Warning,
88
+ CoreSeverity.INFORMATION: types.DiagnosticSeverity.Information,
89
+ }[item.severity]
90
+ range_ = SourceRange(
91
+ item.line,
92
+ item.column,
93
+ item.end_line or item.line,
94
+ item.end_column or max(item.column + 1, item.column),
95
+ )
96
+ return types.Diagnostic(
97
+ range=_range(document, range_),
98
+ message=item.message,
99
+ severity=severity,
100
+ code=item.code,
101
+ source="pseudocode-i18n",
102
+ )
103
+
104
+
105
+ def _symbol_kind(kind: str) -> types.SymbolKind:
106
+ return {
107
+ "class": types.SymbolKind.Class,
108
+ "function": types.SymbolKind.Function,
109
+ "method": types.SymbolKind.Method,
110
+ "constructor": types.SymbolKind.Constructor,
111
+ "process": types.SymbolKind.Function,
112
+ "subprocess": types.SymbolKind.Function,
113
+ "algorithm": types.SymbolKind.Namespace,
114
+ "parameter": types.SymbolKind.Variable,
115
+ "attribute": types.SymbolKind.Field,
116
+ "module": types.SymbolKind.Module,
117
+ "import": types.SymbolKind.Module,
118
+ "variable": types.SymbolKind.Variable,
119
+ }.get(kind, types.SymbolKind.Variable)
120
+
121
+
122
+ def _supports_snippets(ls: PseudocodeLanguageServer) -> bool:
123
+ text_document = getattr(ls.client_capabilities, "text_document", None)
124
+ completion = getattr(text_document, "completion", None)
125
+ completion_item = getattr(completion, "completion_item", None)
126
+ return bool(getattr(completion_item, "snippet_support", False))
127
+
128
+
129
+ def _completion_kind(kind: str) -> types.CompletionItemKind:
130
+ return {
131
+ "snippet": types.CompletionItemKind.Snippet,
132
+ "class": types.CompletionItemKind.Class,
133
+ "function": types.CompletionItemKind.Function,
134
+ "method": types.CompletionItemKind.Method,
135
+ "constructor": types.CompletionItemKind.Constructor,
136
+ "process": types.CompletionItemKind.Function,
137
+ "subprocess": types.CompletionItemKind.Function,
138
+ "parameter": types.CompletionItemKind.Variable,
139
+ "attribute": types.CompletionItemKind.Field,
140
+ "variable": types.CompletionItemKind.Variable,
141
+ "type": types.CompletionItemKind.TypeParameter,
142
+ "constant": types.CompletionItemKind.Constant,
143
+ }.get(kind, types.CompletionItemKind.Text)
144
+
145
+
146
+ @server.feature(types.TEXT_DOCUMENT_DID_OPEN)
147
+ def did_open(ls: PseudocodeLanguageServer, params: types.DidOpenTextDocumentParams) -> None:
148
+ ls.refresh_and_publish(params.text_document.uri, params.text_document.version)
149
+
150
+
151
+ @server.feature(types.TEXT_DOCUMENT_DID_CHANGE)
152
+ def did_change(ls: PseudocodeLanguageServer, params: types.DidChangeTextDocumentParams) -> None:
153
+ ls.refresh_and_publish(params.text_document.uri, params.text_document.version)
154
+
155
+
156
+ @server.feature(types.TEXT_DOCUMENT_DID_SAVE)
157
+ def did_save(ls: PseudocodeLanguageServer, params: types.DidSaveTextDocumentParams) -> None:
158
+ ls.refresh_and_publish(params.text_document.uri)
159
+
160
+
161
+ @server.feature(types.TEXT_DOCUMENT_DID_CLOSE)
162
+ def did_close(ls: PseudocodeLanguageServer, params: types.DidCloseTextDocumentParams) -> None:
163
+ uri = params.text_document.uri
164
+ ls.models.pop(uri, None)
165
+ ls.text_document_publish_diagnostics(types.PublishDiagnosticsParams(uri=uri, diagnostics=[]))
166
+
167
+
168
+ @server.feature(types.TEXT_DOCUMENT_COMPLETION, types.CompletionOptions(resolve_provider=False))
169
+ def completion(
170
+ ls: PseudocodeLanguageServer, params: types.CompletionParams
171
+ ) -> types.CompletionList:
172
+ model = ls.model(params.text_document.uri)
173
+ snippet_support = _supports_snippets(ls)
174
+ items = [
175
+ types.CompletionItem(
176
+ label=item.label,
177
+ kind=_completion_kind(item.kind),
178
+ detail=item.detail,
179
+ documentation=(
180
+ types.MarkupContent(kind=types.MarkupKind.Markdown, value=item.documentation)
181
+ if item.documentation
182
+ else None
183
+ ),
184
+ insert_text=(
185
+ item.insert_text
186
+ if not item.snippet or snippet_support or item.insert_text is None
187
+ else snippet_to_plain_text(item.insert_text)
188
+ ),
189
+ insert_text_format=(
190
+ types.InsertTextFormat.Snippet
191
+ if item.snippet and snippet_support
192
+ else types.InsertTextFormat.PlainText
193
+ ),
194
+ sort_text=("0-" if item.snippet else "1-") + item.label.casefold(),
195
+ )
196
+ for item in model.completions()
197
+ ]
198
+ return types.CompletionList(is_incomplete=False, items=items)
199
+
200
+
201
+ @server.feature(types.TEXT_DOCUMENT_HOVER)
202
+ def hover(ls: PseudocodeLanguageServer, params: types.HoverParams) -> types.Hover | None:
203
+ document = ls.workspace.get_text_document(params.text_document.uri)
204
+ model = ls.model(params.text_document.uri)
205
+ line, column = _core_position(document, params.position)
206
+ symbol = model.index.symbol_at(line, column)
207
+ if symbol is not None:
208
+ details = [f"**{symbol.kind}** `{symbol.signature}`", "", f"Language: `{model.language}`"]
209
+ if symbol.container:
210
+ details.extend(["", f"Container: `{symbol.container}`"])
211
+ return types.Hover(
212
+ contents=types.MarkupContent(kind=types.MarkupKind.Markdown, value="\n".join(details)),
213
+ range=_range(document, symbol.range),
214
+ )
215
+
216
+ help_item = model.help_at(line, column)
217
+ if help_item is None:
218
+ return None
219
+ return types.Hover(
220
+ contents=types.MarkupContent(kind=types.MarkupKind.Markdown, value=help_item.markdown),
221
+ )
222
+
223
+
224
+ @server.feature(
225
+ types.TEXT_DOCUMENT_SIGNATURE_HELP,
226
+ types.SignatureHelpOptions(trigger_characters=["(", ","]),
227
+ )
228
+ def signature_help(
229
+ ls: PseudocodeLanguageServer, params: types.SignatureHelpParams
230
+ ) -> types.SignatureHelp | None:
231
+ document = ls.workspace.get_text_document(params.text_document.uri)
232
+ model = ls.model(params.text_document.uri)
233
+ line, column = _core_position(document, params.position)
234
+ context = model.signature_at(line, column)
235
+ if context is None:
236
+ return None
237
+ return types.SignatureHelp(
238
+ signatures=[
239
+ types.SignatureInformation(
240
+ label=context.signature,
241
+ parameters=[types.ParameterInformation(label=item) for item in context.parameters],
242
+ )
243
+ ],
244
+ active_signature=0,
245
+ active_parameter=context.active_parameter,
246
+ )
247
+
248
+
249
+ @server.feature(types.TEXT_DOCUMENT_DEFINITION)
250
+ def definition(
251
+ ls: PseudocodeLanguageServer, params: types.DefinitionParams
252
+ ) -> types.Location | None:
253
+ document = ls.workspace.get_text_document(params.text_document.uri)
254
+ model = ls.model(params.text_document.uri)
255
+ line, column = _core_position(document, params.position)
256
+ symbol = model.index.definition_at(line, column)
257
+ if symbol is None:
258
+ return None
259
+ return types.Location(uri=params.text_document.uri, range=_range(document, symbol.range))
260
+
261
+
262
+ @server.feature(types.TEXT_DOCUMENT_REFERENCES)
263
+ def references(ls: PseudocodeLanguageServer, params: types.ReferenceParams) -> list[types.Location]:
264
+ document = ls.workspace.get_text_document(params.text_document.uri)
265
+ model = ls.model(params.text_document.uri)
266
+ line, column = _core_position(document, params.position)
267
+ refs = model.index.references_at(
268
+ line,
269
+ column,
270
+ include_definition=params.context.include_declaration,
271
+ )
272
+ return [
273
+ types.Location(uri=params.text_document.uri, range=_range(document, item.range))
274
+ for item in refs
275
+ ]
276
+
277
+
278
+ @server.feature(types.TEXT_DOCUMENT_PREPARE_RENAME)
279
+ def prepare_rename(
280
+ ls: PseudocodeLanguageServer, params: types.PrepareRenameParams
281
+ ) -> types.Range | None:
282
+ document = ls.workspace.get_text_document(params.text_document.uri)
283
+ model = ls.model(params.text_document.uri)
284
+ line, column = _core_position(document, params.position)
285
+ reference = model.index.reference_at(line, column)
286
+ if reference is not None:
287
+ return _range(document, reference.range)
288
+ symbol = model.index.symbol_at(line, column)
289
+ return _range(document, symbol.range) if symbol is not None else None
290
+
291
+
292
+ @server.feature(types.TEXT_DOCUMENT_RENAME)
293
+ def rename(ls: PseudocodeLanguageServer, params: types.RenameParams) -> types.WorkspaceEdit | None:
294
+ document = ls.workspace.get_text_document(params.text_document.uri)
295
+ model = ls.model(params.text_document.uri)
296
+ line, column = _core_position(document, params.position)
297
+ refs = model.index.references_at(
298
+ line,
299
+ column,
300
+ include_definition=True,
301
+ )
302
+ if not refs:
303
+ return None
304
+ edits = [
305
+ types.TextEdit(range=_range(document, item.range), new_text=params.new_name)
306
+ for item in refs
307
+ ]
308
+ return types.WorkspaceEdit(changes={params.text_document.uri: edits})
309
+
310
+
311
+ @server.feature(types.TEXT_DOCUMENT_DOCUMENT_SYMBOL)
312
+ def document_symbols(
313
+ ls: PseudocodeLanguageServer, params: types.DocumentSymbolParams
314
+ ) -> list[types.DocumentSymbol]:
315
+ document = ls.workspace.get_text_document(params.text_document.uri)
316
+ model = ls.model(params.text_document.uri)
317
+ return [
318
+ types.DocumentSymbol(
319
+ name=item.name,
320
+ detail=item.signature,
321
+ kind=_symbol_kind(item.kind),
322
+ range=_range(document, item.range),
323
+ selection_range=_range(document, item.range),
324
+ )
325
+ for item in model.index.symbols
326
+ ]
327
+
328
+
329
+ @server.feature(types.TEXT_DOCUMENT_FORMATTING)
330
+ def formatting(
331
+ ls: PseudocodeLanguageServer, params: types.DocumentFormattingParams
332
+ ) -> list[types.TextEdit]:
333
+ document = ls.workspace.get_text_document(params.text_document.uri)
334
+ model = ls.model(params.text_document.uri)
335
+ formatted = model.formatted()
336
+ if formatted == model.source:
337
+ return []
338
+ return [
339
+ types.TextEdit(
340
+ range=types.Range(
341
+ start=document.client_position_at_offset(0),
342
+ end=document.client_position_at_offset(len(model.source)),
343
+ ),
344
+ new_text=formatted,
345
+ )
346
+ ]