55from pathlib import Path
66from typing import Annotated , Literal , final , override
77
8- from attrs import define , field
8+ from attrs import Factory , define , field
99from loguru import logger
1010from pydantic import BaseModel , Field
1111
1212from lsp_client .jsonrpc .parse import RawPackage
13+ from lsp_client .utils .workspace import Workspace
1314
1415from .abc import LSPServer
1516from .local import LocalServer
@@ -35,7 +36,7 @@ def __str__(self) -> str:
3536
3637
3738class 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
6566class 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
8990class 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]:
114115def _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 )
0 commit comments