lsp-devtools 0.2.2__tar.gz → 0.2.4__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.
Files changed (42) hide show
  1. {lsp_devtools-0.2.2 → lsp_devtools-0.2.4}/CHANGES.md +25 -0
  2. {lsp_devtools-0.2.2 → lsp_devtools-0.2.4}/PKG-INFO +5 -9
  3. lsp_devtools-0.2.4/hatch.toml +19 -0
  4. lsp_devtools-0.2.4/lsp_devtools/__init__.py +1 -0
  5. {lsp_devtools-0.2.2 → lsp_devtools-0.2.4}/lsp_devtools/agent/__init__.py +20 -45
  6. {lsp_devtools-0.2.2 → lsp_devtools-0.2.4}/lsp_devtools/agent/agent.py +101 -28
  7. lsp_devtools-0.2.4/lsp_devtools/agent/client.py +73 -0
  8. lsp_devtools-0.2.4/lsp_devtools/agent/protocol.py +7 -0
  9. lsp_devtools-0.2.4/lsp_devtools/agent/server.py +92 -0
  10. {lsp_devtools-0.2.2 → lsp_devtools-0.2.4}/lsp_devtools/client/__init__.py +9 -6
  11. {lsp_devtools-0.2.2 → lsp_devtools-0.2.4}/lsp_devtools/client/editor/text_editor.py +2 -3
  12. {lsp_devtools-0.2.2 → lsp_devtools-0.2.4}/lsp_devtools/client/lsp.py +17 -2
  13. {lsp_devtools-0.2.2 → lsp_devtools-0.2.4}/lsp_devtools/database.py +16 -23
  14. {lsp_devtools-0.2.2 → lsp_devtools-0.2.4}/lsp_devtools/handlers/__init__.py +21 -21
  15. {lsp_devtools-0.2.2 → lsp_devtools-0.2.4}/lsp_devtools/handlers/sql.py +1 -6
  16. {lsp_devtools-0.2.2 → lsp_devtools-0.2.4}/lsp_devtools/inspector/__init__.py +27 -70
  17. {lsp_devtools-0.2.2 → lsp_devtools-0.2.4}/lsp_devtools/record/__init__.py +50 -36
  18. {lsp_devtools-0.2.2 → lsp_devtools-0.2.4}/lsp_devtools/record/filters.py +17 -15
  19. {lsp_devtools-0.2.2 → lsp_devtools-0.2.4}/lsp_devtools/record/formatters.py +15 -20
  20. {lsp_devtools-0.2.2 → lsp_devtools-0.2.4}/lsp_devtools/record/visualize.py +5 -8
  21. {lsp_devtools-0.2.2 → lsp_devtools-0.2.4}/pyproject.toml +4 -12
  22. {lsp_devtools-0.2.2 → lsp_devtools-0.2.4}/tests/record/test_filters.py +17 -19
  23. {lsp_devtools-0.2.2 → lsp_devtools-0.2.4}/tests/record/test_formatters.py +1 -1
  24. {lsp_devtools-0.2.2 → lsp_devtools-0.2.4}/tests/record/test_record.py +4 -6
  25. {lsp_devtools-0.2.2 → lsp_devtools-0.2.4}/tests/servers/simple.py +8 -2
  26. {lsp_devtools-0.2.2 → lsp_devtools-0.2.4}/tests/test_agent.py +9 -4
  27. lsp_devtools-0.2.2/hatch.toml +0 -9
  28. lsp_devtools-0.2.2/lsp_devtools/__init__.py +0 -1
  29. lsp_devtools-0.2.2/lsp_devtools/agent/client.py +0 -116
  30. lsp_devtools-0.2.2/lsp_devtools/agent/protocol.py +0 -47
  31. lsp_devtools-0.2.2/lsp_devtools/agent/server.py +0 -108
  32. {lsp_devtools-0.2.2 → lsp_devtools-0.2.4}/.gitignore +0 -0
  33. {lsp_devtools-0.2.2 → lsp_devtools-0.2.4}/LICENSE +0 -0
  34. {lsp_devtools-0.2.2 → lsp_devtools-0.2.4}/README.md +0 -0
  35. {lsp_devtools-0.2.2 → lsp_devtools-0.2.4}/lsp_devtools/__main__.py +0 -0
  36. {lsp_devtools-0.2.2 → lsp_devtools-0.2.4}/lsp_devtools/cli.py +0 -0
  37. {lsp_devtools-0.2.2 → lsp_devtools-0.2.4}/lsp_devtools/client/app.css +0 -0
  38. {lsp_devtools-0.2.2 → lsp_devtools-0.2.4}/lsp_devtools/client/editor/__init__.py +0 -0
  39. {lsp_devtools-0.2.2 → lsp_devtools-0.2.4}/lsp_devtools/client/editor/completion.py +0 -0
  40. {lsp_devtools-0.2.2 → lsp_devtools-0.2.4}/lsp_devtools/handlers/dbinit.sql +0 -0
  41. {lsp_devtools-0.2.2 → lsp_devtools-0.2.4}/lsp_devtools/inspector/app.css +0 -0
  42. {lsp_devtools-0.2.2 → lsp_devtools-0.2.4}/lsp_devtools/py.typed +0 -0
