lsp-devtools 0.2.2__tar.gz → 0.2.3__tar.gz
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.
- {lsp_devtools-0.2.2 → lsp_devtools-0.2.3}/CHANGES.md +13 -0
- {lsp_devtools-0.2.2 → lsp_devtools-0.2.3}/PKG-INFO +3 -3
- {lsp_devtools-0.2.2 → lsp_devtools-0.2.3}/hatch.toml +7 -0
- lsp_devtools-0.2.3/lsp_devtools/__init__.py +1 -0
- {lsp_devtools-0.2.2 → lsp_devtools-0.2.3}/lsp_devtools/agent/__init__.py +12 -41
- {lsp_devtools-0.2.2 → lsp_devtools-0.2.3}/lsp_devtools/agent/agent.py +95 -17
- {lsp_devtools-0.2.2 → lsp_devtools-0.2.3}/lsp_devtools/agent/client.py +25 -2
- lsp_devtools-0.2.3/lsp_devtools/agent/protocol.py +7 -0
- lsp_devtools-0.2.3/lsp_devtools/agent/server.py +85 -0
- {lsp_devtools-0.2.2 → lsp_devtools-0.2.3}/lsp_devtools/client/__init__.py +4 -2
- {lsp_devtools-0.2.2 → lsp_devtools-0.2.3}/lsp_devtools/client/lsp.py +17 -2
- {lsp_devtools-0.2.2 → lsp_devtools-0.2.3}/lsp_devtools/database.py +13 -12
- {lsp_devtools-0.2.2 → lsp_devtools-0.2.3}/lsp_devtools/handlers/__init__.py +17 -16
- {lsp_devtools-0.2.2 → lsp_devtools-0.2.3}/lsp_devtools/handlers/sql.py +2 -2
- {lsp_devtools-0.2.2 → lsp_devtools-0.2.3}/lsp_devtools/inspector/__init__.py +15 -56
- {lsp_devtools-0.2.2 → lsp_devtools-0.2.3}/lsp_devtools/record/__init__.py +42 -26
- {lsp_devtools-0.2.2 → lsp_devtools-0.2.3}/lsp_devtools/record/filters.py +2 -2
- {lsp_devtools-0.2.2 → lsp_devtools-0.2.3}/lsp_devtools/record/formatters.py +2 -1
- {lsp_devtools-0.2.2 → lsp_devtools-0.2.3}/lsp_devtools/record/visualize.py +1 -1
- {lsp_devtools-0.2.2 → lsp_devtools-0.2.3}/pyproject.toml +1 -5
- {lsp_devtools-0.2.2 → lsp_devtools-0.2.3}/tests/record/test_filters.py +11 -11
- {lsp_devtools-0.2.2 → lsp_devtools-0.2.3}/tests/record/test_formatters.py +1 -1
- {lsp_devtools-0.2.2 → lsp_devtools-0.2.3}/tests/record/test_record.py +2 -2
- {lsp_devtools-0.2.2 → lsp_devtools-0.2.3}/tests/servers/simple.py +1 -0
- {lsp_devtools-0.2.2 → lsp_devtools-0.2.3}/tests/test_agent.py +9 -4
- lsp_devtools-0.2.2/lsp_devtools/__init__.py +0 -1
- lsp_devtools-0.2.2/lsp_devtools/agent/protocol.py +0 -47
- lsp_devtools-0.2.2/lsp_devtools/agent/server.py +0 -108
- {lsp_devtools-0.2.2 → lsp_devtools-0.2.3}/.gitignore +0 -0
- {lsp_devtools-0.2.2 → lsp_devtools-0.2.3}/LICENSE +0 -0
- {lsp_devtools-0.2.2 → lsp_devtools-0.2.3}/README.md +0 -0
- {lsp_devtools-0.2.2 → lsp_devtools-0.2.3}/lsp_devtools/__main__.py +0 -0
- {lsp_devtools-0.2.2 → lsp_devtools-0.2.3}/lsp_devtools/cli.py +0 -0
- {lsp_devtools-0.2.2 → lsp_devtools-0.2.3}/lsp_devtools/client/app.css +0 -0
- {lsp_devtools-0.2.2 → lsp_devtools-0.2.3}/lsp_devtools/client/editor/__init__.py +0 -0
- {lsp_devtools-0.2.2 → lsp_devtools-0.2.3}/lsp_devtools/client/editor/completion.py +0 -0
- {lsp_devtools-0.2.2 → lsp_devtools-0.2.3}/lsp_devtools/client/editor/text_editor.py +0 -0
- {lsp_devtools-0.2.2 → lsp_devtools-0.2.3}/lsp_devtools/handlers/dbinit.sql +0 -0
- {lsp_devtools-0.2.2 → lsp_devtools-0.2.3}/lsp_devtools/inspector/app.css +0 -0
- {lsp_devtools-0.2.2 → lsp_devtools-0.2.3}/lsp_devtools/py.typed +0 -0
|
@@ -1,3 +1,16 @@
|
|
|
1
|
+
## v0.2.3 - 2024-05-22
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
### Enhancements
|
|
5
|
+
|
|
6
|
+
- The `lsp-devtools agent` now forwards the server's `stderr` channel ([#165](https://github.com/swyddfa/lsp-devtools/issues/165))
|
|
7
|
+
|
|
8
|
+
### Fixes
|
|
9
|
+
|
|
10
|
+
- All `lsp-devtools` commands should no longer crash when encountering messages containing unicode characters ([#157](https://github.com/swyddfa/lsp-devtools/issues/157))
|
|
11
|
+
- Commands like `lsp-devtools record` should now continue to function after encountering an error ([#158](https://github.com/swyddfa/lsp-devtools/issues/158))
|
|
12
|
+
|
|
13
|
+
|
|
1
14
|
## v0.2.2 - 2024-01-29
|
|
2
15
|
|
|
3
16
|
|
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.3
|
|
2
2
|
Name: lsp-devtools
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.3
|
|
4
4
|
Summary: Developer tooling for language servers
|
|
5
5
|
Project-URL: Bug Tracker, https://github.com/swyddfa/lsp-devtools/issues
|
|
6
|
-
Project-URL: Documentation, https://
|
|
6
|
+
Project-URL: Documentation, https://lsp-devtools.readthedocs.io/en/latest/
|
|
7
7
|
Project-URL: Source Code, https://github.com/swyddfa/lsp-devtools
|
|
8
8
|
Author-email: Alex Carney <alcarneyme@gmail.com>
|
|
9
9
|
License: MIT
|
|
@@ -7,3 +7,10 @@ include = ["lsp_devtools", "tests", "CHANGES.md"]
|
|
|
7
7
|
|
|
8
8
|
[build.targets.wheel]
|
|
9
9
|
packages = ["lsp_devtools"]
|
|
10
|
+
|
|
11
|
+
[envs.hatch-test]
|
|
12
|
+
extra-dependencies = ["pytest-asyncio"]
|
|
13
|
+
|
|
14
|
+
[envs.hatch-static-analysis]
|
|
15
|
+
config-path = "ruff_defaults.toml"
|
|
16
|
+
dependencies = ["ruff==0.4.4"]
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "0.2.3"
|
|
@@ -1,57 +1,34 @@
|
|
|
1
1
|
import argparse
|
|
2
2
|
import asyncio
|
|
3
|
-
import logging
|
|
4
3
|
import subprocess
|
|
5
4
|
import sys
|
|
6
5
|
from typing import List
|
|
7
|
-
from uuid import uuid4
|
|
8
6
|
|
|
9
7
|
from .agent import Agent
|
|
8
|
+
from .agent import RPCMessage
|
|
10
9
|
from .agent import logger
|
|
10
|
+
from .agent import parse_rpc_message
|
|
11
11
|
from .client import AgentClient
|
|
12
|
-
from .protocol import MESSAGE_TEXT_NOTIFICATION
|
|
13
|
-
from .protocol import MessageText
|
|
14
12
|
from .server import AgentServer
|
|
15
|
-
from .server import parse_rpc_message
|
|
16
13
|
|
|
17
14
|
__all__ = [
|
|
18
15
|
"Agent",
|
|
19
16
|
"AgentClient",
|
|
20
17
|
"AgentServer",
|
|
18
|
+
"RPCMessage",
|
|
21
19
|
"logger",
|
|
22
|
-
"MESSAGE_TEXT_NOTIFICATION",
|
|
23
|
-
"MessageText",
|
|
24
20
|
"parse_rpc_message",
|
|
25
21
|
]
|
|
26
22
|
|
|
27
23
|
|
|
28
|
-
|
|
29
|
-
"""
|
|
30
|
-
|
|
24
|
+
async def forward_stderr(server: asyncio.subprocess.Process):
|
|
25
|
+
"""Forward the server's stderr to the agent's stderr."""
|
|
26
|
+
if server.stderr is None:
|
|
27
|
+
return
|
|
31
28
|
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
self.session = str(uuid4())
|
|
36
|
-
self._buffer: List[MessageText] = []
|
|
37
|
-
|
|
38
|
-
def emit(self, record: logging.LogRecord):
|
|
39
|
-
message = MessageText(
|
|
40
|
-
text=record.args[0], # type: ignore
|
|
41
|
-
session=self.session,
|
|
42
|
-
timestamp=record.created,
|
|
43
|
-
source=record.__dict__["source"],
|
|
44
|
-
)
|
|
45
|
-
|
|
46
|
-
if not self.client.connected:
|
|
47
|
-
self._buffer.append(message)
|
|
48
|
-
return
|
|
49
|
-
|
|
50
|
-
# Send any buffered messages
|
|
51
|
-
while len(self._buffer) > 0:
|
|
52
|
-
self.client.protocol.message_text_notification(self._buffer.pop(0))
|
|
53
|
-
|
|
54
|
-
self.client.protocol.message_text_notification(message)
|
|
29
|
+
# EOF is signalled with an empty bytestring
|
|
30
|
+
while (line := await server.stderr.readline()) != b"":
|
|
31
|
+
sys.stderr.buffer.write(line)
|
|
55
32
|
|
|
56
33
|
|
|
57
34
|
async def main(args, extra: List[str]):
|
|
@@ -60,7 +37,6 @@ async def main(args, extra: List[str]):
|
|
|
60
37
|
return 1
|
|
61
38
|
|
|
62
39
|
command, *arguments = extra
|
|
63
|
-
|
|
64
40
|
server = await asyncio.create_subprocess_exec(
|
|
65
41
|
command,
|
|
66
42
|
*arguments,
|
|
@@ -68,18 +44,13 @@ async def main(args, extra: List[str]):
|
|
|
68
44
|
stdout=subprocess.PIPE,
|
|
69
45
|
stderr=subprocess.PIPE,
|
|
70
46
|
)
|
|
71
|
-
agent = Agent(server, sys.stdin.buffer, sys.stdout.buffer)
|
|
72
|
-
|
|
73
47
|
client = AgentClient()
|
|
74
|
-
|
|
75
|
-
handler.setLevel(logging.INFO)
|
|
76
|
-
|
|
77
|
-
logger.setLevel(logging.INFO)
|
|
78
|
-
logger.addHandler(handler)
|
|
48
|
+
agent = Agent(server, sys.stdin.buffer, sys.stdout.buffer, client.forward_message)
|
|
79
49
|
|
|
80
50
|
await asyncio.gather(
|
|
81
51
|
client.start_tcp(args.host, args.port),
|
|
82
52
|
agent.start(),
|
|
53
|
+
forward_stderr(server),
|
|
83
54
|
)
|
|
84
55
|
|
|
85
56
|
|
|
@@ -2,35 +2,83 @@ from __future__ import annotations
|
|
|
2
2
|
|
|
3
3
|
import asyncio
|
|
4
4
|
import inspect
|
|
5
|
+
import json
|
|
5
6
|
import logging
|
|
6
7
|
import re
|
|
7
8
|
import sys
|
|
8
9
|
import typing
|
|
10
|
+
from datetime import datetime
|
|
11
|
+
from datetime import timezone
|
|
9
12
|
from functools import partial
|
|
13
|
+
from uuid import uuid4
|
|
14
|
+
|
|
15
|
+
import attrs
|
|
10
16
|
|
|
11
17
|
if typing.TYPE_CHECKING:
|
|
18
|
+
from typing import Any
|
|
12
19
|
from typing import BinaryIO
|
|
20
|
+
from typing import Callable
|
|
21
|
+
from typing import Coroutine
|
|
22
|
+
from typing import Dict
|
|
13
23
|
from typing import Optional
|
|
14
24
|
from typing import Set
|
|
15
25
|
from typing import Tuple
|
|
26
|
+
from typing import Union
|
|
27
|
+
|
|
28
|
+
MessageHandler = Callable[[bytes], Union[None, Coroutine[Any, Any, None]]]
|
|
16
29
|
|
|
30
|
+
UTC = timezone.utc
|
|
17
31
|
logger = logging.getLogger("lsp_devtools.agent")
|
|
18
32
|
|
|
19
33
|
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
await dest.drain()
|
|
34
|
+
@attrs.define
|
|
35
|
+
class RPCMessage:
|
|
36
|
+
"""A Json-RPC message."""
|
|
24
37
|
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
38
|
+
headers: Dict[str, str]
|
|
39
|
+
|
|
40
|
+
body: Dict[str, Any]
|
|
41
|
+
|
|
42
|
+
def __getitem__(self, key: str):
|
|
43
|
+
return self.headers[key]
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def parse_rpc_message(data: bytes) -> RPCMessage:
|
|
47
|
+
"""Parse a JSON-RPC message from the given set of bytes."""
|
|
48
|
+
|
|
49
|
+
headers: Dict[str, str] = {}
|
|
50
|
+
body: Optional[Dict[str, Any]] = None
|
|
51
|
+
headers_complete = False
|
|
52
|
+
|
|
53
|
+
for line in data.split(b"\r\n"):
|
|
54
|
+
if line == b"":
|
|
55
|
+
if "Content-Length" not in headers:
|
|
56
|
+
raise ValueError("Missing 'Content-Length' header")
|
|
31
57
|
|
|
58
|
+
headers_complete = True
|
|
59
|
+
continue
|
|
32
60
|
|
|
33
|
-
|
|
61
|
+
if headers_complete:
|
|
62
|
+
length = int(headers["Content-Length"])
|
|
63
|
+
if len(line) != length:
|
|
64
|
+
raise ValueError("Incorrect 'Content-Length'")
|
|
65
|
+
|
|
66
|
+
body = json.loads(line)
|
|
67
|
+
continue
|
|
68
|
+
|
|
69
|
+
if (idx := line.find(b":")) < 0:
|
|
70
|
+
raise ValueError(f"Invalid header: {line!r}")
|
|
71
|
+
|
|
72
|
+
name, value = line[:idx], line[idx + 1 :]
|
|
73
|
+
headers[name.decode("utf8").strip()] = value.decode("utf8").strip()
|
|
74
|
+
|
|
75
|
+
if body is None:
|
|
76
|
+
raise ValueError("Missing message body")
|
|
77
|
+
|
|
78
|
+
return RPCMessage(headers, body)
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
async def aio_readline(reader: asyncio.StreamReader, message_handler: MessageHandler):
|
|
34
82
|
CONTENT_LENGTH_PATTERN = re.compile(rb"^Content-Length: (\d+)\r\n$")
|
|
35
83
|
|
|
36
84
|
# Initialize message buffer
|
|
@@ -90,11 +138,17 @@ class Agent:
|
|
|
90
138
|
enabling them to be recorded."""
|
|
91
139
|
|
|
92
140
|
def __init__(
|
|
93
|
-
self,
|
|
141
|
+
self,
|
|
142
|
+
server: asyncio.subprocess.Process,
|
|
143
|
+
stdin: BinaryIO,
|
|
144
|
+
stdout: BinaryIO,
|
|
145
|
+
handler: MessageHandler,
|
|
94
146
|
):
|
|
95
147
|
self.stdin = stdin
|
|
96
148
|
self.stdout = stdout
|
|
97
149
|
self.server = server
|
|
150
|
+
self.handler = handler
|
|
151
|
+
self.session_id = str(uuid4())
|
|
98
152
|
|
|
99
153
|
self._tasks: Set[asyncio.Task] = set()
|
|
100
154
|
self.reader: Optional[asyncio.StreamReader] = None
|
|
@@ -105,14 +159,14 @@ class Agent:
|
|
|
105
159
|
self.reader, self.writer = await get_streams(self.stdin, self.stdout)
|
|
106
160
|
|
|
107
161
|
# Keep mypy happy
|
|
108
|
-
|
|
109
|
-
|
|
162
|
+
if self.server.stdin is None or self.server.stdout is None:
|
|
163
|
+
raise RuntimeError("Unable to find server I/O streams")
|
|
110
164
|
|
|
111
165
|
# Connect stdin to the subprocess' stdin
|
|
112
166
|
client_to_server = asyncio.create_task(
|
|
113
167
|
aio_readline(
|
|
114
168
|
self.reader,
|
|
115
|
-
partial(forward_message, "client", self.server.stdin),
|
|
169
|
+
partial(self.forward_message, "client", self.server.stdin),
|
|
116
170
|
),
|
|
117
171
|
)
|
|
118
172
|
self._tasks.add(client_to_server)
|
|
@@ -121,7 +175,7 @@ class Agent:
|
|
|
121
175
|
server_to_client = asyncio.create_task(
|
|
122
176
|
aio_readline(
|
|
123
177
|
self.server.stdout,
|
|
124
|
-
partial(forward_message, "server", self.writer),
|
|
178
|
+
partial(self.forward_message, "server", self.writer),
|
|
125
179
|
),
|
|
126
180
|
)
|
|
127
181
|
self._tasks.add(server_to_client)
|
|
@@ -133,6 +187,30 @@ class Agent:
|
|
|
133
187
|
self._watch_server_process(),
|
|
134
188
|
)
|
|
135
189
|
|
|
190
|
+
async def forward_message(
|
|
191
|
+
self, source: str, dest: asyncio.StreamWriter, message: bytes
|
|
192
|
+
):
|
|
193
|
+
"""Forward the given message to the destination channel"""
|
|
194
|
+
|
|
195
|
+
# Forward the message as-is to the client/server
|
|
196
|
+
dest.write(message)
|
|
197
|
+
await dest.drain()
|
|
198
|
+
|
|
199
|
+
# Include some additional metadata before passing it onto the devtool.
|
|
200
|
+
# TODO: How do we make sure we choose the same encoding as `message`?
|
|
201
|
+
now = datetime.now(tz=UTC).isoformat()
|
|
202
|
+
fields = [
|
|
203
|
+
f"Message-Source: {source}\r\n".encode(),
|
|
204
|
+
f"Message-Session: {self.session_id}\r\n".encode(),
|
|
205
|
+
f"Message-Timestamp: {now}\r\n".encode(),
|
|
206
|
+
message,
|
|
207
|
+
]
|
|
208
|
+
|
|
209
|
+
if inspect.iscoroutine(res := self.handler(b"".join(fields))):
|
|
210
|
+
task = asyncio.create_task(res)
|
|
211
|
+
self._tasks.add(task)
|
|
212
|
+
task.add_done_callback(self._tasks.discard)
|
|
213
|
+
|
|
136
214
|
async def _watch_server_process(self):
|
|
137
215
|
"""Once the server process exits, ensure that the agent is also shutdown."""
|
|
138
216
|
ret = await self.server.wait()
|
|
@@ -149,7 +227,7 @@ class Agent:
|
|
|
149
227
|
self.server.kill()
|
|
150
228
|
|
|
151
229
|
args = {}
|
|
152
|
-
if sys.version_info
|
|
230
|
+
if sys.version_info >= (3, 9):
|
|
153
231
|
args["msg"] = "lsp-devtools agent is stopping."
|
|
154
232
|
|
|
155
233
|
# Cancel the tasks connecting client to server
|
|
@@ -1,6 +1,7 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
1
3
|
import asyncio
|
|
2
|
-
|
|
3
|
-
from typing import Optional
|
|
4
|
+
import typing
|
|
4
5
|
|
|
5
6
|
import stamina
|
|
6
7
|
from pygls.client import JsonRPCClient
|
|
@@ -9,6 +10,11 @@ from pygls.protocol import default_converter
|
|
|
9
10
|
|
|
10
11
|
from lsp_devtools.agent.protocol import AgentProtocol
|
|
11
12
|
|
|
13
|
+
if typing.TYPE_CHECKING:
|
|
14
|
+
from typing import Any
|
|
15
|
+
from typing import List
|
|
16
|
+
from typing import Optional
|
|
17
|
+
|
|
12
18
|
# from websockets.client import WebSocketClientProtocol
|
|
13
19
|
|
|
14
20
|
|
|
@@ -39,6 +45,7 @@ class AgentClient(JsonRPCClient):
|
|
|
39
45
|
protocol_cls=AgentProtocol, converter_factory=default_converter
|
|
40
46
|
)
|
|
41
47
|
self.connected = False
|
|
48
|
+
self._buffer: List[bytes] = []
|
|
42
49
|
|
|
43
50
|
def _report_server_error(self, error, source):
|
|
44
51
|
# Bail on error
|
|
@@ -71,6 +78,22 @@ class AgentClient(JsonRPCClient):
|
|
|
71
78
|
self.connected = True
|
|
72
79
|
self._async_tasks.append(connection)
|
|
73
80
|
|
|
81
|
+
def forward_message(self, message: bytes):
|
|
82
|
+
"""Forward the given message to the server instance."""
|
|
83
|
+
|
|
84
|
+
if not self.connected:
|
|
85
|
+
self._buffer.append(message)
|
|
86
|
+
return
|
|
87
|
+
|
|
88
|
+
if self.protocol.transport is None:
|
|
89
|
+
return
|
|
90
|
+
|
|
91
|
+
# Send any buffered messages
|
|
92
|
+
while len(self._buffer) > 0:
|
|
93
|
+
self.protocol.transport.write(self._buffer.pop(0))
|
|
94
|
+
|
|
95
|
+
self.protocol.transport.write(message)
|
|
96
|
+
|
|
74
97
|
# TODO: Upstream this... or at least something equivalent.
|
|
75
98
|
# def start_ws(self, host: str, port: int):
|
|
76
99
|
# self.protocol._send_only_body = True # Don't send headers within the payload
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
import logging
|
|
5
|
+
import traceback
|
|
6
|
+
import typing
|
|
7
|
+
|
|
8
|
+
from pygls.protocol import default_converter
|
|
9
|
+
from pygls.server import Server
|
|
10
|
+
|
|
11
|
+
from lsp_devtools.agent.agent import aio_readline
|
|
12
|
+
from lsp_devtools.agent.protocol import AgentProtocol
|
|
13
|
+
from lsp_devtools.database import Database
|
|
14
|
+
|
|
15
|
+
if typing.TYPE_CHECKING:
|
|
16
|
+
from typing import Any
|
|
17
|
+
from typing import List
|
|
18
|
+
from typing import Optional
|
|
19
|
+
|
|
20
|
+
from lsp_devtools.agent.agent import MessageHandler
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class AgentServer(Server):
|
|
24
|
+
"""A pygls server that accepts connections from agents allowing them to send their
|
|
25
|
+
collected messages."""
|
|
26
|
+
|
|
27
|
+
lsp: AgentProtocol
|
|
28
|
+
|
|
29
|
+
def __init__(
|
|
30
|
+
self,
|
|
31
|
+
*args,
|
|
32
|
+
logger: Optional[logging.Logger] = None,
|
|
33
|
+
handler: Optional[MessageHandler] = None,
|
|
34
|
+
**kwargs,
|
|
35
|
+
):
|
|
36
|
+
if "protocol_cls" not in kwargs:
|
|
37
|
+
kwargs["protocol_cls"] = AgentProtocol
|
|
38
|
+
|
|
39
|
+
if "converter_factory" not in kwargs:
|
|
40
|
+
kwargs["converter_factory"] = default_converter
|
|
41
|
+
|
|
42
|
+
super().__init__(*args, **kwargs)
|
|
43
|
+
|
|
44
|
+
self.logger = logger or logging.getLogger(__name__)
|
|
45
|
+
self.handler = handler or self.lsp.data_received
|
|
46
|
+
self.db: Optional[Database] = None
|
|
47
|
+
|
|
48
|
+
self._client_buffer: List[str] = []
|
|
49
|
+
self._server_buffer: List[str] = []
|
|
50
|
+
self._tcp_server: Optional[asyncio.Task] = None
|
|
51
|
+
|
|
52
|
+
def _report_server_error(self, exc: Exception, source):
|
|
53
|
+
"""Report internal server errors."""
|
|
54
|
+
tb = "".join(traceback.format_exception(type(exc), exc, exc.__traceback__))
|
|
55
|
+
self.logger.error("%s: %s", type(exc).__name__, exc)
|
|
56
|
+
self.logger.debug("%s", tb)
|
|
57
|
+
|
|
58
|
+
def feature(self, feature_name: str, options: Optional[Any] = None):
|
|
59
|
+
return self.lsp.fm.feature(feature_name, options)
|
|
60
|
+
|
|
61
|
+
async def start_tcp(self, host: str, port: int) -> None: # type: ignore[override]
|
|
62
|
+
async def handle_client(reader, writer):
|
|
63
|
+
self.lsp.connection_made(writer)
|
|
64
|
+
|
|
65
|
+
try:
|
|
66
|
+
await aio_readline(reader, self.handler)
|
|
67
|
+
except asyncio.CancelledError:
|
|
68
|
+
pass
|
|
69
|
+
finally:
|
|
70
|
+
writer.close()
|
|
71
|
+
await writer.wait_closed()
|
|
72
|
+
|
|
73
|
+
# Uncomment if we ever need to introduce a mode where the server stops
|
|
74
|
+
# automatically once a session ends.
|
|
75
|
+
#
|
|
76
|
+
# self.stop()
|
|
77
|
+
|
|
78
|
+
server = await asyncio.start_server(handle_client, host, port)
|
|
79
|
+
async with server:
|
|
80
|
+
self._tcp_server = asyncio.create_task(server.serve_forever())
|
|
81
|
+
await self._tcp_server
|
|
82
|
+
|
|
83
|
+
def stop(self):
|
|
84
|
+
if self._tcp_server is not None:
|
|
85
|
+
self._tcp_server.cancel()
|
|
@@ -69,7 +69,9 @@ class LSPClient(App):
|
|
|
69
69
|
|
|
70
70
|
def compose(self) -> ComposeResult:
|
|
71
71
|
message_viewer = MessageViewer("")
|
|
72
|
-
messages_table = MessagesTable(
|
|
72
|
+
messages_table = MessagesTable(
|
|
73
|
+
self.db, message_viewer, session=self.lsp_client.session_id
|
|
74
|
+
)
|
|
73
75
|
|
|
74
76
|
yield Header()
|
|
75
77
|
yield Explorer(".")
|
|
@@ -145,7 +147,7 @@ def client(args, extra: List[str]):
|
|
|
145
147
|
db = Database(args.dbpath)
|
|
146
148
|
|
|
147
149
|
session = str(uuid4())
|
|
148
|
-
dbhandler = DatabaseLogHandler(db
|
|
150
|
+
dbhandler = DatabaseLogHandler(db)
|
|
149
151
|
dbhandler.setLevel(logging.INFO)
|
|
150
152
|
|
|
151
153
|
logger.setLevel(logging.INFO)
|
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
import importlib.metadata
|
|
2
2
|
import json
|
|
3
|
+
from datetime import datetime
|
|
4
|
+
from datetime import timezone
|
|
3
5
|
from typing import Optional
|
|
6
|
+
from uuid import uuid4
|
|
4
7
|
|
|
5
8
|
from lsprotocol import types
|
|
6
9
|
from pygls.lsp.client import BaseLanguageClient
|
|
@@ -8,6 +11,7 @@ from pygls.protocol import LanguageServerProtocol
|
|
|
8
11
|
|
|
9
12
|
from lsp_devtools.agent import logger
|
|
10
13
|
|
|
14
|
+
UTC = timezone.utc
|
|
11
15
|
VERSION = importlib.metadata.version("lsp-devtools")
|
|
12
16
|
|
|
13
17
|
|
|
@@ -16,12 +20,17 @@ class RecordingLSProtocol(LanguageServerProtocol):
|
|
|
16
20
|
|
|
17
21
|
def __init__(self, server, converter):
|
|
18
22
|
super().__init__(server, converter)
|
|
23
|
+
self.session_id = ""
|
|
19
24
|
|
|
20
25
|
def _procedure_handler(self, message):
|
|
21
26
|
logger.info(
|
|
22
27
|
"%s",
|
|
23
28
|
json.dumps(message, default=self._serialize_message),
|
|
24
|
-
extra={
|
|
29
|
+
extra={
|
|
30
|
+
"Message-Source": "server",
|
|
31
|
+
"Message-Session": self.session_id,
|
|
32
|
+
"Message-Timestamp": datetime.now(tz=UTC).isoformat(),
|
|
33
|
+
},
|
|
25
34
|
)
|
|
26
35
|
return super()._procedure_handler(message)
|
|
27
36
|
|
|
@@ -29,7 +38,11 @@ class RecordingLSProtocol(LanguageServerProtocol):
|
|
|
29
38
|
logger.info(
|
|
30
39
|
"%s",
|
|
31
40
|
json.dumps(data, default=self._serialize_message),
|
|
32
|
-
extra={
|
|
41
|
+
extra={
|
|
42
|
+
"Message-Source": "client",
|
|
43
|
+
"Message-Session": self.session_id,
|
|
44
|
+
"Message-Timestamp": datetime.now(tz=UTC).isoformat(),
|
|
45
|
+
},
|
|
33
46
|
)
|
|
34
47
|
return super()._send_data(data)
|
|
35
48
|
|
|
@@ -40,6 +53,8 @@ class LanguageClient(BaseLanguageClient):
|
|
|
40
53
|
def __init__(self):
|
|
41
54
|
super().__init__("lsp-devtools", VERSION, protocol_cls=RecordingLSProtocol)
|
|
42
55
|
|
|
56
|
+
self.session_id = str(uuid4())
|
|
57
|
+
self.protocol.session_id = self.session_id # type: ignore[attr-defined]
|
|
43
58
|
self._server_capabilities: Optional[types.ServerCapabilities] = None
|
|
44
59
|
|
|
45
60
|
@property
|
|
@@ -9,7 +9,6 @@ from typing import Dict
|
|
|
9
9
|
from typing import List
|
|
10
10
|
from typing import Optional
|
|
11
11
|
from typing import Set
|
|
12
|
-
from uuid import uuid4
|
|
13
12
|
|
|
14
13
|
import aiosqlite
|
|
15
14
|
from textual.app import App
|
|
@@ -17,10 +16,10 @@ from textual.message import Message
|
|
|
17
16
|
|
|
18
17
|
from lsp_devtools.handlers import LspMessage
|
|
19
18
|
|
|
20
|
-
if sys.version_info
|
|
19
|
+
if sys.version_info < (3, 9):
|
|
21
20
|
import importlib_resources as resources
|
|
22
21
|
else:
|
|
23
|
-
|
|
22
|
+
from importlib import resources # type: ignore[no-redef]
|
|
24
23
|
|
|
25
24
|
|
|
26
25
|
class Database:
|
|
@@ -62,14 +61,14 @@ class Database:
|
|
|
62
61
|
|
|
63
62
|
await self.db.commit()
|
|
64
63
|
|
|
65
|
-
async def add_message(self, session: str, timestamp:
|
|
64
|
+
async def add_message(self, session: str, timestamp: str, source: str, rpc: dict):
|
|
66
65
|
"""Add a new rpc message to the database."""
|
|
67
66
|
|
|
68
|
-
msg_id = rpc.get("id"
|
|
69
|
-
method = rpc.get("method"
|
|
70
|
-
params = rpc.get("params"
|
|
71
|
-
result = rpc.get("result"
|
|
72
|
-
error = rpc.get("error"
|
|
67
|
+
msg_id = rpc.get("id")
|
|
68
|
+
method = rpc.get("method")
|
|
69
|
+
params = rpc.get("params")
|
|
70
|
+
result = rpc.get("result")
|
|
71
|
+
error = rpc.get("error")
|
|
73
72
|
|
|
74
73
|
async with self.cursor() as cursor:
|
|
75
74
|
await cursor.execute(
|
|
@@ -149,17 +148,19 @@ class Database:
|
|
|
149
148
|
class DatabaseLogHandler(logging.Handler):
|
|
150
149
|
"""A logging handler that records messages in the given database."""
|
|
151
150
|
|
|
152
|
-
def __init__(self, db: Database, *args,
|
|
151
|
+
def __init__(self, db: Database, *args, **kwargs):
|
|
153
152
|
super().__init__(*args, **kwargs)
|
|
154
153
|
self.db = db
|
|
155
|
-
self.session = session or str(uuid4())
|
|
156
154
|
self._tasks: Set[asyncio.Task] = set()
|
|
157
155
|
|
|
158
156
|
def emit(self, record: logging.LogRecord):
|
|
159
157
|
body = json.loads(record.args[0]) # type: ignore
|
|
160
158
|
task = asyncio.create_task(
|
|
161
159
|
self.db.add_message(
|
|
162
|
-
|
|
160
|
+
record.__dict__["Message-Session"],
|
|
161
|
+
record.__dict__["Message-Timestamp"],
|
|
162
|
+
record.__dict__["Message-Source"],
|
|
163
|
+
body,
|
|
163
164
|
)
|
|
164
165
|
)
|
|
165
166
|
|