#[cfg(test)]
pub(crate) mod conformance;
mod error;
mod local;
mod manifest;
mod path;
mod remote_git;
#[cfg(feature = "s3")]
mod s3;
mod services;
pub use error::{WorkspaceError, WorkspaceResult};
pub use local::LocalWorkspaceBackend;
pub use manifest::{
scan_workspace_files, LocalWorkspaceFile, LocalWorkspaceFileStatus, LocalWorkspaceManifest,
LocalWorkspaceManifestSnapshot, ManifestWorkspaceBackend, RecentWorkspaceFile,
WorkspaceFileChange, WorkspaceFileChangeKind,
};
pub(crate) use path::validate_relative_pattern;
pub use path::VirtualPathResolver;
use path::{
default_path_input, escape_control_chars_for_display, has_windows_path_prefix,
normalize_relative_path, pathbuf_to_workspace_path,
};
pub use remote_git::{RemoteGitBackend, RemoteGitBackendConfig, RemoteGitConflict};
#[cfg(feature = "s3")]
pub use s3::{S3BackendConfig, S3WorkspaceBackend};
pub use services::{WorkspaceServices, WorkspaceServicesBuilder};
use anyhow::Result;
use async_trait::async_trait;
use std::collections::HashMap;
use std::sync::Arc;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct WorkspaceRef {
pub id: String,
pub display_root: String,
}
impl WorkspaceRef {
pub fn new(id: impl Into<String>, display_root: impl Into<String>) -> Self {
Self {
id: id.into(),
display_root: display_root.into(),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct WorkspacePath {
inner: String,
}
impl WorkspacePath {
pub fn root() -> Self {
Self {
inner: ".".to_string(),
}
}
pub fn from_normalized(path: impl Into<String>) -> Self {
let path = path.into();
let path = path.trim_matches('/');
if path.is_empty() || path == "." {
Self::root()
} else {
Self {
inner: path.replace('\\', "/"),
}
}
}
pub fn as_str(&self) -> &str {
&self.inner
}
pub fn is_root(&self) -> bool {
self.inner == "."
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct WorkspaceCapabilities {
pub read: bool,
pub write: bool,
pub exec: bool,
pub search: bool,
pub git: bool,
pub code_intelligence: bool,
}
impl WorkspaceCapabilities {
pub fn local_default() -> Self {
Self {
read: true,
write: true,
exec: true,
search: true,
git: true,
code_intelligence: false,
}
}
pub fn read_write() -> Self {
Self {
read: true,
write: true,
exec: false,
search: false,
git: false,
code_intelligence: false,
}
}
}
impl Default for WorkspaceCapabilities {
fn default() -> Self {
Self::read_write()
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum WorkspaceFileType {
File,
Directory,
Symlink,
Unknown,
}
impl WorkspaceFileType {
pub fn as_tool_kind(self) -> &'static str {
match self {
Self::File => "file",
Self::Directory => "dir",
Self::Symlink => "link",
Self::Unknown => "unknown",
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct WorkspaceDirEntry {
pub name: String,
pub kind: WorkspaceFileType,
pub size: u64,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct WorkspaceWriteOutcome {
pub bytes: usize,
pub lines: usize,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct WorkspaceGlobRequest {
pub base: WorkspacePath,
pub pattern: String,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct WorkspaceGlobResult {
pub matches: Vec<WorkspacePath>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct WorkspaceGrepRequest {
pub base: WorkspacePath,
pub pattern: String,
pub glob: Option<String>,
pub context_lines: usize,
pub case_insensitive: bool,
pub max_output_size: usize,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct WorkspaceGrepResult {
pub output: String,
pub match_count: usize,
pub file_count: usize,
pub truncated: bool,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct WorkspaceGrepOutcome {
pub result: WorkspaceGrepResult,
pub matched_paths: Option<Vec<WorkspacePath>>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct WorkspaceGitStatus {
pub branch: String,
pub commit: String,
pub is_worktree: bool,
pub is_dirty: bool,
pub dirty_count: usize,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct WorkspaceGitCommit {
pub id: String,
pub message: String,
pub author: String,
pub date: String,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct WorkspaceGitBranch {
pub name: String,
pub is_current: bool,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct WorkspaceGitCreateBranchRequest {
pub name: String,
pub base: String,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct WorkspaceGitCheckoutRequest {
pub refspec: String,
pub force: bool,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct WorkspaceGitCheckoutOutput {
pub stdout: String,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct WorkspaceGitDiffRequest {
pub target: Option<String>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct WorkspaceGitStash {
pub index: usize,
pub message: String,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct WorkspaceGitStashRequest {
pub message: Option<String>,
pub include_untracked: bool,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct WorkspaceGitRemote {
pub name: String,
pub url: String,
pub direction: String,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct WorkspaceGitWorktree {
pub path: String,
pub branch: String,
pub is_bare: bool,
pub is_detached: bool,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct WorkspaceGitCreateWorktreeRequest {
pub branch: String,
pub path: Option<String>,
pub new_branch: bool,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct WorkspaceGitRemoveWorktreeRequest {
pub path: String,
pub force: bool,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct WorkspaceGitWorktreeMutation {
pub path: String,
pub branch: Option<String>,
}
#[async_trait]
pub trait CommandOutputObserver: Send + Sync {
async fn on_output_delta(&self, delta: &str);
async fn on_output_complete(&self, _summary: &CommandOutputSummary) {}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct CommandOutputSummary {
pub total_bytes: usize,
pub captured_bytes: usize,
pub truncated: bool,
pub timed_out: bool,
}
#[derive(Clone)]
pub struct CommandRequest {
pub command: String,
pub timeout_ms: u64,
pub output_observer: Option<Arc<dyn CommandOutputObserver>>,
pub env: Option<Arc<HashMap<String, String>>>,
}
impl std::fmt::Debug for CommandRequest {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("CommandRequest")
.field("command", &self.command)
.field("timeout_ms", &self.timeout_ms)
.field("output_observer", &self.output_observer.is_some())
.field("env", &self.env.as_ref().map(|env| env.len()))
.finish()
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct CommandOutput {
pub output: String,
pub exit_code: i32,
pub timed_out: bool,
}
pub trait WorkspacePathResolver: Send + Sync {
fn normalize(&self, input: &str) -> Result<WorkspacePath>;
}
#[async_trait]
pub trait WorkspaceFileSystem: Send + Sync {
async fn read_text(&self, path: &WorkspacePath) -> WorkspaceResult<String>;
async fn write_text(
&self,
path: &WorkspacePath,
content: &str,
) -> WorkspaceResult<WorkspaceWriteOutcome>;
async fn list_dir(&self, path: &WorkspacePath) -> WorkspaceResult<Vec<WorkspaceDirEntry>>;
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct WorkspaceTextRange {
pub lines: Vec<String>,
pub next_offset: Option<usize>,
pub eof: bool,
pub total_lines: Option<usize>,
}
#[async_trait]
pub trait WorkspaceTextReader: Send + Sync {
async fn read_text_range(
&self,
path: &WorkspacePath,
offset: usize,
limit: usize,
) -> WorkspaceResult<WorkspaceTextRange>;
}
#[derive(Debug, Clone, thiserror::Error)]
#[error(
"version conflict on {path}: expected version {expected:?}, found {actual:?} (file modified by another writer; re-read and retry)"
)]
pub struct WorkspaceVersionConflict {
pub path: String,
pub expected: String,
pub actual: Option<String>,
}
#[async_trait]
pub trait WorkspaceFileSystemExt: Send + Sync {
async fn read_text_with_version(
&self,
path: &WorkspacePath,
) -> WorkspaceResult<(String, String)>;
async fn write_text_if_version(
&self,
path: &WorkspacePath,
content: &str,
expected_version: &str,
) -> WorkspaceResult<WorkspaceWriteOutcome>;
}
#[async_trait]
pub trait WorkspaceCommandRunner: Send + Sync {
async fn exec(&self, request: CommandRequest) -> Result<CommandOutput>;
}
#[async_trait]
pub trait WorkspaceSearch: Send + Sync {
async fn glob(&self, request: WorkspaceGlobRequest) -> Result<WorkspaceGlobResult>;
async fn grep(&self, request: WorkspaceGrepRequest) -> Result<WorkspaceGrepResult>;
async fn grep_with_sources(
&self,
request: WorkspaceGrepRequest,
) -> Result<WorkspaceGrepOutcome> {
let result = self.grep(request).await?;
Ok(WorkspaceGrepOutcome {
result,
matched_paths: None,
})
}
}
#[async_trait]
pub trait WorkspaceGit: Send + Sync {
async fn is_repository(&self) -> Result<bool>;
async fn status(&self) -> Result<WorkspaceGitStatus>;
async fn log(&self, max_count: usize) -> Result<Vec<WorkspaceGitCommit>>;
async fn list_branches(&self) -> Result<Vec<WorkspaceGitBranch>>;
async fn create_branch(&self, request: WorkspaceGitCreateBranchRequest) -> Result<()>;
async fn checkout(
&self,
request: WorkspaceGitCheckoutRequest,
) -> Result<WorkspaceGitCheckoutOutput>;
async fn diff(&self, request: WorkspaceGitDiffRequest) -> Result<String>;
async fn list_remotes(&self) -> Result<Vec<WorkspaceGitRemote>>;
}
#[async_trait]
pub trait WorkspaceGitStashProvider: Send + Sync {
async fn list_stashes(&self) -> Result<Vec<WorkspaceGitStash>>;
async fn stash(&self, request: WorkspaceGitStashRequest) -> Result<()>;
}
#[async_trait]
pub trait WorkspaceGitWorktreeProvider: Send + Sync {
async fn list_worktrees(&self) -> Result<Vec<WorkspaceGitWorktree>>;
async fn create_worktree(
&self,
request: WorkspaceGitCreateWorktreeRequest,
) -> Result<WorkspaceGitWorktreeMutation>;
async fn remove_worktree(
&self,
request: WorkspaceGitRemoveWorktreeRequest,
) -> Result<WorkspaceGitWorktreeMutation>;
}
#[cfg(test)]
#[path = "tests.rs"]
mod tests;