@@ -1,3 +1,28 @@
1
+ ## v0.2.4 - 2024-11-23
2
+
3
+ ### Fixes
4
+
5
+ - The `lsp-devtools agent` should now suppress `asyncio.CancelledError` exceptions allowing the agent to process to terminate gracefully ([#191](https://github.com/swyddfa/lsp-devtools/issues/191))
6
+
7
+ ### Misc
8
+
9
+ - Drop Python 3.8 support ([#190](https://github.com/swyddfa/lsp-devtools/issues/190))
10
+ - Migrate to pygls `v2.0a2` ([#192](https://github.com/swyddfa/lsp-devtools/issues/192))
11
+
12
+
13
+ ## v0.2.3 - 2024-05-22
14
+
15
+
16
+ ### Enhancements
17
+
18
+ - The `lsp-devtools agent` now forwards the server's `stderr` channel ([#165](https://github.com/swyddfa/lsp-devtools/issues/165))
19
+
20
+ ### Fixes
21
+
22
+ - All `lsp-devtools` commands should no longer crash when encountering messages containing unicode characters ([#157](https://github.com/swyddfa/lsp-devtools/issues/157))
23
+ - Commands like `lsp-devtools record` should now continue to function after encountering an error ([#158](https://github.com/swyddfa/lsp-devtools/issues/158))
24
+
25
+
1
26
  ## v0.2.2 - 2024-01-29
2
27
 
3
28
 
@@ -1,31 +1,27 @@
1
- Metadata-Version: 2.1
1
+ Metadata-Version: 2.3
2
2
  Name: lsp-devtools
3
- Version: 0.2.2
3
+ Version: 0.2.4
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://swyddfa.github.io/lsp-devtools/
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
10
- License-File: LICENSE
11
10
  Classifier: Development Status :: 3 - Alpha
12
11
  Classifier: License :: OSI Approved :: MIT License
13
12
  Classifier: Programming Language :: Python
14
13
  Classifier: Programming Language :: Python :: 3
15
14
  Classifier: Programming Language :: Python :: 3 :: Only
16
- Classifier: Programming Language :: Python :: 3.8
17
15
  Classifier: Programming Language :: Python :: 3.9
18
16
  Classifier: Programming Language :: Python :: 3.10
19
17
  Classifier: Programming Language :: Python :: 3.11
20
18
  Classifier: Programming Language :: Python :: 3.12
21
- Requires-Python: >=3.8
19
+ Requires-Python: >=3.9
22
20
  Requires-Dist: aiosqlite
23
- Requires-Dist: importlib-resources; python_version < '3.9'
24
21
  Requires-Dist: platformdirs
25
- Requires-Dist: pygls>=1.1.0
22
+ Requires-Dist: pygls>=2.0a2
26
23
  Requires-Dist: stamina
27
24
  Requires-Dist: textual>=0.41.0
28
- Requires-Dist: typing-extensions; python_version < '3.8'
29
25
  Description-Content-Type: text/markdown
30
26
 
31
27
  # lsp-devtools: Developer tooling for language servers
@@ -0,0 +1,19 @@
1
+ [version]
2
+ path = "lsp_devtools/__init__.py"
3
+ validate-bump = false
4
+
5
+ [build.targets.sdist]
6
+ include = ["lsp_devtools", "tests", "CHANGES.md"]
7
+
8
+ [build.targets.wheel]
9
+ packages = ["lsp_devtools"]
10
+
11
+ [envs.hatch-test]
12
+ extra-dependencies = ["pytest-asyncio"]
13
+
14
+ [envs.hatch-test.env-vars]
15
+ UV_PRERELEASE="allow"
16
+
17
+ [envs.hatch-static-analysis]
18
+ config-path = "ruff_defaults.toml"
19
+ dependencies = ["ruff==0.8.0"]
@@ -0,0 +1 @@
1
+ __version__ = "0.2.4"
@@ -1,66 +1,43 @@
1
+ from __future__ import annotations
2
+
1
3
  import argparse
2
4
  import asyncio
3
- import logging
4
5
  import subprocess
5
6
  import sys
6
- from typing import List
7
- from uuid import uuid4
8
7
 
9
8
  from .agent import Agent
9
+ from .agent import RPCMessage
10
10
  from .agent import logger
11
+ from .agent import parse_rpc_message
11
12
  from .client import AgentClient
12
- from .protocol import MESSAGE_TEXT_NOTIFICATION
13
- from .protocol import MessageText
14
13
  from .server import AgentServer
15
- from .server import parse_rpc_message
16
14
 
17
15
  __all__ = [
18
16
  "Agent",
19
17
  "AgentClient",
20
18
  "AgentServer",
19
+ "RPCMessage",
21
20
  "logger",
22
- "MESSAGE_TEXT_NOTIFICATION",
23
- "MessageText",
24
21
  "parse_rpc_message",
25
22
  ]
26
23
 
27
24
 
28
- class MessageHandler(logging.Handler):
29
- """Logging handler that forwards captured JSON-RPC messages through to the
30
- ``AgentServer`` instance."""
31
-
32
- def __init__(self, client: AgentClient, *args, **kwargs):
33
- super().__init__(*args, **kwargs)
34
- self.client = client
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
25
+ async def forward_stderr(server: asyncio.subprocess.Process):
26
+ """Forward the server's stderr to the agent's stderr."""
27
+ if server.stderr is None:
28
+ return
49
29
 
50
- # Send any buffered messages
51
- while len(self._buffer) > 0:
52
- self.client.protocol.message_text_notification(self._buffer.pop(0))
30
+ # EOF is signalled with an empty bytestring
31
+ while (line := await server.stderr.readline()) != b"":
32
+ sys.stderr.buffer.write(line)
53
33
 
54
- self.client.protocol.message_text_notification(message)
55
34
 
56
-
57
- async def main(args, extra: List[str]):
35
+ async def main(args, extra: list[str]):
58
36
  if extra is None:
59
37
  print("Missing server start command", file=sys.stderr)
60
38
  return 1
61
39
 
62
40
  command, *arguments = extra
63
-
64
41
  server = await asyncio.create_subprocess_exec(
65
42
  command,
66
43
  *arguments,
@@ -68,23 +45,21 @@ async def main(args, extra: List[str]):
68
45
  stdout=subprocess.PIPE,
69
46
  stderr=subprocess.PIPE,
70
47
  )
71
- agent = Agent(server, sys.stdin.buffer, sys.stdout.buffer)
72
-
73
48
  client = AgentClient()
74
- handler = MessageHandler(client)
75
- handler.setLevel(logging.INFO)
76
-
77
- logger.setLevel(logging.INFO)
78
- logger.addHandler(handler)
49
+ agent = Agent(server, sys.stdin.buffer, sys.stdout.buffer, client.forward_message)
79
50
 
80
51
  await asyncio.gather(
81
52
  client.start_tcp(args.host, args.port),
82
53
  agent.start(),
54
+ forward_stderr(server),
83
55
  )
84
56
 
85
57
 
86
- def run_agent(args, extra: List[str]):
87
- asyncio.run(main(args, extra))
58
+ def run_agent(args, extra: list[str]):
59
+ try:
60
+ asyncio.run(main(args, extra))
61
+ except asyncio.CancelledError:
62
+ pass
88
63
 
89
64
 
90
65
  def cli(commands: argparse._SubParsersAction):
@@ -2,35 +2,81 @@ 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 collections.abc import Coroutine
19
+ from typing import Any
12
20
  from typing import BinaryIO
13
- from typing import Optional
14
- from typing import Set
15
- from typing import Tuple
21
+ from typing import Callable
22
+ from typing import Union
23
+
24
+ from pygls.io_ import AsyncReader
25
+
26
+ MessageHandler = Callable[[bytes], Union[None, Coroutine[Any, Any, None]]]
16
27
 
28
+ UTC = timezone.utc
17
29
  logger = logging.getLogger("lsp_devtools.agent")
18
30
 
19
31
 
20
- async def forward_message(source: str, dest: asyncio.StreamWriter, message: bytes):
21
- """Forward the given message to the destination channel"""
22
- dest.write(message)
23
- await dest.drain()
32
+ @attrs.define
33
+ class RPCMessage:
34
+ """A Json-RPC message."""
24
35
 
25
- # Log the full message
26
- logger.info(
27
- "%s",
28
- message.decode("utf8"),
29
- extra={"source": source},
30
- )
36
+ headers: dict[str, str]
37
+
38
+ body: dict[str, Any]
39
+
40
+ def __getitem__(self, key: str):
41
+ return self.headers[key]
42
+
43
+
44
+ def parse_rpc_message(data: bytes) -> RPCMessage:
45
+ """Parse a JSON-RPC message from the given set of bytes."""
46
+
47
+ headers: dict[str, str] = {}
48
+ body: dict[str, Any] | None = None
49
+ headers_complete = False
50
+
51
+ for line in data.split(b"\r\n"):
52
+ if line == b"":
53
+ if "Content-Length" not in headers:
54
+ raise ValueError("Missing 'Content-Length' header")
31
55
 
56
+ headers_complete = True
57
+ continue
32
58
 
33
- async def aio_readline(reader: asyncio.StreamReader, message_handler):
59
+ if headers_complete:
60
+ length = int(headers["Content-Length"])
61
+ if len(line) != length:
62
+ raise ValueError("Incorrect 'Content-Length'")
63
+
64
+ body = json.loads(line)
65
+ continue
66
+
67
+ if (idx := line.find(b":")) < 0:
68
+ raise ValueError(f"Invalid header: {line!r}")
69
+
70
+ name, value = line[:idx], line[idx + 1 :]
71
+ headers[name.decode("utf8").strip()] = value.decode("utf8").strip()
72
+
73
+ if body is None:
74
+ raise ValueError("Missing message body")
75
+
76
+ return RPCMessage(headers, body)
77
+
78
+
79
+ async def aio_readline(reader: AsyncReader, message_handler: MessageHandler):
34
80
  CONTENT_LENGTH_PATTERN = re.compile(rb"^Content-Length: (\d+)\r\n$")
35
81
 
36
82
  # Initialize message buffer
@@ -70,7 +116,7 @@ async def aio_readline(reader: asyncio.StreamReader, message_handler):
70
116
 
71
117
  async def get_streams(
72
118
  stdin, stdout
73
- ) -> Tuple[asyncio.StreamReader, asyncio.StreamWriter]:
119
+ ) -> tuple[asyncio.StreamReader, asyncio.StreamWriter]:
74
120
  """Convert blocking stdin/stdout streams into async streams."""
75
121
  loop = asyncio.get_running_loop()
76
122
 
@@ -90,29 +136,35 @@ class Agent:
90
136
  enabling them to be recorded."""
91
137
 
92
138
  def __init__(
93
- self, server: asyncio.subprocess.Process, stdin: BinaryIO, stdout: BinaryIO
139
+ self,
140
+ server: asyncio.subprocess.Process,
141
+ stdin: BinaryIO,
142
+ stdout: BinaryIO,
143
+ handler: MessageHandler,
94
144
  ):
95
145
  self.stdin = stdin
96
146
  self.stdout = stdout
97
147
  self.server = server
148
+ self.handler = handler
149
+ self.session_id = str(uuid4())
98
150
 
99
- self._tasks: Set[asyncio.Task] = set()
100
- self.reader: Optional[asyncio.StreamReader] = None
101
- self.writer: Optional[asyncio.StreamWriter] = None
151
+ self._tasks: set[asyncio.Task] = set()
152
+ self.reader: asyncio.StreamReader | None = None
153
+ self.writer: asyncio.StreamWriter | None = None
102
154
 
103
155
  async def start(self):
104
156
  # Get async versions of stdin/stdout
105
157
  self.reader, self.writer = await get_streams(self.stdin, self.stdout)
106
158
 
107
159
  # Keep mypy happy
108
- assert self.server.stdin
109
- assert self.server.stdout
160
+ if self.server.stdin is None or self.server.stdout is None:
161
+ raise RuntimeError("Unable to find server I/O streams")
110
162
 
111
163
  # Connect stdin to the subprocess' stdin
112
164
  client_to_server = asyncio.create_task(
113
165
  aio_readline(
114
166
  self.reader,
115
- partial(forward_message, "client", self.server.stdin),
167
+ partial(self.forward_message, "client", self.server.stdin),
116
168
  ),
117
169
  )
118
170
  self._tasks.add(client_to_server)
@@ -121,7 +173,7 @@ class Agent:
121
173
  server_to_client = asyncio.create_task(
122
174
  aio_readline(
123
175
  self.server.stdout,
124
- partial(forward_message, "server", self.writer),
176
+ partial(self.forward_message, "server", self.writer),
125
177
  ),
126
178
  )
127
179
  self._tasks.add(server_to_client)
@@ -133,6 +185,30 @@ class Agent:
133
185
  self._watch_server_process(),
134
186
  )
135
187
 
188
+ async def forward_message(
189
+ self, source: str, dest: asyncio.StreamWriter, message: bytes
190
+ ):
191
+ """Forward the given message to the destination channel"""
192
+
193
+ # Forward the message as-is to the client/server
194
+ dest.write(message)
195
+ await dest.drain()
196
+
197
+ # Include some additional metadata before passing it onto the devtool.
198
+ # TODO: How do we make sure we choose the same encoding as `message`?
199
+ now = datetime.now(tz=UTC).isoformat()
200
+ fields = [
201
+ f"Message-Source: {source}\r\n".encode(),
202
+ f"Message-Session: {self.session_id}\r\n".encode(),
203
+ f"Message-Timestamp: {now}\r\n".encode(),
204
+ message,
205
+ ]
206
+
207
+ if inspect.iscoroutine(res := self.handler(b"".join(fields))):
208
+ task = asyncio.create_task(res)
209
+ self._tasks.add(task)
210
+ task.add_done_callback(self._tasks.discard)
211
+
136
212
  async def _watch_server_process(self):
137
213
  """Once the server process exits, ensure that the agent is also shutdown."""
138
214
  ret = await self.server.wait()
@@ -148,13 +224,10 @@ class Agent:
148
224
  except TimeoutError:
149
225
  self.server.kill()
150
226
 
151
- args = {}
152
- if sys.version_info.minor > 8:
153
- args["msg"] = "lsp-devtools agent is stopping."
154
-
155
227
  # Cancel the tasks connecting client to server
156
228
  for task in self._tasks:
157
- task.cancel(**args)
229
+ logger.debug("cancelling: %s", task)
230
+ task.cancel(msg="lsp-devtools agent is stopping.")
158
231
 
159
232
  if self.writer:
160
233
  self.writer.close()
@@ -0,0 +1,73 @@
1
+ from __future__ import annotations
2
+
3
+ import asyncio
4
+ import inspect
5
+ import typing
6
+
7
+ import stamina
8
+ from pygls.client import JsonRPCClient
9
+ from pygls.protocol import default_converter
10
+
11
+ from lsp_devtools.agent.protocol import AgentProtocol
12
+
13
+ if typing.TYPE_CHECKING:
14
+ from typing import Any
15
+
16
+
17
+ class AgentClient(JsonRPCClient):
18
+ """Client for connecting to an AgentServer instance."""
19
+
20
+ protocol: AgentProtocol
21
+
22
+ def __init__(self):
23
+ super().__init__(
24
+ protocol_cls=AgentProtocol, converter_factory=default_converter
25
+ )
26
+ self.connected = False
27
+ self._buffer: list[bytes] = []
28
+ self._tasks: set[asyncio.Task[Any]] = set()
29
+
30
+ def _report_server_error(self, error, source):
31
+ # Bail on error
32
+ # TODO: Report the actual error somehow
33
+ self._stop_event.set()
34
+
35
+ def feature(self, feature_name: str, options: Any | None = None):
36
+ return self.protocol.fm.feature(feature_name, options)
37
+
38
+ async def start_tcp(self, host: str, port: int):
39
+ # The user might not have started the server app immediately and since the
40
+ # agent will live as long as the wrapper language server we may as well
41
+ # try indefinitely.
42
+ retries = stamina.retry_context(
43
+ on=OSError,
44
+ attempts=None,
45
+ timeout=None,
46
+ wait_initial=1,
47
+ wait_max=60,
48
+ )
49
+ async for attempt in retries:
50
+ with attempt:
51
+ await super().start_tcp(host, port)
52
+ self.connected = True
53
+
54
+ def forward_message(self, message: bytes):
55
+ """Forward the given message to the server instance."""
56
+
57
+ if not self.connected or self.protocol.writer is None:
58
+ self._buffer.append(message)
59
+ return
60
+
61
+ # Send any buffered messages
62
+ while len(self._buffer) > 0:
63
+ res = self.protocol.writer.write(self._buffer.pop(0))
64
+ if inspect.isawaitable(res):
65
+ task = asyncio.ensure_future(res)
66
+ task.add_done_callback(self._tasks.discard)
67
+ self._tasks.add(task)
68
+
69
+ res = self.protocol.writer.write(message)
70
+ if inspect.isawaitable(res):
71
+ task = asyncio.ensure_future(res)
72
+ task.add_done_callback(self._tasks.discard)
73
+ self._tasks.add(task)
@@ -0,0 +1,7 @@
1
+ from __future__ import annotations
2
+
3
+ from pygls.protocol import JsonRPCProtocol
4
+
5
+
6
+ class AgentProtocol(JsonRPCProtocol):
7
+ """The RPC protocol exposed by the agent."""
@@ -0,0 +1,92 @@
1
+ from __future__ import annotations
2
+
3
+ import asyncio
4
+ import json
5
+ import logging
6
+ import traceback
7
+ import typing
8
+
9
+ from pygls.protocol import default_converter
10
+ from pygls.server import JsonRPCServer
11
+
12
+ from lsp_devtools.agent.agent import aio_readline
13
+ from lsp_devtools.agent.protocol import AgentProtocol
14
+ from lsp_devtools.database import Database
15
+
16
+ if typing.TYPE_CHECKING:
17
+ from typing import Any
18
+
19
+ from lsp_devtools.agent.agent import MessageHandler
20
+
21
+
22
+ class AgentServer(JsonRPCServer):
23
+ """A pygls server that accepts connections from agents allowing them to send their
24
+ collected messages."""
25
+
26
+ lsp: AgentProtocol
27
+
28
+ def __init__(
29
+ self,
30
+ *args,
31
+ logger: logging.Logger | None = None,
32
+ handler: MessageHandler | None = None,
33
+ **kwargs,
34
+ ):
35
+ if "protocol_cls" not in kwargs:
36
+ kwargs["protocol_cls"] = AgentProtocol
37
+
38
+ if "converter_factory" not in kwargs:
39
+ kwargs["converter_factory"] = default_converter
40
+
41
+ super().__init__(*args, **kwargs)
42
+
43
+ self.logger = logger or logging.getLogger(__name__)
44
+ self.handler = handler or self._default_handler
45
+ self.db: Database | None = None
46
+
47
+ self._client_buffer: list[str] = []
48
+ self._server_buffer: list[str] = []
49
+ self._tcp_server: asyncio.Task | None = None
50
+
51
+ def _default_handler(self, data: bytes):
52
+ message = self.protocol.structure_message(json.loads(data))
53
+ self.protocol.handle_message(message)
54
+
55
+ def _report_server_error(self, error: Exception, source):
56
+ """Report internal server errors."""
57
+ tb = "".join(
58
+ traceback.format_exception(type(error), error, error.__traceback__)
59
+ )
60
+ self.logger.error("%s: %s", type(error).__name__, error)
61
+ self.logger.debug("%s", tb)
62
+
63
+ def feature(self, feature_name: str, options: Any | None = None):
64
+ return self.lsp.fm.feature(feature_name, options)
65
+
66
+ async def start_tcp(self, host: str, port: int) -> None: # type: ignore[override]
67
+ async def handle_client(
68
+ reader: asyncio.StreamReader, writer: asyncio.StreamWriter
69
+ ):
70
+ self.protocol.set_writer(writer)
71
+
72
+ try:
73
+ await aio_readline(reader, self.handler)
74
+ except asyncio.CancelledError:
75
+ pass
76
+ finally:
77
+ writer.close()
78
+ await writer.wait_closed()
79
+
80
+ # Uncomment if we ever need to introduce a mode where the server stops
81
+ # automatically once a session ends.
82
+ #
83
+ # self.stop()
84
+
85
+ server = await asyncio.start_server(handle_client, host, port)
86
+ async with server:
87
+ self._tcp_server = asyncio.create_task(server.serve_forever())
88
+ await self._tcp_server
89
+
90
+ def stop(self):
91
+ if self._tcp_server is not None:
92
+ self._tcp_server.cancel()
@@ -1,9 +1,10 @@
1
+ from __future__ import annotations
2
+
1
3
  import argparse
2
4
  import asyncio
3
5
  import logging
4
6
  import os
5
7
  import pathlib
6
- from typing import List
7
8
  from uuid import uuid4
8
9
 
9
10
  import platformdirs
@@ -54,7 +55,7 @@ class LSPClient(App):
54
55
  ]
55
56
 
56
57
  def __init__(
57
- self, db: Database, server_command: List[str], session: str, *args, **kwargs
58
+ self, db: Database, server_command: list[str], session: str, *args, **kwargs
58
59
  ):
59
60
  super().__init__(*args, **kwargs)
60
61
 
@@ -65,11 +66,13 @@ class LSPClient(App):
65
66
  self.server_command = server_command
66
67
  self.lsp_client = LanguageClient()
67
68
 
68
- self._async_tasks: List[asyncio.Task] = []
69
+ self._async_tasks: list[asyncio.Task] = []
69
70
 
70
71
  def compose(self) -> ComposeResult:
71
72
  message_viewer = MessageViewer("")
72
- messages_table = MessagesTable(self.db, message_viewer, session=self.session)
73
+ messages_table = MessagesTable(
74
+ self.db, message_viewer, session=self.lsp_client.session_id
75
+ )
73
76
 
74
77
  yield Header()
75
78
  yield Explorer(".")
@@ -138,14 +141,14 @@ class LSPClient(App):
138
141
  await super().action_quit()
139
142
 
140
143
 
141
- def client(args, extra: List[str]):
144
+ def client(args, extra: list[str]):
142
145
  if len(extra) == 0:
143
146
  raise ValueError("Missing server command.")
144
147
 
145
148
  db = Database(args.dbpath)
146
149
 
147
150
  session = str(uuid4())
148
- dbhandler = DatabaseLogHandler(db, session=session)
151
+ dbhandler = DatabaseLogHandler(db)
149
152
  dbhandler.setLevel(logging.INFO)
150
153
 
151
154
  logger.setLevel(logging.INFO)
@@ -3,7 +3,6 @@ from __future__ import annotations
3
3
  import contextlib
4
4
  import pathlib
5
5
  import typing
6
- from typing import List
7
6
  from typing import Union
8
7
 
9
8
  from lsprotocol import types
@@ -15,7 +14,7 @@ from textual.widgets import TextArea
15
14
  if typing.TYPE_CHECKING:
16
15
  from lsp_devtools.client.lsp import LanguageClient
17
16
 
18
- CompletionResult = Union[List[types.CompletionItem], types.CompletionList, None]
17
+ CompletionResult = Union[list[types.CompletionItem], types.CompletionList, None]
19
18
 
20
19
 
21
20
  # TODO: Refactor to
@@ -104,7 +103,7 @@ class TextEditor(TextArea):
104
103
  version=self.version, uri=self.uri
105
104
  ),
106
105
  content_changes=[
107
- types.TextDocumentContentChangeEvent_Type1(
106
+ types.TextDocumentContentChangePartial(
108
107
  text=edit.text,
109
108
  range=types.Range(
110
109
  start=types.Position(line=start_line, character=start_col),