1#[allow(unused_imports)]
4use super::{CommandRequest, WorkspaceError, WorkspaceVersionConflict};
5use super::{
6 LocalWorkspaceBackend, ManifestWorkspaceBackend, VirtualPathResolver, WorkspaceCapabilities,
7 WorkspaceCommandRunner, WorkspaceFileSystem, WorkspaceFileSystemExt, WorkspaceGit,
8 WorkspaceGitStashProvider, WorkspaceGitWorktreeProvider, WorkspacePath, WorkspacePathResolver,
9 WorkspaceRef, WorkspaceResult, WorkspaceSearch, WorkspaceTextReader, WorkspaceWriteOutcome,
10};
11use crate::code_intelligence::{LocalCodeIntelligence, WorkspaceCodeIntelligence};
12use anyhow::{anyhow, Result};
13use std::path::{Path, PathBuf};
14use std::sync::Arc;
15
16pub struct WorkspaceServices {
18 workspace_ref: WorkspaceRef,
19 capabilities: WorkspaceCapabilities,
20 path_resolver: Arc<dyn WorkspacePathResolver>,
21 file_system: Arc<dyn WorkspaceFileSystem>,
22 file_system_ext: Option<Arc<dyn WorkspaceFileSystemExt>>,
23 text_reader: Option<Arc<dyn WorkspaceTextReader>>,
24 command_runner: Option<Arc<dyn WorkspaceCommandRunner>>,
25 search: Option<Arc<dyn WorkspaceSearch>>,
26 code_intelligence: Option<Arc<dyn WorkspaceCodeIntelligence>>,
27 git: Option<Arc<dyn WorkspaceGit>>,
28 git_stash: Option<Arc<dyn WorkspaceGitStashProvider>>,
29 git_worktree: Option<Arc<dyn WorkspaceGitWorktreeProvider>>,
30 operation_timeout: Option<std::time::Duration>,
34 local_root: Option<PathBuf>,
35}
36
37impl std::fmt::Debug for WorkspaceServices {
38 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
39 f.debug_struct("WorkspaceServices")
40 .field("workspace_ref", &self.workspace_ref)
41 .field("capabilities", &self.capabilities)
42 .field("file_system_ext", &self.file_system_ext.is_some())
43 .field("text_reader", &self.text_reader.is_some())
44 .field("command_runner", &self.command_runner.is_some())
45 .field("search", &self.search.is_some())
46 .field("code_intelligence", &self.code_intelligence.is_some())
47 .field("git", &self.git.is_some())
48 .field("git_stash", &self.git_stash.is_some())
49 .field("git_worktree", &self.git_worktree.is_some())
50 .field("local_root", &self.local_root)
51 .finish()
52 }
53}
54
55impl WorkspaceServices {
56 pub(crate) fn new_with_git(
57 workspace_ref: WorkspaceRef,
58 mut capabilities: WorkspaceCapabilities,
59 path_resolver: Arc<dyn WorkspacePathResolver>,
60 file_system: Arc<dyn WorkspaceFileSystem>,
61 command_runner: Option<Arc<dyn WorkspaceCommandRunner>>,
62 search: Option<Arc<dyn WorkspaceSearch>>,
63 git: Option<Arc<dyn WorkspaceGit>>,
64 ) -> Self {
65 if command_runner.is_none() {
66 capabilities.exec = false;
67 }
68 if search.is_none() {
69 capabilities.search = false;
70 }
71 if git.is_none() {
72 capabilities.git = false;
73 }
74 capabilities.code_intelligence = false;
75 Self {
76 workspace_ref,
77 capabilities,
78 path_resolver,
79 file_system,
80 file_system_ext: None,
81 text_reader: None,
82 command_runner,
83 search,
84 code_intelligence: None,
85 git,
86 git_stash: None,
87 git_worktree: None,
88 operation_timeout: None,
89 local_root: None,
90 }
91 }
92
93 pub fn builder(
94 workspace_ref: WorkspaceRef,
95 file_system: Arc<dyn WorkspaceFileSystem>,
96 ) -> WorkspaceServicesBuilder {
97 WorkspaceServicesBuilder::new(workspace_ref, file_system)
98 }
99
100 pub fn local(root: impl Into<PathBuf>) -> Arc<Self> {
101 let backend = Arc::new(LocalWorkspaceBackend::new(root.into()));
102 let workspace_ref = WorkspaceRef::new(
103 backend.root.display().to_string(),
104 backend.root.display().to_string(),
105 );
106 let path_resolver: Arc<dyn WorkspacePathResolver> = backend.clone();
107 let file_system: Arc<dyn WorkspaceFileSystem> = backend.clone();
108 let text_reader: Arc<dyn WorkspaceTextReader> = backend.clone();
109 let command_runner: Arc<dyn WorkspaceCommandRunner> = backend.clone();
110 let search: Arc<dyn WorkspaceSearch> = backend.clone();
111 let git: Arc<dyn WorkspaceGit> = backend.clone();
112 let git_stash: Arc<dyn WorkspaceGitStashProvider> = backend.clone();
113 let git_worktree: Arc<dyn WorkspaceGitWorktreeProvider> = backend.clone();
114 Arc::new(Self {
115 workspace_ref,
116 capabilities: WorkspaceCapabilities::local_default(),
117 path_resolver,
118 file_system,
119 file_system_ext: None,
120 text_reader: Some(text_reader),
121 command_runner: Some(command_runner),
122 search: Some(search),
123 code_intelligence: None,
124 git: Some(git),
125 git_stash: Some(git_stash),
126 git_worktree: Some(git_worktree),
127 operation_timeout: None,
128 local_root: Some(backend.root.clone()),
129 })
130 }
131
132 pub fn local_with_manifest(root: impl Into<PathBuf>) -> Arc<Self> {
137 let backend = ManifestWorkspaceBackend::new(root);
138 Self::local_with_manifest_backend(backend)
139 }
140
141 pub async fn local_with_code_intelligence(
146 root: impl Into<PathBuf>,
147 isolation_scope: impl Into<String>,
148 ) -> Result<Arc<Self>> {
149 let backend = ManifestWorkspaceBackend::new(root);
150 Self::local_with_code_intelligence_backend(backend, isolation_scope).await
151 }
152
153 pub async fn local_with_code_intelligence_backend(
155 backend: Arc<ManifestWorkspaceBackend>,
156 isolation_scope: impl Into<String>,
157 ) -> Result<Arc<Self>> {
158 let manifest = backend.manifest();
159 let file_system: Arc<dyn WorkspaceFileSystem> = backend.clone();
160 let services = Self::local_with_manifest_backend(backend);
161 let provider = LocalCodeIntelligence::start(isolation_scope, manifest, file_system)
162 .await
163 .map_err(|error| anyhow!("failed to start Code Intelligence: {error}"))?;
164 Ok(services.with_code_intelligence(provider))
165 }
166
167 pub fn local_with_manifest_backend(backend: Arc<ManifestWorkspaceBackend>) -> Arc<Self> {
170 let workspace_ref = WorkspaceRef::new(
171 backend.local_root().display().to_string(),
172 backend.local_root().display().to_string(),
173 );
174 let path_resolver: Arc<dyn WorkspacePathResolver> = backend.clone();
175 let file_system: Arc<dyn WorkspaceFileSystem> = backend.clone();
176 let text_reader: Arc<dyn WorkspaceTextReader> = backend.clone();
177 let command_runner: Arc<dyn WorkspaceCommandRunner> = backend.clone();
178 let search: Arc<dyn WorkspaceSearch> = backend.clone();
179 let git: Arc<dyn WorkspaceGit> = backend.clone();
180 let git_stash: Arc<dyn WorkspaceGitStashProvider> = backend.clone();
181 let git_worktree: Arc<dyn WorkspaceGitWorktreeProvider> = backend.clone();
182 Arc::new(Self {
183 workspace_ref,
184 capabilities: WorkspaceCapabilities::local_default(),
185 path_resolver,
186 file_system,
187 file_system_ext: None,
188 text_reader: Some(text_reader),
189 command_runner: Some(command_runner),
190 search: Some(search),
191 code_intelligence: None,
192 git: Some(git),
193 git_stash: Some(git_stash),
194 git_worktree: Some(git_worktree),
195 operation_timeout: None,
196 local_root: Some(backend.local_root().to_path_buf()),
197 })
198 }
199
200 pub fn workspace_ref(&self) -> &WorkspaceRef {
201 &self.workspace_ref
202 }
203
204 pub fn capabilities(&self) -> WorkspaceCapabilities {
205 self.capabilities
206 }
207
208 pub fn normalize_path(&self, input: &str) -> Result<WorkspacePath> {
209 self.path_resolver.normalize(input)
210 }
211
212 pub fn fs(&self) -> Arc<dyn WorkspaceFileSystem> {
213 Arc::clone(&self.file_system)
214 }
215
216 pub fn fs_ext(&self) -> Option<Arc<dyn WorkspaceFileSystemExt>> {
223 self.file_system_ext.clone()
224 }
225
226 pub fn text_reader(&self) -> Option<Arc<dyn WorkspaceTextReader>> {
227 self.text_reader.clone()
228 }
229
230 pub fn command_runner(&self) -> Option<Arc<dyn WorkspaceCommandRunner>> {
231 self.command_runner.clone()
232 }
233
234 pub fn search(&self) -> Option<Arc<dyn WorkspaceSearch>> {
235 self.search.clone()
236 }
237
238 pub fn code_intelligence(&self) -> Option<Arc<dyn WorkspaceCodeIntelligence>> {
240 self.code_intelligence.clone()
241 }
242
243 pub fn with_code_intelligence(
246 &self,
247 provider: Arc<dyn WorkspaceCodeIntelligence>,
248 ) -> Arc<Self> {
249 let mut capabilities = self.capabilities;
250 capabilities.code_intelligence = true;
251 Arc::new(Self {
252 workspace_ref: self.workspace_ref.clone(),
253 capabilities,
254 path_resolver: Arc::clone(&self.path_resolver),
255 file_system: Arc::clone(&self.file_system),
256 file_system_ext: self.file_system_ext.clone(),
257 text_reader: self.text_reader.clone(),
258 command_runner: self.command_runner.clone(),
259 search: self.search.clone(),
260 code_intelligence: Some(provider),
261 git: self.git.clone(),
262 git_stash: self.git_stash.clone(),
263 git_worktree: self.git_worktree.clone(),
264 operation_timeout: self.operation_timeout,
265 local_root: self.local_root.clone(),
266 })
267 }
268
269 pub fn git(&self) -> Option<Arc<dyn WorkspaceGit>> {
270 self.git.clone()
271 }
272
273 pub fn git_stash(&self) -> Option<Arc<dyn WorkspaceGitStashProvider>> {
274 self.git_stash.clone()
275 }
276
277 pub fn git_worktree(&self) -> Option<Arc<dyn WorkspaceGitWorktreeProvider>> {
278 self.git_worktree.clone()
279 }
280
281 pub(crate) fn with_git_provider(
298 &self,
299 git: Arc<dyn WorkspaceGit>,
300 git_stash: Option<Arc<dyn WorkspaceGitStashProvider>>,
301 ) -> Arc<Self> {
302 let mut capabilities = self.capabilities;
303 capabilities.git = true;
304 Arc::new(Self {
305 workspace_ref: self.workspace_ref.clone(),
306 capabilities,
307 path_resolver: Arc::clone(&self.path_resolver),
308 file_system: Arc::clone(&self.file_system),
309 file_system_ext: self.file_system_ext.clone(),
310 text_reader: self.text_reader.clone(),
311 command_runner: self.command_runner.clone(),
312 search: self.search.clone(),
313 code_intelligence: self.code_intelligence.clone(),
314 git: Some(git),
315 git_stash,
316 git_worktree: None,
317 operation_timeout: self.operation_timeout,
318 local_root: self.local_root.clone(),
319 })
320 }
321
322 pub fn operation_timeout(&self) -> Option<std::time::Duration> {
328 self.operation_timeout
329 }
330
331 pub async fn run_with_timeout<F, T, E>(
345 &self,
346 op: &'static str,
347 fut: F,
348 ) -> std::result::Result<T, E>
349 where
350 F: std::future::Future<Output = std::result::Result<T, E>>,
351 E: From<anyhow::Error>,
352 {
353 match self.operation_timeout {
354 Some(d) => tokio::time::timeout(d, fut).await.map_err(|_| {
355 E::from(anyhow!(
356 "workspace operation '{}' timed out after {:?}",
357 op,
358 d
359 ))
360 })?,
361 None => fut.await,
362 }
363 }
364
365 pub async fn read_for_edit(
372 &self,
373 path: &WorkspacePath,
374 ) -> WorkspaceResult<(String, Option<String>)> {
375 if let Some(ext) = self.fs_ext() {
376 let path = path.clone();
377 return self
378 .run_with_timeout("read_text_with_version", async move {
379 let (content, version) = ext.read_text_with_version(&path).await?;
380 Ok((content, Some(version)))
381 })
382 .await;
383 }
384 let fs = self.fs();
385 let path_owned = path.clone();
386 let content = self
387 .run_with_timeout("read_text", async move { fs.read_text(&path_owned).await })
388 .await?;
389 Ok((content, None))
390 }
391
392 pub async fn write_for_edit(
400 &self,
401 path: &WorkspacePath,
402 content: &str,
403 expected_version: Option<&str>,
404 ) -> WorkspaceResult<WorkspaceWriteOutcome> {
405 if let (Some(ext), Some(version)) = (self.fs_ext(), expected_version) {
406 let path = path.clone();
407 let content = content.to_string();
408 let expected = version.to_string();
409 return self
410 .run_with_timeout("write_text_if_version", async move {
411 ext.write_text_if_version(&path, &content, &expected).await
412 })
413 .await;
414 }
415 let fs = self.fs();
416 let path = path.clone();
417 let content = content.to_string();
418 self.run_with_timeout(
419 "write_text",
420 async move { fs.write_text(&path, &content).await },
421 )
422 .await
423 }
424
425 pub fn local_root(&self) -> Option<&Path> {
426 self.local_root.as_deref()
427 }
428
429 pub fn display_path(&self, path: &WorkspacePath) -> String {
430 if path.is_root() {
431 return self.workspace_ref.display_root.clone();
432 }
433
434 let root = self.workspace_ref.display_root.trim_end_matches('/');
435 if root.is_empty() {
436 path.as_str().to_string()
437 } else {
438 format!("{root}/{}", path.as_str())
439 }
440 }
441}
442
443pub struct WorkspaceServicesBuilder {
445 workspace_ref: WorkspaceRef,
446 capabilities: WorkspaceCapabilities,
447 path_resolver: Arc<dyn WorkspacePathResolver>,
448 file_system: Arc<dyn WorkspaceFileSystem>,
449 file_system_ext: Option<Arc<dyn WorkspaceFileSystemExt>>,
450 text_reader: Option<Arc<dyn WorkspaceTextReader>>,
451 command_runner: Option<Arc<dyn WorkspaceCommandRunner>>,
452 search: Option<Arc<dyn WorkspaceSearch>>,
453 code_intelligence: Option<Arc<dyn WorkspaceCodeIntelligence>>,
454 git: Option<Arc<dyn WorkspaceGit>>,
455 git_stash: Option<Arc<dyn WorkspaceGitStashProvider>>,
456 git_worktree: Option<Arc<dyn WorkspaceGitWorktreeProvider>>,
457 operation_timeout: Option<std::time::Duration>,
458}
459
460impl WorkspaceServicesBuilder {
461 pub fn new(workspace_ref: WorkspaceRef, file_system: Arc<dyn WorkspaceFileSystem>) -> Self {
462 Self {
463 workspace_ref,
464 capabilities: WorkspaceCapabilities::read_write(),
465 path_resolver: Arc::new(VirtualPathResolver),
466 file_system,
467 file_system_ext: None,
468 text_reader: None,
469 command_runner: None,
470 search: None,
471 code_intelligence: None,
472 git: None,
473 git_stash: None,
474 git_worktree: None,
475 operation_timeout: None,
476 }
477 }
478
479 pub fn capabilities(mut self, capabilities: WorkspaceCapabilities) -> Self {
480 self.capabilities = capabilities;
481 self
482 }
483
484 pub fn command_runner(mut self, command_runner: Arc<dyn WorkspaceCommandRunner>) -> Self {
485 self.capabilities.exec = true;
486 self.command_runner = Some(command_runner);
487 self
488 }
489
490 pub fn search(mut self, search: Arc<dyn WorkspaceSearch>) -> Self {
491 self.capabilities.search = true;
492 self.search = Some(search);
493 self
494 }
495
496 pub fn code_intelligence(mut self, provider: Arc<dyn WorkspaceCodeIntelligence>) -> Self {
497 self.capabilities.code_intelligence = true;
498 self.code_intelligence = Some(provider);
499 self
500 }
501
502 pub fn git(mut self, git: Arc<dyn WorkspaceGit>) -> Self {
503 self.capabilities.git = true;
504 self.git = Some(git);
505 self
506 }
507
508 pub fn git_stash(mut self, git_stash: Arc<dyn WorkspaceGitStashProvider>) -> Self {
509 self.git_stash = Some(git_stash);
510 self
511 }
512
513 pub fn git_worktree(mut self, git_worktree: Arc<dyn WorkspaceGitWorktreeProvider>) -> Self {
514 self.git_worktree = Some(git_worktree);
515 self
516 }
517
518 pub fn file_system_ext(mut self, ext: Arc<dyn WorkspaceFileSystemExt>) -> Self {
523 self.file_system_ext = Some(ext);
524 self
525 }
526
527 pub fn text_reader(mut self, reader: Arc<dyn WorkspaceTextReader>) -> Self {
528 self.text_reader = Some(reader);
529 self
530 }
531
532 pub fn operation_timeout(mut self, timeout: std::time::Duration) -> Self {
536 self.operation_timeout = Some(timeout);
537 self
538 }
539
540 pub fn build(self) -> Arc<WorkspaceServices> {
541 let mut services = WorkspaceServices::new_with_git(
542 self.workspace_ref,
543 self.capabilities,
544 self.path_resolver,
545 self.file_system,
546 self.command_runner,
547 self.search,
548 self.git,
549 );
550 services.file_system_ext = self.file_system_ext;
551 services.text_reader = self.text_reader;
552 services.capabilities.code_intelligence = self.code_intelligence.is_some();
553 services.code_intelligence = self.code_intelligence;
554 services.git_stash = self.git_stash;
555 services.git_worktree = self.git_worktree;
556 services.operation_timeout = self.operation_timeout;
557 Arc::new(services)
558 }
559}