Skip to content

Commit d175c54

Browse files
committed
feat: Provide workspace to LSP servers and containers
1 parent 25939fe commit d175c54

File tree

5 files changed

+61
-33
lines changed

5 files changed

+61
-33
lines changed

examples/pyright_container.py

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -18,11 +18,9 @@ async def main():
1818
# Set up workspace directory and mount it in Docker
1919
workspace = Path.cwd()
2020
async with PyrightClient(
21-
# here we use `PyrightContainerServer`
22-
server=PyrightContainerServer(
23-
mounts=[workspace] # Mount workspace into container
24-
),
2521
workspace=workspace,
22+
# here we use the containerized Pyright server
23+
server=PyrightContainerServer(),
2624
) as client:
2725
# Find definition of PyrightContainerServer at line 12, column 28
2826
refs = await client.request_definition_locations(

src/lsp_client/client/abc.py

Lines changed: 6 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
import anyio
1111
import asyncer
1212
from anyio import AsyncContextManagerMixin
13-
from attrs import Factory, define, field
13+
from attrs import define, field
1414
from loguru import logger
1515

1616
from lsp_client.capability.build import (
@@ -51,20 +51,16 @@ class LSPClient(
5151
AsyncContextManagerMixin,
5252
ABC,
5353
):
54-
_server: LSPServer = field(
55-
alias="server",
56-
default=Factory(lambda self: self.create_default_server(), takes_self=True),
57-
)
58-
54+
_server: LSPServer | None = field(alias="server", default=None)
5955
_workspace: RawWorkspace = field(alias="workspace", factory=Path.cwd)
56+
6057
sync_file: bool = True
6158
request_timeout: float = 5.0
6259

6360
_buffer: LSPFileBuffer = field(factory=LSPFileBuffer, init=False)
6461

65-
@property
66-
def server(self) -> LSPServer:
67-
return self._server
62+
def get_server(self) -> LSPServer:
63+
return self._server or self.create_default_server()
6864

6965
@override
7066
def get_workspace(self) -> Workspace:
@@ -241,7 +237,7 @@ async def __asynccontextmanager__(self) -> AsyncGenerator[Self]:
241237
async with (
242238
asyncer.create_task_group() as tg,
243239
channel[ServerRequest].create() as (sender, receiver),
244-
self._server.serve(sender=sender),
240+
self._server.serve(workspace=self.get_workspace(), sender=sender),
245241
):
246242
# start to receive server requests here,
247243
# since server notification can be sent before `initialize`

src/lsp_client/server/abc.py

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
)
1919
from lsp_client.server.types import ServerRequest
2020
from lsp_client.utils.channel import Sender
21+
from lsp_client.utils.workspace import Workspace
2122

2223

2324
@define(kw_only=True)
@@ -40,7 +41,7 @@ async def kill(self) -> None:
4041

4142
@abstractmethod
4243
@asynccontextmanager
43-
def run_process(self) -> AsyncGenerator[None]:
44+
def run_process(self, workspace: Workspace) -> AsyncGenerator[None]:
4445
"""Run the server process."""
4546

4647
async def _dispatch(self, sender: Sender[ServerRequest] | None) -> None:
@@ -82,12 +83,14 @@ async def notify(self, notification: RawNotification) -> None:
8283

8384
@asynccontextmanager
8485
async def serve(
85-
self, *, sender: Sender[ServerRequest] | None = None
86+
self,
87+
workspace: Workspace,
88+
*,
89+
sender: Sender[ServerRequest] | None = None,
8690
) -> AsyncGenerator[Self]:
8791
async with (
88-
self.run_process(),
92+
self.run_process(workspace),
8993
asyncer.create_task_group() as tg,
9094
):
9195
tg.soonify(self._dispatch)(sender)
92-
9396
yield self

src/lsp_client/server/container.py

Lines changed: 42 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,12 @@
55
from pathlib import Path
66
from typing import Annotated, Literal, final, override
77

8-
from attrs import define, field
8+
from attrs import Factory, define, field
99
from loguru import logger
1010
from pydantic import BaseModel, Field
1111

1212
from lsp_client.jsonrpc.parse import RawPackage
13+
from lsp_client.utils.workspace import Workspace
1314

1415
from .abc import LSPServer
1516
from .local import LocalServer
@@ -35,7 +36,7 @@ def __str__(self) -> str:
3536

3637

3738
class BindMount(MountBase):
38-
type: Literal["bind"] = "bind"
39+
type: str = "bind"
3940

4041
bind_propagation: (
4142
Literal["private", "rprivate", "shared", "rshared", "slave", "rslave"] | None
@@ -63,7 +64,7 @@ def from_path(cls, path: Path) -> BindMount:
6364

6465

6566
class VolumeMount(MountBase):
66-
type: Literal["volume"] = "volume"
67+
type: str = "volume"
6768

6869
volume_driver: str | None = None
6970
volume_subpath: str | None = None
@@ -87,7 +88,7 @@ def _parts(self) -> list[str]:
8788

8889

8990
class TmpfsMount(MountBase):
90-
type: Literal["tmpfs"] = "tmpfs"
91+
type: str = "tmpfs"
9192

9293
tmpfs_size: int | None = None
9394
tmpfs_mode: int | None = None
@@ -114,7 +115,6 @@ def _parts(self) -> list[str]:
114115
def _format_mount(mount: Mount) -> str:
115116
if isinstance(mount, Path):
116117
mount = BindMount.from_path(mount)
117-
118118
return str(mount)
119119

120120

@@ -124,23 +124,55 @@ class ContainerServer(LSPServer):
124124
"""Runtime for container backend, e.g. `docker` or `podman`."""
125125

126126
image: str
127-
mounts: list[Mount]
127+
"""The container image to use."""
128+
129+
workdir: Path = Path("/workspace")
130+
"""The working directory inside the container."""
131+
132+
mounts: list[Mount] = Factory(list)
133+
"""List of extra mounts to be mounted inside the container."""
128134

129135
backend: Literal["docker", "podman"] = "docker"
136+
"""The container backend to use. Can be either `docker` or `podman`."""
137+
130138
container_name: str | None = None
139+
"""Optional name for the container."""
140+
131141
extra_container_args: list[str] | None = None
142+
"""Extra arguments to pass to the container runtime."""
132143

133144
_local: LocalServer = field(init=False)
134145

135-
def format_command(self) -> list[str]:
146+
def format_command(self, workspace: Workspace) -> list[str]:
136147
cmd = [self.backend, "run", "--rm", "-i"]
137148

138149
if self.container_name:
139150
cmd.extend(("--name", self.container_name))
140151

141-
for mount in self.mounts:
152+
mounts = list(self.mounts)
153+
154+
match workspace.to_folders():
155+
case [folder]:
156+
mount = BindMount(
157+
source=str(folder.path),
158+
target=self.workdir.as_posix(),
159+
)
160+
mounts.append(mount)
161+
case folders:
162+
mounts.extend(
163+
BindMount(
164+
source=str(folder.path),
165+
target=(self.workdir / folder.name).as_posix(),
166+
)
167+
for folder in folders
168+
)
169+
170+
for mount in mounts:
142171
cmd.extend(("--mount", _format_mount(mount)))
143172

173+
# Set working directory
174+
cmd.extend(("--workdir", self.workdir.as_posix()))
175+
144176
if self.extra_container_args:
145177
cmd.extend(self.extra_container_args)
146178

@@ -162,8 +194,8 @@ async def kill(self) -> None:
162194

163195
@override
164196
@asynccontextmanager
165-
async def run_process(self) -> AsyncGenerator[None]:
166-
command = self.format_command()
197+
async def run_process(self, workspace: Workspace) -> AsyncGenerator[None]:
198+
command = self.format_command(workspace)
167199
logger.debug("Running docker runtime with command: {}", command)
168200

169201
self._local = LocalServer(command=command)

src/lsp_client/utils/workspace.py

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -19,9 +19,6 @@ def path(self) -> Path:
1919
return from_local_uri(self.uri)
2020

2121

22-
# type Workspace = Mapping[str, WorkspaceFolder]
23-
24-
2522
class Workspace(dict[str, WorkspaceFolder]):
2623
def to_folders(self) -> list[WorkspaceFolder]:
2724
return list(self.values())
@@ -38,10 +35,10 @@ def to_folders(self) -> list[WorkspaceFolder]:
3835
}
3936
)
4037

41-
type RawWorkspace = AnyPath | Mapping[str, AnyPath]
38+
type RawWorkspace = AnyPath | Mapping[str, AnyPath] | Workspace
4239

4340

44-
def workspace_converter(raw: RawWorkspace) -> Workspace:
41+
def format_workspace(raw: RawWorkspace) -> Workspace:
4542
match raw:
4643
case str() | os.PathLike() as root_folder_path:
4744
return Workspace(
@@ -52,6 +49,8 @@ def workspace_converter(raw: RawWorkspace) -> Workspace:
5249
)
5350
}
5451
)
52+
case Workspace() as ws:
53+
return ws
5554
case _ as mapping:
5655
return Workspace(
5756
{

0 commit comments

Comments
 (0)