linked-data-python 0.0.4__py3-none-any.whl → 0.2.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.
- ldpy/__init__.py +35 -11
- ldpy/__main__.py +75 -250
- ldpy/build.py +91 -0
- ldpy/console.py +116 -0
- ldpy/debug.py +235 -0
- ldpy/formatter.py +329 -0
- ldpy/importer.py +110 -0
- ldpy/lsp/__init__.py +11 -0
- ldpy/lsp/__main__.py +4 -0
- ldpy/lsp/backend.py +118 -0
- ldpy/lsp/rpc.py +104 -0
- ldpy/lsp/server.py +353 -0
- ldpy/lsp/translate.py +140 -0
- ldpy/pygments_lexer.py +613 -0
- ldpy/runtime.py +931 -0
- ldpy/sparql.py +551 -0
- ldpy/transpiler/__init__.py +14 -0
- ldpy/transpiler/core.py +2558 -0
- ldpy/transpiler/errors.py +38 -0
- ldpy/transpiler/linemap.py +292 -0
- linked_data_python-0.2.0.dist-info/METADATA +158 -0
- linked_data_python-0.2.0.dist-info/RECORD +26 -0
- {linked_data_python-0.0.4.dist-info → linked_data_python-0.2.0.dist-info}/WHEEL +1 -1
- linked_data_python-0.2.0.dist-info/entry_points.txt +9 -0
- {linked_data_python-0.0.4.dist-info → linked_data_python-0.2.0.dist-info/licenses}/LICENSE.md +0 -0
- {linked_data_python-0.0.4.dist-info → linked_data_python-0.2.0.dist-info}/top_level.txt +0 -0
- ldpy/grun/lib.py +0 -63
- ldpy/grun/util.py +0 -11
- ldpy/ldpy.py +0 -183
- ldpy/rewriter/IndentedStringWriter.py +0 -54
- ldpy/rewriter/LDPythonRewriter.py +0 -677
- ldpy/rewriter/MultiChannelTokenStream.py +0 -127
- ldpy/rewriter/Result.py +0 -49
- ldpy/rewriter/__init__.py +0 -7
- ldpy/rewriter/antlr/LDPythonLexer.py +0 -870
- ldpy/rewriter/antlr/LDPythonParser.py +0 -9336
- ldpy/rewriter/antlr/LDPythonVisitor.py +0 -573
- ldpy/sparql/builtin.py +0 -326
- linked_data_python-0.0.4.dist-info/METADATA +0 -139
- linked_data_python-0.0.4.dist-info/RECORD +0 -20
- linked_data_python-0.0.4.dist-info/entry_points.txt +0 -3
ldpy/lsp/backend.py
ADDED
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
"""Delegated Python backend: a real Python language server (pylsp) in a
|
|
2
|
+
subprocess, to which the ldpy server forwards translated requests
|
|
3
|
+
(request forwarding, NOT a fork).
|
|
4
|
+
|
|
5
|
+
The backend only ever sees shadow documents: for every open .ldpy, a
|
|
6
|
+
didOpen/didChange of the transpiled Python under the URI <uri>.shadow.py.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
import subprocess
|
|
10
|
+
import sys
|
|
11
|
+
import threading
|
|
12
|
+
|
|
13
|
+
from ldpy.lsp.rpc import Endpoint, read_message, RpcClosed
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class PythonBackend:
|
|
17
|
+
"""Lifecycle and delegation to `python -m pylsp` (or another argv)."""
|
|
18
|
+
|
|
19
|
+
def __init__(self, argv=None, root_uri=None):
|
|
20
|
+
self.argv = argv or [sys.executable, "-m", "pylsp"]
|
|
21
|
+
self.root_uri = root_uri
|
|
22
|
+
self.proc = None
|
|
23
|
+
self.endpoint = None
|
|
24
|
+
self.diagnostics_handler = None # callable(uri_ombre, diagnostics)
|
|
25
|
+
self._reader = None
|
|
26
|
+
|
|
27
|
+
# -- cycle de vie -------------------------------------------------------
|
|
28
|
+
|
|
29
|
+
def start(self):
|
|
30
|
+
"""Start the subprocess and do the initialize handshake."""
|
|
31
|
+
self.proc = subprocess.Popen(
|
|
32
|
+
self.argv, stdin=subprocess.PIPE, stdout=subprocess.PIPE,
|
|
33
|
+
stderr=subprocess.DEVNULL)
|
|
34
|
+
self.endpoint = Endpoint(self.proc.stdout, self.proc.stdin)
|
|
35
|
+
self._reader = threading.Thread(target=self._pump, daemon=True)
|
|
36
|
+
self._reader.start()
|
|
37
|
+
self.endpoint.request("initialize", {
|
|
38
|
+
"processId": None,
|
|
39
|
+
"rootUri": self.root_uri,
|
|
40
|
+
"capabilities": {"textDocument": {
|
|
41
|
+
"synchronization": {},
|
|
42
|
+
"completion": {"completionItem": {"snippetSupport": False}},
|
|
43
|
+
"hover": {"contentFormat": ["markdown", "plaintext"]},
|
|
44
|
+
"definition": {}, "references": {}, "signatureHelp": {},
|
|
45
|
+
}},
|
|
46
|
+
}, timeout=30.0)
|
|
47
|
+
self.endpoint.notify("initialized", {})
|
|
48
|
+
return self
|
|
49
|
+
|
|
50
|
+
def _pump(self):
|
|
51
|
+
try:
|
|
52
|
+
while True:
|
|
53
|
+
msg = read_message(self.endpoint.reader)
|
|
54
|
+
if "id" in msg and ("result" in msg or "error" in msg):
|
|
55
|
+
self.endpoint.feed_response(msg)
|
|
56
|
+
elif msg.get("method") == "textDocument/publishDiagnostics":
|
|
57
|
+
if self.diagnostics_handler:
|
|
58
|
+
p = msg.get("params", {})
|
|
59
|
+
self.diagnostics_handler(p.get("uri", ""),
|
|
60
|
+
p.get("diagnostics", []))
|
|
61
|
+
elif "id" in msg:
|
|
62
|
+
# server->client request (configuration…): empty answer
|
|
63
|
+
self.endpoint.respond(msg["id"], result=None)
|
|
64
|
+
except (RpcClosed, ValueError, OSError):
|
|
65
|
+
pass
|
|
66
|
+
|
|
67
|
+
def stop(self):
|
|
68
|
+
"""Clean stop (shutdown/exit), kill as a last resort."""
|
|
69
|
+
if not self.proc:
|
|
70
|
+
return
|
|
71
|
+
try:
|
|
72
|
+
self.endpoint.request("shutdown", {}, timeout=5.0)
|
|
73
|
+
self.endpoint.notify("exit")
|
|
74
|
+
self.proc.wait(timeout=5.0)
|
|
75
|
+
except Exception:
|
|
76
|
+
self.proc.kill()
|
|
77
|
+
self.proc = None
|
|
78
|
+
|
|
79
|
+
def alive(self):
|
|
80
|
+
"""Is the backend subprocess still running?"""
|
|
81
|
+
return self.proc is not None and self.proc.poll() is None
|
|
82
|
+
|
|
83
|
+
# -- shadow documents ---------------------------------------------------
|
|
84
|
+
|
|
85
|
+
def open_shadow(self, uri, text, version=1):
|
|
86
|
+
"""didOpen of the Python shadow document, on the backend."""
|
|
87
|
+
self.endpoint.notify("textDocument/didOpen", {"textDocument": {
|
|
88
|
+
"uri": uri, "languageId": "python",
|
|
89
|
+
"version": version, "text": text}})
|
|
90
|
+
|
|
91
|
+
def change_shadow(self, uri, text, version):
|
|
92
|
+
"""didChange (full sync) of the shadow document."""
|
|
93
|
+
self.endpoint.notify("textDocument/didChange", {
|
|
94
|
+
"textDocument": {"uri": uri, "version": version},
|
|
95
|
+
"contentChanges": [{"text": text}]})
|
|
96
|
+
|
|
97
|
+
def close_shadow(self, uri):
|
|
98
|
+
"""didClose of the shadow document."""
|
|
99
|
+
self.endpoint.notify("textDocument/didClose",
|
|
100
|
+
{"textDocument": {"uri": uri}})
|
|
101
|
+
|
|
102
|
+
# -- requests -----------------------------------------------------------
|
|
103
|
+
|
|
104
|
+
def forward(self, method, params, timeout=15.0):
|
|
105
|
+
"""Forward an already translated request; returns the raw result
|
|
106
|
+
(or raises the backend error)."""
|
|
107
|
+
resp = self.endpoint.request(method, params, timeout=timeout)
|
|
108
|
+
if "error" in resp:
|
|
109
|
+
raise BackendError(resp["error"])
|
|
110
|
+
return resp.get("result")
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
class BackendError(Exception):
|
|
114
|
+
"""A JSON-RPC error returned by the delegated backend."""
|
|
115
|
+
|
|
116
|
+
def __init__(self, error):
|
|
117
|
+
super().__init__(error.get("message", "erreur backend"))
|
|
118
|
+
self.error = error
|
ldpy/lsp/rpc.py
ADDED
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
"""JSON-RPC 2.0 over streams with Content-Length headers (the LSP framing).
|
|
2
|
+
|
|
3
|
+
Used twice: on the server side (the editor's stdin/stdout) and on the client
|
|
4
|
+
side (the pipes of the delegated Python backend). No dependency.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import io
|
|
8
|
+
import json
|
|
9
|
+
import threading
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class RpcClosed(Exception):
|
|
13
|
+
"""The JSON-RPC stream is over (EOF or a truncated body)."""
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def read_message(stream):
|
|
17
|
+
"""Read one framed message; raises RpcClosed at end of stream."""
|
|
18
|
+
length = None
|
|
19
|
+
while True:
|
|
20
|
+
line = stream.readline()
|
|
21
|
+
if not line:
|
|
22
|
+
raise RpcClosed()
|
|
23
|
+
line = line.strip()
|
|
24
|
+
if not line:
|
|
25
|
+
break
|
|
26
|
+
if line.lower().startswith(b"content-length:"):
|
|
27
|
+
length = int(line.split(b":", 1)[1])
|
|
28
|
+
if length is None:
|
|
29
|
+
raise RpcClosed()
|
|
30
|
+
body = stream.read(length)
|
|
31
|
+
if len(body) < length:
|
|
32
|
+
raise RpcClosed()
|
|
33
|
+
return json.loads(body.decode("utf-8"))
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def write_message(stream, msg):
|
|
37
|
+
"""Write a message with its Content-Length header."""
|
|
38
|
+
body = json.dumps(msg, ensure_ascii=False).encode("utf-8")
|
|
39
|
+
stream.write(b"Content-Length: %d\r\n\r\n" % len(body))
|
|
40
|
+
stream.write(body)
|
|
41
|
+
stream.flush()
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
class Endpoint:
|
|
45
|
+
"""A JSON-RPC endpoint: outgoing requests paired up, writes serialised.
|
|
46
|
+
|
|
47
|
+
Pumping incoming messages is the caller's job (the server loop) or a
|
|
48
|
+
reader thread's (the backend client)."""
|
|
49
|
+
|
|
50
|
+
def __init__(self, reader, writer):
|
|
51
|
+
self.reader = reader
|
|
52
|
+
self.writer = writer
|
|
53
|
+
self._wlock = threading.Lock()
|
|
54
|
+
self._next_id = 1
|
|
55
|
+
self._pending = {} # id -> threading.Event + answer slot
|
|
56
|
+
|
|
57
|
+
def send(self, msg):
|
|
58
|
+
"""Write a raw message (writes serialised by a lock)."""
|
|
59
|
+
with self._wlock:
|
|
60
|
+
write_message(self.writer, msg)
|
|
61
|
+
|
|
62
|
+
def notify(self, method, params=None):
|
|
63
|
+
"""Send a notification (no id)."""
|
|
64
|
+
self.send({"jsonrpc": "2.0", "method": method,
|
|
65
|
+
"params": params if params is not None else {}})
|
|
66
|
+
|
|
67
|
+
def request_async(self, method, params=None):
|
|
68
|
+
"""Send a request; returns (id, event) — the answer will arrive
|
|
69
|
+
via feed_response()."""
|
|
70
|
+
with self._wlock:
|
|
71
|
+
rid = self._next_id
|
|
72
|
+
self._next_id += 1
|
|
73
|
+
slot = {"event": threading.Event(), "response": None}
|
|
74
|
+
self._pending[rid] = slot
|
|
75
|
+
write_message(self.writer, {"jsonrpc": "2.0", "id": rid,
|
|
76
|
+
"method": method,
|
|
77
|
+
"params": params if params is not None else {}})
|
|
78
|
+
return rid, slot
|
|
79
|
+
|
|
80
|
+
def request(self, method, params=None, timeout=15.0):
|
|
81
|
+
"""Synchronous request (needs a thread pumping feed_response)."""
|
|
82
|
+
rid, slot = self.request_async(method, params)
|
|
83
|
+
if not slot["event"].wait(timeout):
|
|
84
|
+
self._pending.pop(rid, None)
|
|
85
|
+
raise TimeoutError("no answer to %s" % method)
|
|
86
|
+
return slot["response"]
|
|
87
|
+
|
|
88
|
+
def feed_response(self, msg):
|
|
89
|
+
"""Call this for every incoming message carrying a response id."""
|
|
90
|
+
slot = self._pending.pop(msg.get("id"), None)
|
|
91
|
+
if slot is not None:
|
|
92
|
+
slot["response"] = msg
|
|
93
|
+
slot["event"].set()
|
|
94
|
+
return True
|
|
95
|
+
return False
|
|
96
|
+
|
|
97
|
+
def respond(self, rid, result=None, error=None):
|
|
98
|
+
"""Answer the incoming request with id ``rid``."""
|
|
99
|
+
msg = {"jsonrpc": "2.0", "id": rid}
|
|
100
|
+
if error is not None:
|
|
101
|
+
msg["error"] = error
|
|
102
|
+
else:
|
|
103
|
+
msg["result"] = result
|
|
104
|
+
self.send(msg)
|
ldpy/lsp/server.py
ADDED
|
@@ -0,0 +1,353 @@
|
|
|
1
|
+
"""Serveur LSP Linked-Data Python — mince, par request-forwarding.
|
|
2
|
+
|
|
3
|
+
Architecture (voir docs/explanation/tooling.md) :
|
|
4
|
+
|
|
5
|
+
- ZERO dependency: home-made JSON-RPC/framing (ldpy/lsp/rpc.py), no pygls.
|
|
6
|
+
- NATIVE layer: transpiler diagnostics (errors + scope warnings), hover on
|
|
7
|
+
the islands, semantic tokens for the islands (a precise complement to the
|
|
8
|
+
coloration TextMate, docs/how-to/use-vscode.md).
|
|
9
|
+
- DELEGATED layer: for every .ldpy, a Python shadow document is kept up to
|
|
10
|
+
date on a REAL Python language server (pylsp, subprocess, not forked);
|
|
11
|
+
completion, definition, references, signatureHelp — and hover outside an
|
|
12
|
+
island — are forwarded to it with positions translated through the
|
|
13
|
+
LanguageMap, answers translated back (shadow URIs in Locations included).
|
|
14
|
+
Without pylsp installed, the server works in native-only mode.
|
|
15
|
+
|
|
16
|
+
Lancement : python -m ldpy.lsp [--backend pylsp|none]
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
import sys
|
|
20
|
+
|
|
21
|
+
from ldpy.transpiler import transpile, LdpySyntaxError
|
|
22
|
+
from ldpy.lsp.rpc import Endpoint, read_message, RpcClosed
|
|
23
|
+
from ldpy.lsp import translate as tr
|
|
24
|
+
from ldpy.transpiler.linemap import snap_breakpoint_lines
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def fmt_default():
|
|
28
|
+
"""Default line length, without importing black."""
|
|
29
|
+
from ldpy.formatter import DEFAULT_LINE_LENGTH
|
|
30
|
+
return DEFAULT_LINE_LENGTH
|
|
31
|
+
|
|
32
|
+
FORWARDED = {
|
|
33
|
+
"textDocument/completion",
|
|
34
|
+
"textDocument/definition",
|
|
35
|
+
"textDocument/references",
|
|
36
|
+
"textDocument/signatureHelp",
|
|
37
|
+
"textDocument/documentHighlight",
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
class Document:
|
|
42
|
+
"""An open .ldpy document: text, version, transpilation result."""
|
|
43
|
+
|
|
44
|
+
__slots__ = ("uri", "text", "version", "result",
|
|
45
|
+
"native_diags", "py_diags")
|
|
46
|
+
|
|
47
|
+
def __init__(self, uri, text, version):
|
|
48
|
+
self.uri = uri
|
|
49
|
+
self.text = text
|
|
50
|
+
self.version = version
|
|
51
|
+
self.result = None # TranspileResult, or None on error
|
|
52
|
+
self.native_diags = [] # diagnostics du transpileur
|
|
53
|
+
self.py_diags = [] # backend diagnostics, already translated
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
class LdpyServer:
|
|
57
|
+
"""The language server: native layer plus delegation to the Python backend."""
|
|
58
|
+
|
|
59
|
+
def __init__(self, reader, writer, backend="pylsp", backend_argv=None,
|
|
60
|
+
line_length=None):
|
|
61
|
+
self.endpoint = Endpoint(reader, writer)
|
|
62
|
+
self.docs = {} # uri -> Document
|
|
63
|
+
self.backend_kind = backend
|
|
64
|
+
self.backend_argv = backend_argv
|
|
65
|
+
self.backend = None
|
|
66
|
+
self.line_length = line_length
|
|
67
|
+
self._shutdown = False
|
|
68
|
+
|
|
69
|
+
# ------------------------------------------------------------ backend
|
|
70
|
+
|
|
71
|
+
def _ensure_backend(self, root_uri=None):
|
|
72
|
+
if self.backend is not None or self.backend_kind == "none":
|
|
73
|
+
return self.backend
|
|
74
|
+
try:
|
|
75
|
+
from ldpy.lsp.backend import PythonBackend
|
|
76
|
+
self.backend = PythonBackend(argv=self.backend_argv,
|
|
77
|
+
root_uri=root_uri).start()
|
|
78
|
+
self.backend.diagnostics_handler = self._on_backend_diags
|
|
79
|
+
except Exception as e:
|
|
80
|
+
print("ldpy-lsp: backend indisponible (%s) — mode natif seul" % e,
|
|
81
|
+
file=sys.stderr)
|
|
82
|
+
self.backend_kind = "none"
|
|
83
|
+
return self.backend
|
|
84
|
+
|
|
85
|
+
# ---------------------------------------------------------- documents
|
|
86
|
+
|
|
87
|
+
def _maps_by_uri(self):
|
|
88
|
+
return {u: d.result.map for u, d in self.docs.items() if d.result}
|
|
89
|
+
|
|
90
|
+
def _sync(self, doc, opened):
|
|
91
|
+
"""Transpile, publish the diagnostics, sync the shadow."""
|
|
92
|
+
diags = []
|
|
93
|
+
try:
|
|
94
|
+
doc.result = transpile(doc.text, doc.uri)
|
|
95
|
+
except LdpySyntaxError as e:
|
|
96
|
+
doc.result = None
|
|
97
|
+
diags.append({
|
|
98
|
+
"range": {"start": {"line": e.line, "character": e.col},
|
|
99
|
+
"end": {"line": e.line, "character": e.col + 1}},
|
|
100
|
+
"severity": 1, "source": "ldpy", "message": e.msg})
|
|
101
|
+
else:
|
|
102
|
+
for w in doc.result.warnings:
|
|
103
|
+
diags.append({
|
|
104
|
+
"range": {"start": {"line": w.line, "character": w.col},
|
|
105
|
+
"end": {"line": w.line, "character": w.col + 1}},
|
|
106
|
+
"severity": 2, "source": "ldpy", "message": w.message})
|
|
107
|
+
doc.native_diags = diags
|
|
108
|
+
if doc.result is None:
|
|
109
|
+
doc.py_diags = []
|
|
110
|
+
self._publish(doc)
|
|
111
|
+
if doc.result and self.backend:
|
|
112
|
+
su = tr.shadow_uri(doc.uri)
|
|
113
|
+
if opened:
|
|
114
|
+
self.backend.open_shadow(su, doc.result.code, doc.version)
|
|
115
|
+
else:
|
|
116
|
+
self.backend.change_shadow(su, doc.result.code, doc.version)
|
|
117
|
+
|
|
118
|
+
def _publish(self, doc):
|
|
119
|
+
self.endpoint.notify(
|
|
120
|
+
"textDocument/publishDiagnostics",
|
|
121
|
+
{"uri": doc.uri,
|
|
122
|
+
"diagnostics": doc.native_diags + doc.py_diags})
|
|
123
|
+
|
|
124
|
+
def _on_backend_diags(self, shadow, diags):
|
|
125
|
+
"""The backend's Python diagnostics, re-projected onto the .ldpy.
|
|
126
|
+
Those landing on synthetic text (the prelude) are dropped."""
|
|
127
|
+
doc = self.docs.get(tr.unshadow_uri(shadow))
|
|
128
|
+
if doc is None or doc.result is None:
|
|
129
|
+
return
|
|
130
|
+
lmap = doc.result.map
|
|
131
|
+
kept = []
|
|
132
|
+
for d in diags:
|
|
133
|
+
rng = d.get("range", {})
|
|
134
|
+
start = tr.pos_to_ldpy(lmap, rng.get("start", {}))
|
|
135
|
+
if start is None:
|
|
136
|
+
continue # prelude, or off the map
|
|
137
|
+
end = tr.pos_to_ldpy(lmap, rng.get("end", {})) or start
|
|
138
|
+
d = dict(d, range={"start": start, "end": end},
|
|
139
|
+
source=d.get("source") or "python")
|
|
140
|
+
kept.append(d)
|
|
141
|
+
doc.py_diags = kept
|
|
142
|
+
self._publish(doc)
|
|
143
|
+
|
|
144
|
+
# ------------------------------------------------------------- boucle
|
|
145
|
+
|
|
146
|
+
def serve(self):
|
|
147
|
+
"""Main loop: read stdin, dispatch, answer; leave on exit."""
|
|
148
|
+
while True:
|
|
149
|
+
try:
|
|
150
|
+
msg = read_message(self.endpoint.reader)
|
|
151
|
+
except RpcClosed:
|
|
152
|
+
break
|
|
153
|
+
if "id" in msg and ("result" in msg or "error" in msg):
|
|
154
|
+
self.endpoint.feed_response(msg)
|
|
155
|
+
continue
|
|
156
|
+
method = msg.get("method", "")
|
|
157
|
+
rid = msg.get("id")
|
|
158
|
+
params = msg.get("params") or {}
|
|
159
|
+
try:
|
|
160
|
+
if method == "exit":
|
|
161
|
+
break
|
|
162
|
+
result = self._dispatch(method, params, rid)
|
|
163
|
+
if rid is not None:
|
|
164
|
+
self.endpoint.respond(rid, result=result)
|
|
165
|
+
except Exception as e: # jamais fatal
|
|
166
|
+
if rid is not None:
|
|
167
|
+
self.endpoint.respond(
|
|
168
|
+
rid, error={"code": -32603, "message": str(e)})
|
|
169
|
+
if self.backend:
|
|
170
|
+
self.backend.stop()
|
|
171
|
+
|
|
172
|
+
# ------------------------------------------------------------ dispatch
|
|
173
|
+
|
|
174
|
+
def _dispatch(self, method, params, rid):
|
|
175
|
+
if method == "initialize":
|
|
176
|
+
self._ensure_backend(params.get("rootUri"))
|
|
177
|
+
return {
|
|
178
|
+
"capabilities": {
|
|
179
|
+
"textDocumentSync": 1, # complet
|
|
180
|
+
"hoverProvider": True,
|
|
181
|
+
"completionProvider": {
|
|
182
|
+
"triggerCharacters": [".", ":", "<", "?", "/"]},
|
|
183
|
+
"definitionProvider": True,
|
|
184
|
+
"documentFormattingProvider": self._can_format(),
|
|
185
|
+
"referencesProvider": True,
|
|
186
|
+
"signatureHelpProvider": {
|
|
187
|
+
"triggerCharacters": ["(", ","]},
|
|
188
|
+
"semanticTokensProvider": {
|
|
189
|
+
"legend": {"tokenTypes": tr.TOKEN_TYPES,
|
|
190
|
+
"tokenModifiers": []},
|
|
191
|
+
"full": True},
|
|
192
|
+
# our own extension, announced so the client can detect
|
|
193
|
+
# it instead of assuming it (record vscode/103)
|
|
194
|
+
"experimental": {"ldpyBreakpointLines": True},
|
|
195
|
+
},
|
|
196
|
+
"serverInfo": {"name": "ldpy-lsp", "version": "0.2.0"},
|
|
197
|
+
}
|
|
198
|
+
if method in ("initialized", "workspace/didChangeConfiguration",
|
|
199
|
+
"$/setTrace", "$/cancelRequest"):
|
|
200
|
+
return None
|
|
201
|
+
if method == "shutdown":
|
|
202
|
+
self._shutdown = True
|
|
203
|
+
return None
|
|
204
|
+
|
|
205
|
+
if method == "textDocument/didOpen":
|
|
206
|
+
td = params["textDocument"]
|
|
207
|
+
doc = Document(td["uri"], td["text"], td.get("version", 1))
|
|
208
|
+
self.docs[doc.uri] = doc
|
|
209
|
+
self._sync(doc, opened=True)
|
|
210
|
+
return None
|
|
211
|
+
if method == "textDocument/didChange":
|
|
212
|
+
td = params["textDocument"]
|
|
213
|
+
doc = self.docs.get(td["uri"])
|
|
214
|
+
if doc is None:
|
|
215
|
+
return None
|
|
216
|
+
doc.text = params["contentChanges"][-1]["text"]
|
|
217
|
+
doc.version = td.get("version", doc.version + 1)
|
|
218
|
+
self._sync(doc, opened=False)
|
|
219
|
+
return None
|
|
220
|
+
if method == "textDocument/didClose":
|
|
221
|
+
uri = params["textDocument"]["uri"]
|
|
222
|
+
self.docs.pop(uri, None)
|
|
223
|
+
if self.backend:
|
|
224
|
+
self.backend.close_shadow(tr.shadow_uri(uri))
|
|
225
|
+
self.endpoint.notify("textDocument/publishDiagnostics",
|
|
226
|
+
{"uri": uri, "diagnostics": []})
|
|
227
|
+
return None
|
|
228
|
+
|
|
229
|
+
if method == "textDocument/formatting":
|
|
230
|
+
return self._format(params)
|
|
231
|
+
|
|
232
|
+
if method == "ldpy/breakpointLines":
|
|
233
|
+
# Snap breakpoint lines onto those that really bind (inside a
|
|
234
|
+
# multi-line island -> the island's first line). The extension
|
|
235
|
+
# then moves the dot, instead of leaving the user with a
|
|
236
|
+
# breakpoint that never fires.
|
|
237
|
+
lines = list(params.get("lines") or [])
|
|
238
|
+
doc = self.docs.get(params["textDocument"]["uri"])
|
|
239
|
+
if doc is None or doc.result is None:
|
|
240
|
+
return {"lines": lines}
|
|
241
|
+
return {"lines": snap_breakpoint_lines(doc.result.map, lines)}
|
|
242
|
+
|
|
243
|
+
if method == "textDocument/semanticTokens/full":
|
|
244
|
+
doc = self.docs.get(params["textDocument"]["uri"])
|
|
245
|
+
if doc is None or doc.result is None:
|
|
246
|
+
return {"data": []}
|
|
247
|
+
return {"data": tr.semantic_tokens(doc.result.map)}
|
|
248
|
+
|
|
249
|
+
if method == "textDocument/hover":
|
|
250
|
+
return self._hover(params)
|
|
251
|
+
|
|
252
|
+
if method in FORWARDED:
|
|
253
|
+
return self._forward(method, params)
|
|
254
|
+
|
|
255
|
+
if rid is not None:
|
|
256
|
+
return None
|
|
257
|
+
return None
|
|
258
|
+
|
|
259
|
+
# ------------------------------------------------------------- natives
|
|
260
|
+
|
|
261
|
+
def _can_format(self):
|
|
262
|
+
"""The formatter delegates Python to black (extra `[format]`): we
|
|
263
|
+
only announce the capability when it is actually available."""
|
|
264
|
+
try:
|
|
265
|
+
from ldpy.formatter import _black
|
|
266
|
+
_black()
|
|
267
|
+
return True
|
|
268
|
+
except Exception:
|
|
269
|
+
return False
|
|
270
|
+
|
|
271
|
+
def _format(self, params):
|
|
272
|
+
"""`textDocument/formatting`: one edit replacing everything.
|
|
273
|
+
|
|
274
|
+
A faulty document is not formatted — we then return NO edit rather
|
|
275
|
+
than invented text; the diagnostics already say why."""
|
|
276
|
+
doc = self.docs.get(params["textDocument"]["uri"])
|
|
277
|
+
if doc is None:
|
|
278
|
+
return None
|
|
279
|
+
from ldpy.formatter import format_source, FormatterUnavailable
|
|
280
|
+
opts = params.get("options") or {}
|
|
281
|
+
width = (self.line_length
|
|
282
|
+
or opts.get("ldpyLineLength")
|
|
283
|
+
or fmt_default())
|
|
284
|
+
try:
|
|
285
|
+
new = format_source(doc.text, doc.uri, int(width))
|
|
286
|
+
except (LdpySyntaxError, FormatterUnavailable):
|
|
287
|
+
return None
|
|
288
|
+
if new == doc.text:
|
|
289
|
+
return []
|
|
290
|
+
lines = doc.text.split("\n")
|
|
291
|
+
end = {"line": len(lines) - 1, "character": len(lines[-1])}
|
|
292
|
+
return [{"range": {"start": {"line": 0, "character": 0}, "end": end},
|
|
293
|
+
"newText": new}]
|
|
294
|
+
|
|
295
|
+
def _hover(self, params):
|
|
296
|
+
doc = self.docs.get(params["textDocument"]["uri"])
|
|
297
|
+
if doc is None or doc.result is None:
|
|
298
|
+
return None
|
|
299
|
+
pos = params["position"]
|
|
300
|
+
seg = tr.island_at(doc.result.map, pos["line"], pos["character"])
|
|
301
|
+
if seg is not None:
|
|
302
|
+
gl0, gc0, gl1, gc1 = seg.gen
|
|
303
|
+
lines = doc.result.code.split("\n")[gl0:gl1 + 1]
|
|
304
|
+
if len(lines) == 1:
|
|
305
|
+
excerpt = lines[0][gc0:gc1]
|
|
306
|
+
else:
|
|
307
|
+
lines[0] = lines[0][gc0:]
|
|
308
|
+
lines[-1] = lines[-1][:gc1]
|
|
309
|
+
excerpt = "\n".join(lines)
|
|
310
|
+
sl0, sc0, sl1, sc1 = seg.src
|
|
311
|
+
return {"contents": {"kind": "markdown", "value":
|
|
312
|
+
"**%s island**\n```python\n%s\n```" % (
|
|
313
|
+
seg.kind.split(":", 1)[1], excerpt)},
|
|
314
|
+
"range": {"start": {"line": sl0, "character": sc0},
|
|
315
|
+
"end": {"line": sl1, "character": sc1}}}
|
|
316
|
+
return self._forward("textDocument/hover", params)
|
|
317
|
+
|
|
318
|
+
# --------------------------------------------------------- delegation
|
|
319
|
+
|
|
320
|
+
def _forward(self, method, params):
|
|
321
|
+
doc = self.docs.get(params.get("textDocument", {}).get("uri", ""))
|
|
322
|
+
if doc is None or doc.result is None or not self.backend \
|
|
323
|
+
or not self.backend.alive():
|
|
324
|
+
return None
|
|
325
|
+
lmap = doc.result.map
|
|
326
|
+
fwd = dict(params)
|
|
327
|
+
fwd["textDocument"] = {"uri": tr.shadow_uri(doc.uri)}
|
|
328
|
+
if "position" in params:
|
|
329
|
+
p = tr.pos_to_py(lmap, params["position"])
|
|
330
|
+
if p is None:
|
|
331
|
+
return None
|
|
332
|
+
fwd["position"] = p
|
|
333
|
+
raw = self.backend.forward(method, fwd)
|
|
334
|
+
return tr.translate_result(raw, lmap, self._maps_by_uri())
|
|
335
|
+
|
|
336
|
+
|
|
337
|
+
def main(argv=None):
|
|
338
|
+
import argparse
|
|
339
|
+
parser = argparse.ArgumentParser(prog="ldpy.lsp")
|
|
340
|
+
parser.add_argument("--backend", default="pylsp",
|
|
341
|
+
choices=["pylsp", "none"],
|
|
342
|
+
help="delegated Python server (default: pylsp)")
|
|
343
|
+
parser.add_argument("--line-length", type=int, default=None,
|
|
344
|
+
help="formatter line length "
|
|
345
|
+
"(default: black's own)")
|
|
346
|
+
args = parser.parse_args(argv)
|
|
347
|
+
server = LdpyServer(sys.stdin.buffer, sys.stdout.buffer,
|
|
348
|
+
backend=args.backend, line_length=args.line_length)
|
|
349
|
+
server.serve()
|
|
350
|
+
|
|
351
|
+
|
|
352
|
+
if __name__ == "__main__":
|
|
353
|
+
main()
|