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 async fn on_output_complete(&self, _summary: &CommandOutputSummary) {}
333}
334
335#[derive(Debug, Clone, Copy, PartialEq, Eq)]
337pub struct CommandOutputSummary {
338 pub total_bytes: usize,
340 pub captured_bytes: usize,
342 pub truncated: bool,
344 pub timed_out: bool,
346}
347
348#[derive(Clone)]
350pub struct CommandRequest {
351 pub command: String,
352 pub timeout_ms: u64,
353 pub output_observer: Option<Arc<dyn CommandOutputObserver>>,
354 pub env: Option<Arc<HashMap<String, String>>>,
355}
356
357impl std::fmt::Debug for CommandRequest {
358 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
359 f.debug_struct("CommandRequest")
360 .field("command", &self.command)
361 .field("timeout_ms", &self.timeout_ms)
362 .field("output_observer", &self.output_observer.is_some())
363 .field("env", &self.env.as_ref().map(|env| env.len()))
364 .finish()
365 }
366}
367
368#[derive(Debug, Clone, PartialEq, Eq)]
370pub struct CommandOutput {
371 pub output: String,
372 pub exit_code: i32,
373 pub timed_out: bool,
374}
375
376pub trait WorkspacePathResolver: Send + Sync {
378 fn normalize(&self, input: &str) -> Result<WorkspacePath>;
379}
380
381#[async_trait]
391pub trait WorkspaceFileSystem: Send + Sync {
392 async fn read_text(&self, path: &WorkspacePath) -> WorkspaceResult<String>;
393 async fn write_text(
394 &self,
395 path: &WorkspacePath,
396 content: &str,
397 ) -> WorkspaceResult<WorkspaceWriteOutcome>;
398 async fn list_dir(&self, path: &WorkspacePath) -> WorkspaceResult<Vec<WorkspaceDirEntry>>;
399}
400
401#[derive(Debug, Clone, PartialEq, Eq)]
403pub struct WorkspaceTextRange {
404 pub lines: Vec<String>,
405 pub next_offset: Option<usize>,
406 pub eof: bool,
407 pub total_lines: Option<usize>,
409}
410
411#[async_trait]
414pub trait WorkspaceTextReader: Send + Sync {
415 async fn read_text_range(
416 &self,
417 path: &WorkspacePath,
418 offset: usize,
419 limit: usize,
420 ) -> WorkspaceResult<WorkspaceTextRange>;
421}
422
423#[derive(Debug, Clone, thiserror::Error)]
430#[error(
431 "version conflict on {path}: expected version {expected:?}, found {actual:?} (file modified by another writer; re-read and retry)"
432)]
433pub struct WorkspaceVersionConflict {
434 pub path: String,
435 pub expected: String,
436 pub actual: Option<String>,
439}
440
441#[async_trait]
453pub trait WorkspaceFileSystemExt: Send + Sync {
454 async fn read_text_with_version(
459 &self,
460 path: &WorkspacePath,
461 ) -> WorkspaceResult<(String, String)>;
462
463 async fn write_text_if_version(
469 &self,
470 path: &WorkspacePath,
471 content: &str,
472 expected_version: &str,
473 ) -> WorkspaceResult<WorkspaceWriteOutcome>;
474}
475
476#[async_trait]
478pub trait WorkspaceCommandRunner: Send + Sync {
479 async fn exec(&self, request: CommandRequest) -> Result<CommandOutput>;
480}
481
482#[async_trait]
484pub trait WorkspaceSearch: Send + Sync {
485 async fn glob(&self, request: WorkspaceGlobRequest) -> Result<WorkspaceGlobResult>;
486 async fn grep(&self, request: WorkspaceGrepRequest) -> Result<WorkspaceGrepResult>;
487
488 async fn grep_with_sources(
493 &self,
494 request: WorkspaceGrepRequest,
495 ) -> Result<WorkspaceGrepOutcome> {
496 let result = self.grep(request).await?;
497 Ok(WorkspaceGrepOutcome {
498 result,
499 matched_paths: None,
500 })
501 }
502}
503
504#[async_trait]
510pub trait WorkspaceGit: Send + Sync {
511 async fn is_repository(&self) -> Result<bool>;
512 async fn status(&self) -> Result<WorkspaceGitStatus>;
513 async fn log(&self, max_count: usize) -> Result<Vec<WorkspaceGitCommit>>;
514 async fn list_branches(&self) -> Result<Vec<WorkspaceGitBranch>>;
515 async fn create_branch(&self, request: WorkspaceGitCreateBranchRequest) -> Result<()>;
516 async fn checkout(
517 &self,
518 request: WorkspaceGitCheckoutRequest,
519 ) -> Result<WorkspaceGitCheckoutOutput>;
520 async fn diff(&self, request: WorkspaceGitDiffRequest) -> Result<String>;
521 async fn list_remotes(&self) -> Result<Vec<WorkspaceGitRemote>>;
522}
523
524#[async_trait]
529pub trait WorkspaceGitStashProvider: Send + Sync {
530 async fn list_stashes(&self) -> Result<Vec<WorkspaceGitStash>>;
531 async fn stash(&self, request: WorkspaceGitStashRequest) -> Result<()>;
532}
533
534#[async_trait]
539pub trait WorkspaceGitWorktreeProvider: Send + Sync {
540 async fn list_worktrees(&self) -> Result<Vec<WorkspaceGitWorktree>>;
541 async fn create_worktree(
542 &self,
543 request: WorkspaceGitCreateWorktreeRequest,
544 ) -> Result<WorkspaceGitWorktreeMutation>;
545 async fn remove_worktree(
546 &self,
547 request: WorkspaceGitRemoveWorktreeRequest,
548 ) -> Result<WorkspaceGitWorktreeMutation>;
549}
550
551pub struct WorkspaceServices {
553 workspace_ref: WorkspaceRef,
554 capabilities: WorkspaceCapabilities,
555 path_resolver: Arc<dyn WorkspacePathResolver>,
556 file_system: Arc<dyn WorkspaceFileSystem>,
557 file_system_ext: Option<Arc<dyn WorkspaceFileSystemExt>>,
558 text_reader: Option<Arc<dyn WorkspaceTextReader>>,
559 command_runner: Option<Arc<dyn WorkspaceCommandRunner>>,
560 search: Option<Arc<dyn WorkspaceSearch>>,
561 git: Option<Arc<dyn WorkspaceGit>>,
562 git_stash: Option<Arc<dyn WorkspaceGitStashProvider>>,
563 git_worktree: Option<Arc<dyn WorkspaceGitWorktreeProvider>>,
564 operation_timeout: Option<std::time::Duration>,
568 local_root: Option<PathBuf>,
569}
570
571impl std::fmt::Debug for WorkspaceServices {
572 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
573 f.debug_struct("WorkspaceServices")
574 .field("workspace_ref", &self.workspace_ref)
575 .field("capabilities", &self.capabilities)
576 .field("file_system_ext", &self.file_system_ext.is_some())
577 .field("text_reader", &self.text_reader.is_some())
578 .field("command_runner", &self.command_runner.is_some())
579 .field("search", &self.search.is_some())
580 .field("git", &self.git.is_some())
581 .field("git_stash", &self.git_stash.is_some())
582 .field("git_worktree", &self.git_worktree.is_some())
583 .field("local_root", &self.local_root)
584 .finish()
585 }
586}
587
588impl WorkspaceServices {
589 pub(crate) fn new_with_git(
590 workspace_ref: WorkspaceRef,
591 mut capabilities: WorkspaceCapabilities,
592 path_resolver: Arc<dyn WorkspacePathResolver>,
593 file_system: Arc<dyn WorkspaceFileSystem>,
594 command_runner: Option<Arc<dyn WorkspaceCommandRunner>>,
595 search: Option<Arc<dyn WorkspaceSearch>>,
596 git: Option<Arc<dyn WorkspaceGit>>,
597 ) -> Self {
598 if command_runner.is_none() {
599 capabilities.exec = false;
600 }
601 if search.is_none() {
602 capabilities.search = false;
603 }
604 if git.is_none() {
605 capabilities.git = false;
606 }
607 Self {
608 workspace_ref,
609 capabilities,
610 path_resolver,
611 file_system,
612 file_system_ext: None,
613 text_reader: None,
614 command_runner,
615 search,
616 git,
617 git_stash: None,
618 git_worktree: None,
619 operation_timeout: None,
620 local_root: None,
621 }
622 }
623
624 pub fn builder(
625 workspace_ref: WorkspaceRef,
626 file_system: Arc<dyn WorkspaceFileSystem>,
627 ) -> WorkspaceServicesBuilder {
628 WorkspaceServicesBuilder::new(workspace_ref, file_system)
629 }
630
631 pub fn local(root: impl Into<PathBuf>) -> Arc<Self> {
632 let backend = Arc::new(LocalWorkspaceBackend::new(root.into()));
633 let workspace_ref = WorkspaceRef::new(
634 backend.root.display().to_string(),
635 backend.root.display().to_string(),
636 );
637 let path_resolver: Arc<dyn WorkspacePathResolver> = backend.clone();
638 let file_system: Arc<dyn WorkspaceFileSystem> = backend.clone();
639 let text_reader: Arc<dyn WorkspaceTextReader> = backend.clone();
640 let command_runner: Arc<dyn WorkspaceCommandRunner> = backend.clone();
641 let search: Arc<dyn WorkspaceSearch> = backend.clone();
642 let git: Arc<dyn WorkspaceGit> = backend.clone();
643 let git_stash: Arc<dyn WorkspaceGitStashProvider> = backend.clone();
644 let git_worktree: Arc<dyn WorkspaceGitWorktreeProvider> = backend.clone();
645 Arc::new(Self {
646 workspace_ref,
647 capabilities: WorkspaceCapabilities::local_default(),
648 path_resolver,
649 file_system,
650 file_system_ext: None,
651 text_reader: Some(text_reader),
652 command_runner: Some(command_runner),
653 search: Some(search),
654 git: Some(git),
655 git_stash: Some(git_stash),
656 git_worktree: Some(git_worktree),
657 operation_timeout: None,
658 local_root: Some(backend.root.clone()),
659 })
660 }
661
662 pub fn local_with_manifest(root: impl Into<PathBuf>) -> Arc<Self> {
667 let backend = ManifestWorkspaceBackend::new(root);
668 Self::local_with_manifest_backend(backend)
669 }
670
671 pub fn local_with_manifest_backend(backend: Arc<ManifestWorkspaceBackend>) -> Arc<Self> {
674 let workspace_ref = WorkspaceRef::new(
675 backend.local_root().display().to_string(),
676 backend.local_root().display().to_string(),
677 );
678 let path_resolver: Arc<dyn WorkspacePathResolver> = backend.clone();
679 let file_system: Arc<dyn WorkspaceFileSystem> = backend.clone();
680 let text_reader: Arc<dyn WorkspaceTextReader> = backend.clone();
681 let command_runner: Arc<dyn WorkspaceCommandRunner> = backend.clone();
682 let search: Arc<dyn WorkspaceSearch> = backend.clone();
683 let git: Arc<dyn WorkspaceGit> = backend.clone();
684 let git_stash: Arc<dyn WorkspaceGitStashProvider> = backend.clone();
685 let git_worktree: Arc<dyn WorkspaceGitWorktreeProvider> = backend.clone();
686 Arc::new(Self {
687 workspace_ref,
688 capabilities: WorkspaceCapabilities::local_default(),
689 path_resolver,
690 file_system,
691 file_system_ext: None,
692 text_reader: Some(text_reader),
693 command_runner: Some(command_runner),
694 search: Some(search),
695 git: Some(git),
696 git_stash: Some(git_stash),
697 git_worktree: Some(git_worktree),
698 operation_timeout: None,
699 local_root: Some(backend.local_root().to_path_buf()),
700 })
701 }
702
703 pub fn workspace_ref(&self) -> &WorkspaceRef {
704 &self.workspace_ref
705 }
706
707 pub fn capabilities(&self) -> WorkspaceCapabilities {
708 self.capabilities
709 }
710
711 pub fn normalize_path(&self, input: &str) -> Result<WorkspacePath> {
712 self.path_resolver.normalize(input)
713 }
714
715 pub fn fs(&self) -> Arc<dyn WorkspaceFileSystem> {
716 Arc::clone(&self.file_system)
717 }
718
719 pub fn fs_ext(&self) -> Option<Arc<dyn WorkspaceFileSystemExt>> {
726 self.file_system_ext.clone()
727 }
728
729 pub fn text_reader(&self) -> Option<Arc<dyn WorkspaceTextReader>> {
730 self.text_reader.clone()
731 }
732
733 pub fn command_runner(&self) -> Option<Arc<dyn WorkspaceCommandRunner>> {
734 self.command_runner.clone()
735 }
736
737 pub fn search(&self) -> Option<Arc<dyn WorkspaceSearch>> {
738 self.search.clone()
739 }
740
741 pub fn git(&self) -> Option<Arc<dyn WorkspaceGit>> {
742 self.git.clone()
743 }
744
745 pub fn git_stash(&self) -> Option<Arc<dyn WorkspaceGitStashProvider>> {
746 self.git_stash.clone()
747 }
748
749 pub fn git_worktree(&self) -> Option<Arc<dyn WorkspaceGitWorktreeProvider>> {
750 self.git_worktree.clone()
751 }
752
753 pub(crate) fn with_git_provider(
770 &self,
771 git: Arc<dyn WorkspaceGit>,
772 git_stash: Option<Arc<dyn WorkspaceGitStashProvider>>,
773 ) -> Arc<Self> {
774 let mut capabilities = self.capabilities;
775 capabilities.git = true;
776 Arc::new(Self {
777 workspace_ref: self.workspace_ref.clone(),
778 capabilities,
779 path_resolver: Arc::clone(&self.path_resolver),
780 file_system: Arc::clone(&self.file_system),
781 file_system_ext: self.file_system_ext.clone(),
782 text_reader: self.text_reader.clone(),
783 command_runner: self.command_runner.clone(),
784 search: self.search.clone(),
785 git: Some(git),
786 git_stash,
787 git_worktree: None,
788 operation_timeout: self.operation_timeout,
789 local_root: self.local_root.clone(),
790 })
791 }
792
793 pub fn operation_timeout(&self) -> Option<std::time::Duration> {
799 self.operation_timeout
800 }
801
802 pub async fn run_with_timeout<F, T, E>(
816 &self,
817 op: &'static str,
818 fut: F,
819 ) -> std::result::Result<T, E>
820 where
821 F: std::future::Future<Output = std::result::Result<T, E>>,
822 E: From<anyhow::Error>,
823 {
824 match self.operation_timeout {
825 Some(d) => tokio::time::timeout(d, fut).await.map_err(|_| {
826 E::from(anyhow!(
827 "workspace operation '{}' timed out after {:?}",
828 op,
829 d
830 ))
831 })?,
832 None => fut.await,
833 }
834 }
835
836 pub async fn read_for_edit(
843 &self,
844 path: &WorkspacePath,
845 ) -> WorkspaceResult<(String, Option<String>)> {
846 if let Some(ext) = self.fs_ext() {
847 let path = path.clone();
848 return self
849 .run_with_timeout("read_text_with_version", async move {
850 let (content, version) = ext.read_text_with_version(&path).await?;
851 Ok((content, Some(version)))
852 })
853 .await;
854 }
855 let fs = self.fs();
856 let path_owned = path.clone();
857 let content = self
858 .run_with_timeout("read_text", async move { fs.read_text(&path_owned).await })
859 .await?;
860 Ok((content, None))
861 }
862
863 pub async fn write_for_edit(
871 &self,
872 path: &WorkspacePath,
873 content: &str,
874 expected_version: Option<&str>,
875 ) -> WorkspaceResult<WorkspaceWriteOutcome> {
876 if let (Some(ext), Some(version)) = (self.fs_ext(), expected_version) {
877 let path = path.clone();
878 let content = content.to_string();
879 let expected = version.to_string();
880 return self
881 .run_with_timeout("write_text_if_version", async move {
882 ext.write_text_if_version(&path, &content, &expected).await
883 })
884 .await;
885 }
886 let fs = self.fs();
887 let path = path.clone();
888 let content = content.to_string();
889 self.run_with_timeout(
890 "write_text",
891 async move { fs.write_text(&path, &content).await },
892 )
893 .await
894 }
895
896 pub fn local_root(&self) -> Option<&Path> {
897 self.local_root.as_deref()
898 }
899
900 pub fn display_path(&self, path: &WorkspacePath) -> String {
901 if path.is_root() {
902 return self.workspace_ref.display_root.clone();
903 }
904
905 let root = self.workspace_ref.display_root.trim_end_matches('/');
906 if root.is_empty() {
907 path.as_str().to_string()
908 } else {
909 format!("{root}/{}", path.as_str())
910 }
911 }
912}
913
914pub struct WorkspaceServicesBuilder {
916 workspace_ref: WorkspaceRef,
917 capabilities: WorkspaceCapabilities,
918 path_resolver: Arc<dyn WorkspacePathResolver>,
919 file_system: Arc<dyn WorkspaceFileSystem>,
920 file_system_ext: Option<Arc<dyn WorkspaceFileSystemExt>>,
921 text_reader: Option<Arc<dyn WorkspaceTextReader>>,
922 command_runner: Option<Arc<dyn WorkspaceCommandRunner>>,
923 search: Option<Arc<dyn WorkspaceSearch>>,
924 git: Option<Arc<dyn WorkspaceGit>>,
925 git_stash: Option<Arc<dyn WorkspaceGitStashProvider>>,
926 git_worktree: Option<Arc<dyn WorkspaceGitWorktreeProvider>>,
927 operation_timeout: Option<std::time::Duration>,
928}
929
930impl WorkspaceServicesBuilder {
931 pub fn new(workspace_ref: WorkspaceRef, file_system: Arc<dyn WorkspaceFileSystem>) -> Self {
932 Self {
933 workspace_ref,
934 capabilities: WorkspaceCapabilities::read_write(),
935 path_resolver: Arc::new(VirtualPathResolver),
936 file_system,
937 file_system_ext: None,
938 text_reader: None,
939 command_runner: None,
940 search: None,
941 git: None,
942 git_stash: None,
943 git_worktree: None,
944 operation_timeout: None,
945 }
946 }
947
948 pub fn capabilities(mut self, capabilities: WorkspaceCapabilities) -> Self {
949 self.capabilities = capabilities;
950 self
951 }
952
953 pub fn command_runner(mut self, command_runner: Arc<dyn WorkspaceCommandRunner>) -> Self {
954 self.capabilities.exec = true;
955 self.command_runner = Some(command_runner);
956 self
957 }
958
959 pub fn search(mut self, search: Arc<dyn WorkspaceSearch>) -> Self {
960 self.capabilities.search = true;
961 self.search = Some(search);
962 self
963 }
964
965 pub fn git(mut self, git: Arc<dyn WorkspaceGit>) -> Self {
966 self.capabilities.git = true;
967 self.git = Some(git);
968 self
969 }
970
971 pub fn git_stash(mut self, git_stash: Arc<dyn WorkspaceGitStashProvider>) -> Self {
972 self.git_stash = Some(git_stash);
973 self
974 }
975
976 pub fn git_worktree(mut self, git_worktree: Arc<dyn WorkspaceGitWorktreeProvider>) -> Self {
977 self.git_worktree = Some(git_worktree);
978 self
979 }
980
981 pub fn file_system_ext(mut self, ext: Arc<dyn WorkspaceFileSystemExt>) -> Self {
986 self.file_system_ext = Some(ext);
987 self
988 }
989
990 pub fn text_reader(mut self, reader: Arc<dyn WorkspaceTextReader>) -> Self {
991 self.text_reader = Some(reader);
992 self
993 }
994
995 pub fn operation_timeout(mut self, timeout: std::time::Duration) -> Self {
999 self.operation_timeout = Some(timeout);
1000 self
1001 }
1002
1003 pub fn build(self) -> Arc<WorkspaceServices> {
1004 let mut services = WorkspaceServices::new_with_git(
1005 self.workspace_ref,
1006 self.capabilities,
1007 self.path_resolver,
1008 self.file_system,
1009 self.command_runner,
1010 self.search,
1011 self.git,
1012 );
1013 services.file_system_ext = self.file_system_ext;
1014 services.text_reader = self.text_reader;
1015 services.git_stash = self.git_stash;
1016 services.git_worktree = self.git_worktree;
1017 services.operation_timeout = self.operation_timeout;
1018 Arc::new(services)
1019 }
1020}
1021
1022#[cfg(test)]
1023#[path = "tests.rs"]
1024mod tests;