1#[allow(unused_imports)]
4use super::{CommandRequest, WorkspaceError, WorkspaceVersionConflict};
5use super::{
6 DirectWriteGuard, LocalWorkspaceBackend, ManifestWorkspaceBackend, VirtualPathResolver,
7 WorkspaceCapabilities, WorkspaceChunkCatalog, WorkspaceCommandRunner, WorkspaceFileSystem,
8 WorkspaceFileSystemExt, WorkspaceGit, WorkspaceGitStashProvider, WorkspaceGitWorktreeProvider,
9 WorkspacePath, WorkspacePathResolver, WorkspacePersistentIndex, WorkspaceRef, WorkspaceResult,
10 WorkspaceRetrievalRuntime, WorkspaceSearch, WorkspaceTextReader, WorkspaceWriteOutcome,
11};
12use crate::code_intelligence::{LocalCodeIntelligence, WorkspaceCodeIntelligence};
13use anyhow::{anyhow, Result};
14use std::path::{Path, PathBuf};
15use std::sync::{Arc, OnceLock};
16
17type LazyLexicalHandles = (
19 Arc<WorkspaceChunkCatalog>,
20 Option<Arc<WorkspacePersistentIndex>>,
21);
22
23pub struct WorkspaceServices {
25 workspace_ref: WorkspaceRef,
26 capabilities: WorkspaceCapabilities,
27 path_resolver: Arc<dyn WorkspacePathResolver>,
28 file_system: Arc<dyn WorkspaceFileSystem>,
29 file_system_ext: Option<Arc<dyn WorkspaceFileSystemExt>>,
30 text_reader: Option<Arc<dyn WorkspaceTextReader>>,
31 command_runner: Option<Arc<dyn WorkspaceCommandRunner>>,
32 search: Option<Arc<dyn WorkspaceSearch>>,
33 code_intelligence: Option<Arc<dyn WorkspaceCodeIntelligence>>,
34 chunk_catalog: Option<Arc<WorkspaceChunkCatalog>>,
35 persistent_index: Option<Arc<WorkspacePersistentIndex>>,
36 lazy_lexical_backend: Option<Arc<ManifestWorkspaceBackend>>,
40 lazy_lexical: Arc<OnceLock<LazyLexicalHandles>>,
41 workspace_retrieval: Option<Arc<WorkspaceRetrievalRuntime>>,
42 git: Option<Arc<dyn WorkspaceGit>>,
43 git_stash: Option<Arc<dyn WorkspaceGitStashProvider>>,
44 git_worktree: Option<Arc<dyn WorkspaceGitWorktreeProvider>>,
45 operation_timeout: Option<std::time::Duration>,
49 local_root: Option<PathBuf>,
50 direct_write_guard: Option<Arc<dyn DirectWriteGuard>>,
51}
52
53impl std::fmt::Debug for WorkspaceServices {
54 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
55 f.debug_struct("WorkspaceServices")
56 .field("workspace_ref", &self.workspace_ref)
57 .field("capabilities", &self.capabilities)
58 .field("file_system_ext", &self.file_system_ext.is_some())
59 .field("text_reader", &self.text_reader.is_some())
60 .field("command_runner", &self.command_runner.is_some())
61 .field("search", &self.search.is_some())
62 .field("code_intelligence", &self.code_intelligence.is_some())
63 .field("chunk_catalog", &self.chunk_catalog.is_some())
64 .field("persistent_index", &self.persistent_index.is_some())
65 .field("workspace_retrieval", &self.workspace_retrieval.is_some())
66 .field("git", &self.git.is_some())
67 .field("git_stash", &self.git_stash.is_some())
68 .field("git_worktree", &self.git_worktree.is_some())
69 .field("local_root", &self.local_root)
70 .finish()
71 }
72}
73
74impl WorkspaceServices {
75 pub(crate) fn new_with_git(
76 workspace_ref: WorkspaceRef,
77 mut capabilities: WorkspaceCapabilities,
78 path_resolver: Arc<dyn WorkspacePathResolver>,
79 file_system: Arc<dyn WorkspaceFileSystem>,
80 command_runner: Option<Arc<dyn WorkspaceCommandRunner>>,
81 search: Option<Arc<dyn WorkspaceSearch>>,
82 git: Option<Arc<dyn WorkspaceGit>>,
83 ) -> Self {
84 if command_runner.is_none() {
85 capabilities.exec = false;
86 }
87 if search.is_none() {
88 capabilities.search = false;
89 }
90 if git.is_none() {
91 capabilities.git = false;
92 }
93 capabilities.code_intelligence = false;
94 Self {
95 workspace_ref,
96 capabilities,
97 path_resolver,
98 file_system,
99 file_system_ext: None,
100 text_reader: None,
101 command_runner,
102 search,
103 code_intelligence: None,
104 chunk_catalog: None,
105 persistent_index: None,
106 lazy_lexical_backend: None,
107 lazy_lexical: Arc::new(OnceLock::new()),
108 workspace_retrieval: None,
109 git,
110 git_stash: None,
111 git_worktree: None,
112 operation_timeout: None,
113 local_root: None,
114 direct_write_guard: None,
115 }
116 }
117
118 pub fn builder(
119 workspace_ref: WorkspaceRef,
120 file_system: Arc<dyn WorkspaceFileSystem>,
121 ) -> WorkspaceServicesBuilder {
122 WorkspaceServicesBuilder::new(workspace_ref, file_system)
123 }
124
125 pub fn local(root: impl Into<PathBuf>) -> Arc<Self> {
126 let backend = Arc::new(LocalWorkspaceBackend::new(root.into()));
127 let workspace_ref = WorkspaceRef::new(
128 backend.root.display().to_string(),
129 backend.root.display().to_string(),
130 );
131 let path_resolver: Arc<dyn WorkspacePathResolver> = backend.clone();
132 let file_system: Arc<dyn WorkspaceFileSystem> = backend.clone();
133 let text_reader: Arc<dyn WorkspaceTextReader> = backend.clone();
134 let command_runner: Arc<dyn WorkspaceCommandRunner> = backend.clone();
135 let search: Arc<dyn WorkspaceSearch> = backend.clone();
136 let git: Arc<dyn WorkspaceGit> = backend.clone();
137 let git_stash: Arc<dyn WorkspaceGitStashProvider> = backend.clone();
138 let git_worktree: Arc<dyn WorkspaceGitWorktreeProvider> = backend.clone();
139 Arc::new(Self {
140 workspace_ref,
141 capabilities: WorkspaceCapabilities::local_default(),
142 path_resolver,
143 file_system,
144 file_system_ext: None,
145 text_reader: Some(text_reader),
146 command_runner: Some(command_runner),
147 search: Some(search),
148 code_intelligence: None,
149 chunk_catalog: None,
150 persistent_index: None,
151 lazy_lexical_backend: None,
152 lazy_lexical: Arc::new(OnceLock::new()),
153 workspace_retrieval: None,
154 git: Some(git),
155 git_stash: Some(git_stash),
156 git_worktree: Some(git_worktree),
157 operation_timeout: None,
158 local_root: Some(backend.root.clone()),
159 direct_write_guard: Some(backend),
160 })
161 }
162
163 pub fn local_with_manifest(root: impl Into<PathBuf>) -> Arc<Self> {
168 let backend = ManifestWorkspaceBackend::new(root);
169 Self::local_with_manifest_backend(backend)
170 }
171
172 pub fn local_with_retrieval(root: impl Into<PathBuf>) -> Arc<Self> {
175 let backend = ManifestWorkspaceBackend::new(root);
176 Self::local_with_retrieval_backend(backend)
177 }
178
179 pub async fn local_with_code_intelligence(
184 root: impl Into<PathBuf>,
185 isolation_scope: impl Into<String>,
186 ) -> Result<Arc<Self>> {
187 let backend = ManifestWorkspaceBackend::new(root);
188 Self::local_with_code_intelligence_backend(backend, isolation_scope).await
189 }
190
191 pub async fn local_with_retrieval_and_code_intelligence(
194 root: impl Into<PathBuf>,
195 isolation_scope: impl Into<String>,
196 ) -> Result<Arc<Self>> {
197 let backend = ManifestWorkspaceBackend::new(root);
198 Self::local_with_retrieval_and_code_intelligence_backend(backend, isolation_scope).await
199 }
200
201 pub async fn local_with_code_intelligence_backend(
203 backend: Arc<ManifestWorkspaceBackend>,
204 isolation_scope: impl Into<String>,
205 ) -> Result<Arc<Self>> {
206 let manifest = backend.manifest();
207 let file_system: Arc<dyn WorkspaceFileSystem> = backend.clone();
208 let services = Self::local_with_manifest_backend(backend);
209 let provider = LocalCodeIntelligence::start(isolation_scope, manifest, file_system)
210 .await
211 .map_err(|error| anyhow!("failed to start Code Intelligence: {error}"))?;
212 Ok(services.with_code_intelligence(provider))
213 }
214
215 pub async fn local_with_retrieval_and_code_intelligence_backend(
217 backend: Arc<ManifestWorkspaceBackend>,
218 isolation_scope: impl Into<String>,
219 ) -> Result<Arc<Self>> {
220 let manifest = backend.manifest();
221 let file_system: Arc<dyn WorkspaceFileSystem> = backend.clone();
222 let services = Self::local_with_retrieval_backend(backend);
223 let provider = LocalCodeIntelligence::start(isolation_scope, manifest, file_system)
224 .await
225 .map_err(|error| anyhow!("failed to start Code Intelligence: {error}"))?;
226 Ok(services.with_code_intelligence(provider))
227 }
228
229 pub fn local_with_manifest_backend(backend: Arc<ManifestWorkspaceBackend>) -> Arc<Self> {
232 Self::local_with_manifest_backend_and_catalog(backend, None, None, None)
233 }
234
235 pub fn local_with_retrieval_backend(backend: Arc<ManifestWorkspaceBackend>) -> Arc<Self> {
243 if backend.catalog_is_configured() {
244 let catalog = backend.chunk_catalog();
245 let persistent = backend.persistent_index();
246 Self::local_with_manifest_backend_and_catalog(
247 backend.clone(),
248 Some(catalog),
249 persistent,
250 Some(backend),
251 )
252 } else {
253 Self::local_with_manifest_backend_and_catalog(
254 backend.clone(),
255 None,
256 None,
257 Some(backend),
258 )
259 }
260 }
261
262 pub fn local_with_indexed_retrieval(root: impl Into<PathBuf>) -> Result<Arc<Self>> {
266 let backend = ManifestWorkspaceBackend::new(root);
267 let index_root = backend.local_root().join(".a3s-code").join("index");
268 let persistent = backend
269 .configure_persistent_index(index_root)
270 .map_err(|error| anyhow!("failed to configure persistent workspace index: {error}"))?;
271 let catalog = backend.chunk_catalog();
272 Ok(Self::local_with_manifest_backend_and_catalog(
273 backend,
274 Some(catalog),
275 Some(persistent),
276 None,
277 ))
278 }
279
280 fn local_with_manifest_backend_and_catalog(
281 backend: Arc<ManifestWorkspaceBackend>,
282 chunk_catalog: Option<Arc<WorkspaceChunkCatalog>>,
283 persistent_index: Option<Arc<WorkspacePersistentIndex>>,
284 lazy_lexical_backend: Option<Arc<ManifestWorkspaceBackend>>,
285 ) -> Arc<Self> {
286 let workspace_ref = WorkspaceRef::new(
287 backend.local_root().display().to_string(),
288 backend.local_root().display().to_string(),
289 );
290 let path_resolver: Arc<dyn WorkspacePathResolver> = backend.clone();
291 let file_system: Arc<dyn WorkspaceFileSystem> = backend.clone();
292 let text_reader: Arc<dyn WorkspaceTextReader> = backend.clone();
293 let command_runner: Arc<dyn WorkspaceCommandRunner> = backend.clone();
294 let search: Arc<dyn WorkspaceSearch> = backend.clone();
295 let git: Arc<dyn WorkspaceGit> = backend.clone();
296 let git_stash: Arc<dyn WorkspaceGitStashProvider> = backend.clone();
297 let git_worktree: Arc<dyn WorkspaceGitWorktreeProvider> = backend.clone();
298 Arc::new(Self {
299 workspace_ref,
300 capabilities: WorkspaceCapabilities::local_default(),
301 path_resolver,
302 file_system,
303 file_system_ext: None,
304 text_reader: Some(text_reader),
305 command_runner: Some(command_runner),
306 search: Some(search),
307 code_intelligence: None,
308 chunk_catalog,
309 persistent_index,
310 lazy_lexical_backend,
311 lazy_lexical: Arc::new(OnceLock::new()),
312 workspace_retrieval: None,
313 git: Some(git),
314 git_stash: Some(git_stash),
315 git_worktree: Some(git_worktree),
316 operation_timeout: None,
317 local_root: Some(backend.local_root().to_path_buf()),
318 direct_write_guard: Some(backend),
319 })
320 }
321
322 pub fn workspace_ref(&self) -> &WorkspaceRef {
323 &self.workspace_ref
324 }
325
326 pub fn capabilities(&self) -> WorkspaceCapabilities {
327 self.capabilities
328 }
329
330 pub fn normalize_path(&self, input: &str) -> Result<WorkspacePath> {
331 self.path_resolver.normalize(input)
332 }
333
334 pub fn fs(&self) -> Arc<dyn WorkspaceFileSystem> {
335 Arc::clone(&self.file_system)
336 }
337
338 pub fn fs_ext(&self) -> Option<Arc<dyn WorkspaceFileSystemExt>> {
345 self.file_system_ext.clone()
346 }
347
348 pub fn text_reader(&self) -> Option<Arc<dyn WorkspaceTextReader>> {
349 self.text_reader.clone()
350 }
351
352 pub fn command_runner(&self) -> Option<Arc<dyn WorkspaceCommandRunner>> {
353 self.command_runner.clone()
354 }
355
356 pub fn search(&self) -> Option<Arc<dyn WorkspaceSearch>> {
357 self.search.clone()
358 }
359
360 pub fn code_intelligence(&self) -> Option<Arc<dyn WorkspaceCodeIntelligence>> {
362 self.code_intelligence.clone()
363 }
364
365 pub fn chunk_catalog(&self) -> Option<Arc<WorkspaceChunkCatalog>> {
367 if let Some(catalog) = &self.chunk_catalog {
368 return Some(Arc::clone(catalog));
369 }
370 self.ensure_lazy_lexical()
371 .map(|(catalog, _)| Arc::clone(catalog))
372 }
373
374 pub fn persistent_index(&self) -> Option<Arc<WorkspacePersistentIndex>> {
375 if let Some(index) = &self.persistent_index {
376 return Some(Arc::clone(index));
377 }
378 if let Some(index) = self
384 .lazy_lexical_backend
385 .as_ref()
386 .and_then(|backend| backend.ensure_persistent_index())
387 {
388 let _ = self.ensure_lazy_lexical();
389 return Some(index);
390 }
391 if let Some(handles) = self.ensure_lazy_lexical() {
392 if let Some(index) = &handles.1 {
393 return Some(Arc::clone(index));
394 }
395 }
396 None
397 }
398
399 pub fn open_persistent_index_without_catalog(&self) -> Option<Arc<WorkspacePersistentIndex>> {
406 if let Some(index) = &self.persistent_index {
407 return Some(Arc::clone(index));
408 }
409 if let Some(handles) = self.lazy_lexical.get() {
410 if let Some(index) = &handles.1 {
411 return Some(Arc::clone(index));
412 }
413 }
414 self.lazy_lexical_backend
415 .as_ref()
416 .and_then(|backend| backend.ensure_persistent_index())
417 }
418
419 pub fn chunk_catalog_started(&self) -> bool {
421 self.chunk_catalog.is_some() || self.lazy_lexical.get().is_some()
422 }
423
424 fn ensure_lazy_lexical(&self) -> Option<&LazyLexicalHandles> {
425 let backend = self.lazy_lexical_backend.as_ref()?;
426 Some(self.lazy_lexical.get_or_init(|| {
427 let catalog = backend.chunk_catalog();
428 let persistent = backend.persistent_index();
430 (catalog, persistent)
431 }))
432 }
433
434 pub fn workspace_retrieval(&self) -> Option<Arc<WorkspaceRetrievalRuntime>> {
436 self.workspace_retrieval.clone()
437 }
438
439 pub async fn semantic_search(
442 &self,
443 mut request: super::WorkspaceSemanticSearchRequest,
444 cancellation: tokio_util::sync::CancellationToken,
445 ) -> super::WorkspaceRetrievalResult<super::WorkspaceSemanticSearchResult> {
446 if !self.capabilities.read {
447 return Err(super::WorkspaceRetrievalError::Unavailable);
448 }
449 if let Some(path) = request.path.take() {
450 request.path = Some(
451 self.path_resolver
452 .normalize(&path)
453 .map_err(|_| {
454 super::WorkspaceRetrievalError::InvalidQuery(
455 "path was rejected by the workspace resolver".to_owned(),
456 )
457 })?
458 .as_str()
459 .to_owned(),
460 );
461 }
462 let runtime = self
463 .workspace_retrieval
464 .as_ref()
465 .ok_or(super::WorkspaceRetrievalError::Unavailable)?;
466 runtime
467 .search(
468 request,
469 Arc::clone(&self.file_system),
470 self.operation_timeout,
471 cancellation,
472 )
473 .await
474 }
475
476 pub async fn hybrid_search(
479 &self,
480 mut request: super::WorkspaceHybridSearchRequest,
481 cancellation: tokio_util::sync::CancellationToken,
482 ) -> super::WorkspaceRetrievalResult<super::WorkspaceHybridSearchResult> {
483 if !self.capabilities.read {
484 return Err(super::WorkspaceRetrievalError::Unavailable);
485 }
486 if let Some(path) = request.path.take() {
487 request.path = Some(
488 self.path_resolver
489 .normalize(&path)
490 .map_err(|_| {
491 super::WorkspaceRetrievalError::InvalidQuery(
492 "path was rejected by the workspace resolver".to_owned(),
493 )
494 })?
495 .as_str()
496 .to_owned(),
497 );
498 }
499 let runtime = self
500 .workspace_retrieval
501 .as_ref()
502 .ok_or(super::WorkspaceRetrievalError::Unavailable)?;
503 runtime
504 .hybrid_search(
505 request,
506 Arc::clone(&self.file_system),
507 self.code_intelligence.clone(),
508 self.operation_timeout,
509 cancellation,
510 )
511 .await
512 }
513
514 pub(crate) fn with_workspace_retrieval(
515 &self,
516 runtime: Arc<WorkspaceRetrievalRuntime>,
517 ) -> Option<Arc<Self>> {
518 if self.workspace_retrieval.is_some() {
519 return None;
520 }
521 Some(Arc::new(Self {
522 workspace_ref: self.workspace_ref.clone(),
523 capabilities: self.capabilities,
524 path_resolver: Arc::clone(&self.path_resolver),
525 file_system: Arc::clone(&self.file_system),
526 file_system_ext: self.file_system_ext.clone(),
527 text_reader: self.text_reader.clone(),
528 command_runner: self.command_runner.clone(),
529 search: self.search.clone(),
530 code_intelligence: self.code_intelligence.clone(),
531 chunk_catalog: self.chunk_catalog.clone(),
532 persistent_index: self.persistent_index.clone(),
533 lazy_lexical_backend: self.lazy_lexical_backend.clone(),
534 lazy_lexical: Arc::clone(&self.lazy_lexical),
535 workspace_retrieval: Some(runtime),
536 git: self.git.clone(),
537 git_stash: self.git_stash.clone(),
538 git_worktree: self.git_worktree.clone(),
539 operation_timeout: self.operation_timeout,
540 local_root: self.local_root.clone(),
541 direct_write_guard: self.direct_write_guard.clone(),
542 }))
543 }
544
545 pub fn with_code_intelligence(
548 &self,
549 provider: Arc<dyn WorkspaceCodeIntelligence>,
550 ) -> Arc<Self> {
551 let mut capabilities = self.capabilities;
552 capabilities.code_intelligence = true;
553 Arc::new(Self {
554 workspace_ref: self.workspace_ref.clone(),
555 capabilities,
556 path_resolver: Arc::clone(&self.path_resolver),
557 file_system: Arc::clone(&self.file_system),
558 file_system_ext: self.file_system_ext.clone(),
559 text_reader: self.text_reader.clone(),
560 command_runner: self.command_runner.clone(),
561 search: self.search.clone(),
562 code_intelligence: Some(provider),
563 chunk_catalog: self.chunk_catalog.clone(),
564 persistent_index: self.persistent_index.clone(),
565 lazy_lexical_backend: self.lazy_lexical_backend.clone(),
566 lazy_lexical: Arc::clone(&self.lazy_lexical),
567 workspace_retrieval: self.workspace_retrieval.clone(),
568 git: self.git.clone(),
569 git_stash: self.git_stash.clone(),
570 git_worktree: self.git_worktree.clone(),
571 operation_timeout: self.operation_timeout,
572 local_root: self.local_root.clone(),
573 direct_write_guard: self.direct_write_guard.clone(),
574 })
575 }
576
577 pub fn git(&self) -> Option<Arc<dyn WorkspaceGit>> {
578 self.git.clone()
579 }
580
581 pub fn git_stash(&self) -> Option<Arc<dyn WorkspaceGitStashProvider>> {
582 self.git_stash.clone()
583 }
584
585 pub fn git_worktree(&self) -> Option<Arc<dyn WorkspaceGitWorktreeProvider>> {
586 self.git_worktree.clone()
587 }
588
589 pub(crate) fn with_git_provider(
606 &self,
607 git: Arc<dyn WorkspaceGit>,
608 git_stash: Option<Arc<dyn WorkspaceGitStashProvider>>,
609 ) -> Arc<Self> {
610 let mut capabilities = self.capabilities;
611 capabilities.git = true;
612 Arc::new(Self {
613 workspace_ref: self.workspace_ref.clone(),
614 capabilities,
615 path_resolver: Arc::clone(&self.path_resolver),
616 file_system: Arc::clone(&self.file_system),
617 file_system_ext: self.file_system_ext.clone(),
618 text_reader: self.text_reader.clone(),
619 command_runner: self.command_runner.clone(),
620 search: self.search.clone(),
621 code_intelligence: self.code_intelligence.clone(),
622 chunk_catalog: self.chunk_catalog.clone(),
623 persistent_index: self.persistent_index.clone(),
624 lazy_lexical_backend: self.lazy_lexical_backend.clone(),
625 lazy_lexical: Arc::clone(&self.lazy_lexical),
626 workspace_retrieval: self.workspace_retrieval.clone(),
627 git: Some(git),
628 git_stash,
629 git_worktree: None,
630 operation_timeout: self.operation_timeout,
631 local_root: self.local_root.clone(),
632 direct_write_guard: self.direct_write_guard.clone(),
633 })
634 }
635
636 pub fn refuse_direct_write(&self, path: &WorkspacePath) -> Result<()> {
642 match &self.direct_write_guard {
643 Some(guard) => guard.refuse_direct_write(path),
644 None => Ok(()),
645 }
646 }
647
648 pub fn operation_timeout(&self) -> Option<std::time::Duration> {
654 self.operation_timeout
655 }
656
657 pub async fn run_with_timeout<F, T, E>(
671 &self,
672 op: &'static str,
673 fut: F,
674 ) -> std::result::Result<T, E>
675 where
676 F: std::future::Future<Output = std::result::Result<T, E>>,
677 E: From<anyhow::Error>,
678 {
679 match self.operation_timeout {
680 Some(d) => tokio::time::timeout(d, fut).await.map_err(|_| {
681 E::from(anyhow!(
682 "workspace operation '{}' timed out after {:?}",
683 op,
684 d
685 ))
686 })?,
687 None => fut.await,
688 }
689 }
690
691 pub async fn read_for_edit(
698 &self,
699 path: &WorkspacePath,
700 ) -> WorkspaceResult<(String, Option<String>)> {
701 if let Some(ext) = self.fs_ext() {
702 let path = path.clone();
703 return self
704 .run_with_timeout("read_text_with_version", async move {
705 let (content, version) = ext.read_text_with_version(&path).await?;
706 Ok((content, Some(version)))
707 })
708 .await;
709 }
710 let fs = self.fs();
711 let path_owned = path.clone();
712 let content = self
713 .run_with_timeout("read_text", async move { fs.read_text(&path_owned).await })
714 .await?;
715 Ok((content, None))
716 }
717
718 pub async fn write_for_edit(
726 &self,
727 path: &WorkspacePath,
728 content: &str,
729 expected_version: Option<&str>,
730 ) -> WorkspaceResult<WorkspaceWriteOutcome> {
731 if let (Some(ext), Some(version)) = (self.fs_ext(), expected_version) {
732 let path = path.clone();
733 let content = content.to_string();
734 let expected = version.to_string();
735 return self
736 .run_with_timeout("write_text_if_version", async move {
737 ext.write_text_if_version(&path, &content, &expected).await
738 })
739 .await;
740 }
741 let fs = self.fs();
742 let path = path.clone();
743 let content = content.to_string();
744 self.run_with_timeout(
745 "write_text",
746 async move { fs.write_text(&path, &content).await },
747 )
748 .await
749 }
750
751 pub fn local_root(&self) -> Option<&Path> {
752 self.local_root.as_deref()
753 }
754
755 pub fn display_path(&self, path: &WorkspacePath) -> String {
756 if path.is_root() {
757 return self.workspace_ref.display_root.clone();
758 }
759
760 let root = self.workspace_ref.display_root.trim_end_matches('/');
761 if root.is_empty() {
762 path.as_str().to_string()
763 } else {
764 format!("{root}/{}", path.as_str())
765 }
766 }
767}
768
769pub struct WorkspaceServicesBuilder {
771 workspace_ref: WorkspaceRef,
772 capabilities: WorkspaceCapabilities,
773 path_resolver: Arc<dyn WorkspacePathResolver>,
774 file_system: Arc<dyn WorkspaceFileSystem>,
775 file_system_ext: Option<Arc<dyn WorkspaceFileSystemExt>>,
776 text_reader: Option<Arc<dyn WorkspaceTextReader>>,
777 command_runner: Option<Arc<dyn WorkspaceCommandRunner>>,
778 search: Option<Arc<dyn WorkspaceSearch>>,
779 code_intelligence: Option<Arc<dyn WorkspaceCodeIntelligence>>,
780 chunk_catalog: Option<Arc<WorkspaceChunkCatalog>>,
781 persistent_index: Option<Arc<WorkspacePersistentIndex>>,
782 git: Option<Arc<dyn WorkspaceGit>>,
783 git_stash: Option<Arc<dyn WorkspaceGitStashProvider>>,
784 git_worktree: Option<Arc<dyn WorkspaceGitWorktreeProvider>>,
785 operation_timeout: Option<std::time::Duration>,
786}
787
788impl WorkspaceServicesBuilder {
789 pub fn new(workspace_ref: WorkspaceRef, file_system: Arc<dyn WorkspaceFileSystem>) -> Self {
790 Self {
791 workspace_ref,
792 capabilities: WorkspaceCapabilities::read_write(),
793 path_resolver: Arc::new(VirtualPathResolver),
794 file_system,
795 file_system_ext: None,
796 text_reader: None,
797 command_runner: None,
798 search: None,
799 code_intelligence: None,
800 chunk_catalog: None,
801 persistent_index: None,
802 git: None,
803 git_stash: None,
804 git_worktree: None,
805 operation_timeout: None,
806 }
807 }
808
809 pub fn capabilities(mut self, capabilities: WorkspaceCapabilities) -> Self {
810 self.capabilities = capabilities;
811 self
812 }
813
814 pub fn command_runner(mut self, command_runner: Arc<dyn WorkspaceCommandRunner>) -> Self {
815 self.capabilities.exec = true;
816 self.command_runner = Some(command_runner);
817 self
818 }
819
820 pub fn search(mut self, search: Arc<dyn WorkspaceSearch>) -> Self {
821 self.capabilities.search = true;
822 self.search = Some(search);
823 self
824 }
825
826 pub fn code_intelligence(mut self, provider: Arc<dyn WorkspaceCodeIntelligence>) -> Self {
827 self.capabilities.code_intelligence = true;
828 self.code_intelligence = Some(provider);
829 self
830 }
831
832 pub fn chunk_catalog(mut self, catalog: Arc<WorkspaceChunkCatalog>) -> Self {
834 self.chunk_catalog = Some(catalog);
835 self
836 }
837
838 pub fn persistent_index(mut self, index: Arc<WorkspacePersistentIndex>) -> Self {
839 self.persistent_index = Some(index);
840 self
841 }
842
843 pub fn git(mut self, git: Arc<dyn WorkspaceGit>) -> Self {
844 self.capabilities.git = true;
845 self.git = Some(git);
846 self
847 }
848
849 pub fn git_stash(mut self, git_stash: Arc<dyn WorkspaceGitStashProvider>) -> Self {
850 self.git_stash = Some(git_stash);
851 self
852 }
853
854 pub fn git_worktree(mut self, git_worktree: Arc<dyn WorkspaceGitWorktreeProvider>) -> Self {
855 self.git_worktree = Some(git_worktree);
856 self
857 }
858
859 pub fn file_system_ext(mut self, ext: Arc<dyn WorkspaceFileSystemExt>) -> Self {
864 self.file_system_ext = Some(ext);
865 self
866 }
867
868 pub fn text_reader(mut self, reader: Arc<dyn WorkspaceTextReader>) -> Self {
869 self.text_reader = Some(reader);
870 self
871 }
872
873 pub fn operation_timeout(mut self, timeout: std::time::Duration) -> Self {
877 self.operation_timeout = Some(timeout);
878 self
879 }
880
881 pub fn build(self) -> Arc<WorkspaceServices> {
882 let mut services = WorkspaceServices::new_with_git(
883 self.workspace_ref,
884 self.capabilities,
885 self.path_resolver,
886 self.file_system,
887 self.command_runner,
888 self.search,
889 self.git,
890 );
891 services.file_system_ext = self.file_system_ext;
892 services.text_reader = self.text_reader;
893 services.capabilities.code_intelligence = self.code_intelligence.is_some();
894 services.code_intelligence = self.code_intelligence;
895 services.chunk_catalog = self.chunk_catalog;
896 services.persistent_index = self.persistent_index;
897 services.workspace_retrieval = None;
898 services.git_stash = self.git_stash;
899 services.git_worktree = self.git_worktree;
900 services.operation_timeout = self.operation_timeout;
901 Arc::new(services)
902 }
903}