1#[cfg(test)]
11pub(crate) mod conformance;
12mod error;
13mod local;
14mod manifest;
15mod path;
16mod remote_git;
17#[cfg(feature = "s3")]
18mod s3;
19
20pub use error::{WorkspaceError, WorkspaceResult};
21pub use local::LocalWorkspaceBackend;
22pub use manifest::{
23 scan_workspace_files, LocalWorkspaceFile, LocalWorkspaceFileStatus, LocalWorkspaceManifest,
24 LocalWorkspaceManifestSnapshot, ManifestWorkspaceBackend, RecentWorkspaceFile,
25};
26pub(crate) use path::validate_relative_pattern;
27pub use path::VirtualPathResolver;
28use path::{
29 default_path_input, escape_control_chars_for_display, has_windows_path_prefix,
30 normalize_relative_path, pathbuf_to_workspace_path,
31};
32pub use remote_git::{RemoteGitBackend, RemoteGitBackendConfig, RemoteGitConflict};
33#[cfg(feature = "s3")]
34pub use s3::{S3BackendConfig, S3WorkspaceBackend};
35
36use anyhow::{anyhow, Result};
37use async_trait::async_trait;
38use std::collections::HashMap;
39use std::path::{Path, PathBuf};
40use std::sync::Arc;
41
42#[derive(Debug, Clone, PartialEq, Eq)]
44pub struct WorkspaceRef {
45 pub id: String,
47 pub display_root: String,
49}
50
51impl WorkspaceRef {
52 pub fn new(id: impl Into<String>, display_root: impl Into<String>) -> Self {
53 Self {
54 id: id.into(),
55 display_root: display_root.into(),
56 }
57 }
58}
59
60#[derive(Debug, Clone, PartialEq, Eq, Hash)]
62pub struct WorkspacePath {
63 inner: String,
64}
65
66impl WorkspacePath {
67 pub fn root() -> Self {
68 Self {
69 inner: ".".to_string(),
70 }
71 }
72
73 pub fn from_normalized(path: impl Into<String>) -> Self {
74 let path = path.into();
75 let path = path.trim_matches('/');
76 if path.is_empty() || path == "." {
77 Self::root()
78 } else {
79 Self {
80 inner: path.replace('\\', "/"),
81 }
82 }
83 }
84
85 pub fn as_str(&self) -> &str {
86 &self.inner
87 }
88
89 pub fn is_root(&self) -> bool {
90 self.inner == "."
91 }
92}
93
94#[derive(Debug, Clone, Copy, PartialEq, Eq)]
100pub struct WorkspaceCapabilities {
101 pub read: bool,
102 pub write: bool,
103 pub exec: bool,
104 pub search: bool,
105 pub git: bool,
106}
107
108impl WorkspaceCapabilities {
109 pub fn local_default() -> Self {
110 Self {
111 read: true,
112 write: true,
113 exec: true,
114 search: true,
115 git: true,
116 }
117 }
118
119 pub fn read_write() -> Self {
120 Self {
121 read: true,
122 write: true,
123 exec: false,
124 search: false,
125 git: false,
126 }
127 }
128}
129
130impl Default for WorkspaceCapabilities {
131 fn default() -> Self {
132 Self::read_write()
133 }
134}
135
136#[derive(Debug, Clone, Copy, PartialEq, Eq)]
138pub enum WorkspaceFileType {
139 File,
140 Directory,
141 Symlink,
142 Unknown,
143}
144
145impl WorkspaceFileType {
146 pub fn as_tool_kind(self) -> &'static str {
147 match self {
148 Self::File => "file",
149 Self::Directory => "dir",
150 Self::Symlink => "link",
151 Self::Unknown => "unknown",
152 }
153 }
154}
155
156#[derive(Debug, Clone, PartialEq, Eq)]
158pub struct WorkspaceDirEntry {
159 pub name: String,
160 pub kind: WorkspaceFileType,
161 pub size: u64,
162}
163
164#[derive(Debug, Clone, PartialEq, Eq)]
166pub struct WorkspaceWriteOutcome {
167 pub bytes: usize,
168 pub lines: usize,
169}
170
171#[derive(Debug, Clone, PartialEq, Eq)]
173pub struct WorkspaceGlobRequest {
174 pub base: WorkspacePath,
175 pub pattern: String,
176}
177
178#[derive(Debug, Clone, PartialEq, Eq)]
180pub struct WorkspaceGlobResult {
181 pub matches: Vec<WorkspacePath>,
182}
183
184#[derive(Debug, Clone, PartialEq, Eq)]
186pub struct WorkspaceGrepRequest {
187 pub base: WorkspacePath,
188 pub pattern: String,
189 pub glob: Option<String>,
190 pub context_lines: usize,
191 pub case_insensitive: bool,
192 pub max_output_size: usize,
193}
194
195#[derive(Debug, Clone, PartialEq, Eq)]
197pub struct WorkspaceGrepResult {
198 pub output: String,
199 pub match_count: usize,
200 pub file_count: usize,
201 pub truncated: bool,
202}
203
204#[derive(Debug, Clone, PartialEq, Eq)]
206pub struct WorkspaceGrepOutcome {
207 pub result: WorkspaceGrepResult,
208 pub matched_paths: Option<Vec<WorkspacePath>>,
211}
212
213#[derive(Debug, Clone, PartialEq, Eq)]
215pub struct WorkspaceGitStatus {
216 pub branch: String,
217 pub commit: String,
218 pub is_worktree: bool,
219 pub is_dirty: bool,
220 pub dirty_count: usize,
221}
222
223#[derive(Debug, Clone, PartialEq, Eq)]
225pub struct WorkspaceGitCommit {
226 pub id: String,
227 pub message: String,
228 pub author: String,
229 pub date: String,
230}
231
232#[derive(Debug, Clone, PartialEq, Eq)]
234pub struct WorkspaceGitBranch {
235 pub name: String,
236 pub is_current: bool,
237}
238
239#[derive(Debug, Clone, PartialEq, Eq)]
241pub struct WorkspaceGitCreateBranchRequest {
242 pub name: String,
243 pub base: String,
244}
245
246#[derive(Debug, Clone, PartialEq, Eq)]
248pub struct WorkspaceGitCheckoutRequest {
249 pub refspec: String,
250 pub force: bool,
251}
252
253#[derive(Debug, Clone, PartialEq, Eq)]
255pub struct WorkspaceGitCheckoutOutput {
256 pub stdout: String,
257}
258
259#[derive(Debug, Clone, PartialEq, Eq)]
261pub struct WorkspaceGitDiffRequest {
262 pub target: Option<String>,
263}
264
265#[derive(Debug, Clone, PartialEq, Eq)]
267pub struct WorkspaceGitStash {
268 pub index: usize,
269 pub message: String,
270}
271
272#[derive(Debug, Clone, PartialEq, Eq)]
274pub struct WorkspaceGitStashRequest {
275 pub message: Option<String>,
276 pub include_untracked: bool,
277}
278
279#[derive(Debug, Clone, PartialEq, Eq)]
281pub struct WorkspaceGitRemote {
282 pub name: String,
283 pub url: String,
284 pub direction: String,
285}
286
287#[derive(Debug, Clone, PartialEq, Eq)]
289pub struct WorkspaceGitWorktree {
290 pub path: String,
291 pub branch: String,
292 pub is_bare: bool,
293 pub is_detached: bool,
294}
295
296#[derive(Debug, Clone, PartialEq, Eq)]
298pub struct WorkspaceGitCreateWorktreeRequest {
299 pub branch: String,
300 pub path: Option<String>,
301 pub new_branch: bool,
302}
303
304#[derive(Debug, Clone, PartialEq, Eq)]
306pub struct WorkspaceGitRemoveWorktreeRequest {
307 pub path: String,
308 pub force: bool,
309}
310
311#[derive(Debug, Clone, PartialEq, Eq)]
313pub struct WorkspaceGitWorktreeMutation {
314 pub path: String,
315 pub branch: Option<String>,
316}
317
318#[async_trait]
324pub trait CommandOutputObserver: Send + Sync {
325 async fn on_output_delta(&self, delta: &str);
326}
327
328#[derive(Clone)]
330pub struct CommandRequest {
331 pub command: String,
332 pub timeout_ms: u64,
333 pub output_observer: Option<Arc<dyn CommandOutputObserver>>,
334 pub env: Option<Arc<HashMap<String, String>>>,
335}
336
337impl std::fmt::Debug for CommandRequest {
338 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
339 f.debug_struct("CommandRequest")
340 .field("command", &self.command)
341 .field("timeout_ms", &self.timeout_ms)
342 .field("output_observer", &self.output_observer.is_some())
343 .field("env", &self.env.as_ref().map(|env| env.len()))
344 .finish()
345 }
346}
347
348#[derive(Debug, Clone, PartialEq, Eq)]
350pub struct CommandOutput {
351 pub output: String,
352 pub exit_code: i32,
353 pub timed_out: bool,
354}
355
356pub trait WorkspacePathResolver: Send + Sync {
358 fn normalize(&self, input: &str) -> Result<WorkspacePath>;
359}
360
361#[async_trait]
371pub trait WorkspaceFileSystem: Send + Sync {
372 async fn read_text(&self, path: &WorkspacePath) -> WorkspaceResult<String>;
373 async fn write_text(
374 &self,
375 path: &WorkspacePath,
376 content: &str,
377 ) -> WorkspaceResult<WorkspaceWriteOutcome>;
378 async fn list_dir(&self, path: &WorkspacePath) -> WorkspaceResult<Vec<WorkspaceDirEntry>>;
379}
380
381#[derive(Debug, Clone, thiserror::Error)]
388#[error(
389 "version conflict on {path}: expected version {expected:?}, found {actual:?} (file modified by another writer; re-read and retry)"
390)]
391pub struct WorkspaceVersionConflict {
392 pub path: String,
393 pub expected: String,
394 pub actual: Option<String>,
397}
398
399#[async_trait]
411pub trait WorkspaceFileSystemExt: Send + Sync {
412 async fn read_text_with_version(
417 &self,
418 path: &WorkspacePath,
419 ) -> WorkspaceResult<(String, String)>;
420
421 async fn write_text_if_version(
427 &self,
428 path: &WorkspacePath,
429 content: &str,
430 expected_version: &str,
431 ) -> WorkspaceResult<WorkspaceWriteOutcome>;
432}
433
434#[async_trait]
436pub trait WorkspaceCommandRunner: Send + Sync {
437 async fn exec(&self, request: CommandRequest) -> Result<CommandOutput>;
438}
439
440#[async_trait]
442pub trait WorkspaceSearch: Send + Sync {
443 async fn glob(&self, request: WorkspaceGlobRequest) -> Result<WorkspaceGlobResult>;
444 async fn grep(&self, request: WorkspaceGrepRequest) -> Result<WorkspaceGrepResult>;
445
446 async fn grep_with_sources(
451 &self,
452 request: WorkspaceGrepRequest,
453 ) -> Result<WorkspaceGrepOutcome> {
454 let result = self.grep(request).await?;
455 Ok(WorkspaceGrepOutcome {
456 result,
457 matched_paths: None,
458 })
459 }
460}
461
462#[async_trait]
468pub trait WorkspaceGit: Send + Sync {
469 async fn is_repository(&self) -> Result<bool>;
470 async fn status(&self) -> Result<WorkspaceGitStatus>;
471 async fn log(&self, max_count: usize) -> Result<Vec<WorkspaceGitCommit>>;
472 async fn list_branches(&self) -> Result<Vec<WorkspaceGitBranch>>;
473 async fn create_branch(&self, request: WorkspaceGitCreateBranchRequest) -> Result<()>;
474 async fn checkout(
475 &self,
476 request: WorkspaceGitCheckoutRequest,
477 ) -> Result<WorkspaceGitCheckoutOutput>;
478 async fn diff(&self, request: WorkspaceGitDiffRequest) -> Result<String>;
479 async fn list_remotes(&self) -> Result<Vec<WorkspaceGitRemote>>;
480}
481
482#[async_trait]
487pub trait WorkspaceGitStashProvider: Send + Sync {
488 async fn list_stashes(&self) -> Result<Vec<WorkspaceGitStash>>;
489 async fn stash(&self, request: WorkspaceGitStashRequest) -> Result<()>;
490}
491
492#[async_trait]
497pub trait WorkspaceGitWorktreeProvider: Send + Sync {
498 async fn list_worktrees(&self) -> Result<Vec<WorkspaceGitWorktree>>;
499 async fn create_worktree(
500 &self,
501 request: WorkspaceGitCreateWorktreeRequest,
502 ) -> Result<WorkspaceGitWorktreeMutation>;
503 async fn remove_worktree(
504 &self,
505 request: WorkspaceGitRemoveWorktreeRequest,
506 ) -> Result<WorkspaceGitWorktreeMutation>;
507}
508
509pub struct WorkspaceServices {
511 workspace_ref: WorkspaceRef,
512 capabilities: WorkspaceCapabilities,
513 path_resolver: Arc<dyn WorkspacePathResolver>,
514 file_system: Arc<dyn WorkspaceFileSystem>,
515 file_system_ext: Option<Arc<dyn WorkspaceFileSystemExt>>,
516 command_runner: Option<Arc<dyn WorkspaceCommandRunner>>,
517 search: Option<Arc<dyn WorkspaceSearch>>,
518 git: Option<Arc<dyn WorkspaceGit>>,
519 git_stash: Option<Arc<dyn WorkspaceGitStashProvider>>,
520 git_worktree: Option<Arc<dyn WorkspaceGitWorktreeProvider>>,
521 operation_timeout: Option<std::time::Duration>,
525 local_root: Option<PathBuf>,
526}
527
528impl std::fmt::Debug for WorkspaceServices {
529 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
530 f.debug_struct("WorkspaceServices")
531 .field("workspace_ref", &self.workspace_ref)
532 .field("capabilities", &self.capabilities)
533 .field("file_system_ext", &self.file_system_ext.is_some())
534 .field("command_runner", &self.command_runner.is_some())
535 .field("search", &self.search.is_some())
536 .field("git", &self.git.is_some())
537 .field("git_stash", &self.git_stash.is_some())
538 .field("git_worktree", &self.git_worktree.is_some())
539 .field("local_root", &self.local_root)
540 .finish()
541 }
542}
543
544impl WorkspaceServices {
545 pub(crate) fn new_with_git(
546 workspace_ref: WorkspaceRef,
547 mut capabilities: WorkspaceCapabilities,
548 path_resolver: Arc<dyn WorkspacePathResolver>,
549 file_system: Arc<dyn WorkspaceFileSystem>,
550 command_runner: Option<Arc<dyn WorkspaceCommandRunner>>,
551 search: Option<Arc<dyn WorkspaceSearch>>,
552 git: Option<Arc<dyn WorkspaceGit>>,
553 ) -> Self {
554 if command_runner.is_none() {
555 capabilities.exec = false;
556 }
557 if search.is_none() {
558 capabilities.search = false;
559 }
560 if git.is_none() {
561 capabilities.git = false;
562 }
563 Self {
564 workspace_ref,
565 capabilities,
566 path_resolver,
567 file_system,
568 file_system_ext: None,
569 command_runner,
570 search,
571 git,
572 git_stash: None,
573 git_worktree: None,
574 operation_timeout: None,
575 local_root: None,
576 }
577 }
578
579 pub fn builder(
580 workspace_ref: WorkspaceRef,
581 file_system: Arc<dyn WorkspaceFileSystem>,
582 ) -> WorkspaceServicesBuilder {
583 WorkspaceServicesBuilder::new(workspace_ref, file_system)
584 }
585
586 pub fn local(root: impl Into<PathBuf>) -> Arc<Self> {
587 let backend = Arc::new(LocalWorkspaceBackend::new(root.into()));
588 let workspace_ref = WorkspaceRef::new(
589 backend.root.display().to_string(),
590 backend.root.display().to_string(),
591 );
592 let path_resolver: Arc<dyn WorkspacePathResolver> = backend.clone();
593 let file_system: Arc<dyn WorkspaceFileSystem> = backend.clone();
594 let command_runner: Arc<dyn WorkspaceCommandRunner> = backend.clone();
595 let search: Arc<dyn WorkspaceSearch> = backend.clone();
596 let git: Arc<dyn WorkspaceGit> = backend.clone();
597 let git_stash: Arc<dyn WorkspaceGitStashProvider> = backend.clone();
598 let git_worktree: Arc<dyn WorkspaceGitWorktreeProvider> = backend.clone();
599 Arc::new(Self {
600 workspace_ref,
601 capabilities: WorkspaceCapabilities::local_default(),
602 path_resolver,
603 file_system,
604 file_system_ext: None,
605 command_runner: Some(command_runner),
606 search: Some(search),
607 git: Some(git),
608 git_stash: Some(git_stash),
609 git_worktree: Some(git_worktree),
610 operation_timeout: None,
611 local_root: Some(backend.root.clone()),
612 })
613 }
614
615 pub fn local_with_manifest(root: impl Into<PathBuf>) -> Arc<Self> {
620 let backend = ManifestWorkspaceBackend::new(root);
621 Self::local_with_manifest_backend(backend)
622 }
623
624 pub fn local_with_manifest_backend(backend: Arc<ManifestWorkspaceBackend>) -> Arc<Self> {
627 let workspace_ref = WorkspaceRef::new(
628 backend.local_root().display().to_string(),
629 backend.local_root().display().to_string(),
630 );
631 let path_resolver: Arc<dyn WorkspacePathResolver> = backend.clone();
632 let file_system: Arc<dyn WorkspaceFileSystem> = backend.clone();
633 let command_runner: Arc<dyn WorkspaceCommandRunner> = backend.clone();
634 let search: Arc<dyn WorkspaceSearch> = backend.clone();
635 let git: Arc<dyn WorkspaceGit> = backend.clone();
636 let git_stash: Arc<dyn WorkspaceGitStashProvider> = backend.clone();
637 let git_worktree: Arc<dyn WorkspaceGitWorktreeProvider> = backend.clone();
638 Arc::new(Self {
639 workspace_ref,
640 capabilities: WorkspaceCapabilities::local_default(),
641 path_resolver,
642 file_system,
643 file_system_ext: None,
644 command_runner: Some(command_runner),
645 search: Some(search),
646 git: Some(git),
647 git_stash: Some(git_stash),
648 git_worktree: Some(git_worktree),
649 operation_timeout: None,
650 local_root: Some(backend.local_root().to_path_buf()),
651 })
652 }
653
654 pub fn workspace_ref(&self) -> &WorkspaceRef {
655 &self.workspace_ref
656 }
657
658 pub fn capabilities(&self) -> WorkspaceCapabilities {
659 self.capabilities
660 }
661
662 pub fn normalize_path(&self, input: &str) -> Result<WorkspacePath> {
663 self.path_resolver.normalize(input)
664 }
665
666 pub fn fs(&self) -> Arc<dyn WorkspaceFileSystem> {
667 Arc::clone(&self.file_system)
668 }
669
670 pub fn fs_ext(&self) -> Option<Arc<dyn WorkspaceFileSystemExt>> {
677 self.file_system_ext.clone()
678 }
679
680 pub fn command_runner(&self) -> Option<Arc<dyn WorkspaceCommandRunner>> {
681 self.command_runner.clone()
682 }
683
684 pub fn search(&self) -> Option<Arc<dyn WorkspaceSearch>> {
685 self.search.clone()
686 }
687
688 pub fn git(&self) -> Option<Arc<dyn WorkspaceGit>> {
689 self.git.clone()
690 }
691
692 pub fn git_stash(&self) -> Option<Arc<dyn WorkspaceGitStashProvider>> {
693 self.git_stash.clone()
694 }
695
696 pub fn git_worktree(&self) -> Option<Arc<dyn WorkspaceGitWorktreeProvider>> {
697 self.git_worktree.clone()
698 }
699
700 pub(crate) fn with_git_provider(
717 &self,
718 git: Arc<dyn WorkspaceGit>,
719 git_stash: Option<Arc<dyn WorkspaceGitStashProvider>>,
720 ) -> Arc<Self> {
721 let mut capabilities = self.capabilities;
722 capabilities.git = true;
723 Arc::new(Self {
724 workspace_ref: self.workspace_ref.clone(),
725 capabilities,
726 path_resolver: Arc::clone(&self.path_resolver),
727 file_system: Arc::clone(&self.file_system),
728 file_system_ext: self.file_system_ext.clone(),
729 command_runner: self.command_runner.clone(),
730 search: self.search.clone(),
731 git: Some(git),
732 git_stash,
733 git_worktree: None,
734 operation_timeout: self.operation_timeout,
735 local_root: self.local_root.clone(),
736 })
737 }
738
739 pub fn operation_timeout(&self) -> Option<std::time::Duration> {
745 self.operation_timeout
746 }
747
748 pub async fn run_with_timeout<F, T, E>(
762 &self,
763 op: &'static str,
764 fut: F,
765 ) -> std::result::Result<T, E>
766 where
767 F: std::future::Future<Output = std::result::Result<T, E>>,
768 E: From<anyhow::Error>,
769 {
770 match self.operation_timeout {
771 Some(d) => tokio::time::timeout(d, fut).await.map_err(|_| {
772 E::from(anyhow!(
773 "workspace operation '{}' timed out after {:?}",
774 op,
775 d
776 ))
777 })?,
778 None => fut.await,
779 }
780 }
781
782 pub async fn read_for_edit(
789 &self,
790 path: &WorkspacePath,
791 ) -> WorkspaceResult<(String, Option<String>)> {
792 if let Some(ext) = self.fs_ext() {
793 let path = path.clone();
794 return self
795 .run_with_timeout("read_text_with_version", async move {
796 let (content, version) = ext.read_text_with_version(&path).await?;
797 Ok((content, Some(version)))
798 })
799 .await;
800 }
801 let fs = self.fs();
802 let path_owned = path.clone();
803 let content = self
804 .run_with_timeout("read_text", async move { fs.read_text(&path_owned).await })
805 .await?;
806 Ok((content, None))
807 }
808
809 pub async fn write_for_edit(
817 &self,
818 path: &WorkspacePath,
819 content: &str,
820 expected_version: Option<&str>,
821 ) -> WorkspaceResult<WorkspaceWriteOutcome> {
822 if let (Some(ext), Some(version)) = (self.fs_ext(), expected_version) {
823 let path = path.clone();
824 let content = content.to_string();
825 let expected = version.to_string();
826 return self
827 .run_with_timeout("write_text_if_version", async move {
828 ext.write_text_if_version(&path, &content, &expected).await
829 })
830 .await;
831 }
832 let fs = self.fs();
833 let path = path.clone();
834 let content = content.to_string();
835 self.run_with_timeout(
836 "write_text",
837 async move { fs.write_text(&path, &content).await },
838 )
839 .await
840 }
841
842 pub fn local_root(&self) -> Option<&Path> {
843 self.local_root.as_deref()
844 }
845
846 pub fn display_path(&self, path: &WorkspacePath) -> String {
847 if path.is_root() {
848 return self.workspace_ref.display_root.clone();
849 }
850
851 let root = self.workspace_ref.display_root.trim_end_matches('/');
852 if root.is_empty() {
853 path.as_str().to_string()
854 } else {
855 format!("{root}/{}", path.as_str())
856 }
857 }
858}
859
860pub struct WorkspaceServicesBuilder {
862 workspace_ref: WorkspaceRef,
863 capabilities: WorkspaceCapabilities,
864 path_resolver: Arc<dyn WorkspacePathResolver>,
865 file_system: Arc<dyn WorkspaceFileSystem>,
866 file_system_ext: Option<Arc<dyn WorkspaceFileSystemExt>>,
867 command_runner: Option<Arc<dyn WorkspaceCommandRunner>>,
868 search: Option<Arc<dyn WorkspaceSearch>>,
869 git: Option<Arc<dyn WorkspaceGit>>,
870 git_stash: Option<Arc<dyn WorkspaceGitStashProvider>>,
871 git_worktree: Option<Arc<dyn WorkspaceGitWorktreeProvider>>,
872 operation_timeout: Option<std::time::Duration>,
873}
874
875impl WorkspaceServicesBuilder {
876 pub fn new(workspace_ref: WorkspaceRef, file_system: Arc<dyn WorkspaceFileSystem>) -> Self {
877 Self {
878 workspace_ref,
879 capabilities: WorkspaceCapabilities::read_write(),
880 path_resolver: Arc::new(VirtualPathResolver),
881 file_system,
882 file_system_ext: None,
883 command_runner: None,
884 search: None,
885 git: None,
886 git_stash: None,
887 git_worktree: None,
888 operation_timeout: None,
889 }
890 }
891
892 pub fn capabilities(mut self, capabilities: WorkspaceCapabilities) -> Self {
893 self.capabilities = capabilities;
894 self
895 }
896
897 pub fn command_runner(mut self, command_runner: Arc<dyn WorkspaceCommandRunner>) -> Self {
898 self.capabilities.exec = true;
899 self.command_runner = Some(command_runner);
900 self
901 }
902
903 pub fn search(mut self, search: Arc<dyn WorkspaceSearch>) -> Self {
904 self.capabilities.search = true;
905 self.search = Some(search);
906 self
907 }
908
909 pub fn git(mut self, git: Arc<dyn WorkspaceGit>) -> Self {
910 self.capabilities.git = true;
911 self.git = Some(git);
912 self
913 }
914
915 pub fn git_stash(mut self, git_stash: Arc<dyn WorkspaceGitStashProvider>) -> Self {
916 self.git_stash = Some(git_stash);
917 self
918 }
919
920 pub fn git_worktree(mut self, git_worktree: Arc<dyn WorkspaceGitWorktreeProvider>) -> Self {
921 self.git_worktree = Some(git_worktree);
922 self
923 }
924
925 pub fn file_system_ext(mut self, ext: Arc<dyn WorkspaceFileSystemExt>) -> Self {
930 self.file_system_ext = Some(ext);
931 self
932 }
933
934 pub fn operation_timeout(mut self, timeout: std::time::Duration) -> Self {
938 self.operation_timeout = Some(timeout);
939 self
940 }
941
942 pub fn build(self) -> Arc<WorkspaceServices> {
943 let mut services = WorkspaceServices::new_with_git(
944 self.workspace_ref,
945 self.capabilities,
946 self.path_resolver,
947 self.file_system,
948 self.command_runner,
949 self.search,
950 self.git,
951 );
952 services.file_system_ext = self.file_system_ext;
953 services.git_stash = self.git_stash;
954 services.git_worktree = self.git_worktree;
955 services.operation_timeout = self.operation_timeout;
956 Arc::new(services)
957 }
958}
959
960#[cfg(test)]
961#[path = "tests.rs"]
962mod tests;