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.
- pseudocode_i18n_languageserver/__init__.py +3 -0
- pseudocode_i18n_languageserver/cli.py +48 -0
- pseudocode_i18n_languageserver/compat.py +22 -0
- pseudocode_i18n_languageserver/model.py +229 -0
- pseudocode_i18n_languageserver/server.py +346 -0
- pseudocode_i18n_languageserver-0.9.1.dist-info/METADATA +147 -0
- pseudocode_i18n_languageserver-0.9.1.dist-info/RECORD +10 -0
- pseudocode_i18n_languageserver-0.9.1.dist-info/WHEEL +4 -0
- pseudocode_i18n_languageserver-0.9.1.dist-info/entry_points.txt +2 -0
- pseudocode_i18n_languageserver-0.9.1.dist-info/licenses/LICENSE +674 -0
|
@@ -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
|
+
]
|