use super::*;
pub const WORKSPACE_MISSING_FILE_REVISION: &str = "missing";
pub fn revision_matches(expected: &str, actual: Option<&str>) -> bool {
if expected == WORKSPACE_MISSING_FILE_REVISION {
actual.is_none()
} else {
actual == Some(expected)
}
}
#[async_trait]
pub trait Workspace: Send + Sync + Debug {
fn description(&self) -> String;
fn root(&self) -> PathBuf;
fn resolved_backend_id(&self) -> Option<ResolvedBackendId> {
None
}
fn supports_change_sets(&self) -> bool {
false
}
async fn apply_change_set(
&self,
_request: WorkspaceChangeSetRequest,
) -> Result<WorkspaceChangeSet> {
anyhow::bail!("workspace change sets are not supported by this workspace")
}
async fn get_change_set(&self, _id: &str) -> Result<WorkspaceChangeSet> {
anyhow::bail!("workspace change sets are not supported by this workspace")
}
async fn revert_change_set(
&self,
_id: &str,
_conversation_id: Option<String>,
) -> Result<WorkspaceChangeSet> {
anyhow::bail!("workspace change sets are not supported by this workspace")
}
async fn read_file(&self, path: &str) -> Result<String>;
async fn read_file_tail(&self, path: &str, max_bytes: usize) -> Result<String> {
if max_bytes == 0 {
return Ok(String::new());
}
let mut value = self.read_file(path).await?;
if value.len() <= max_bytes {
return Ok(value);
}
let mut start = value.len() - max_bytes;
while !value.is_char_boundary(start) {
start += 1;
}
value.drain(..start);
Ok(value)
}
async fn read_file_bytes(&self, path: &str) -> Result<Vec<u8>> {
Ok(self.read_file(path).await?.into_bytes())
}
async fn write_file(&self, path: &str, content: &str) -> Result<()>;
async fn write_file_bytes(&self, path: &str, content: &[u8]) -> Result<()> {
let content = std::str::from_utf8(content)?;
self.write_file(path, content).await
}
async fn file_revision(&self, path: &str) -> Result<Option<String>> {
if !self.exists(path).await? {
return Ok(None);
}
Ok(Some(content_revision(&self.read_file_bytes(path).await?)))
}
async fn read_file_versioned(&self, path: &str) -> Result<VersionedFile> {
let bytes = self.read_file_bytes(path).await?;
let revision = content_revision(&bytes);
Ok(VersionedFile { bytes, revision })
}
async fn write_file_if_match(
&self,
path: &str,
content: &[u8],
if_match: Option<&str>,
) -> Result<String> {
if let Some(expected) = if_match {
let actual = self.file_revision(path).await?;
if !revision_matches(expected, actual.as_deref()) {
return Err(WorkspaceVersionConflict {
expected: expected.to_string(),
actual,
}
.into());
}
}
self.write_file_bytes(path, content).await?;
Ok(content_revision(content))
}
async fn write_file_chunk(
&self,
_path: &str,
_offset: u64,
_content: bytes::Bytes,
_final_chunk: bool,
) -> Result<()> {
anyhow::bail!("resumable uploads are not supported by this workspace")
}
async fn create_dir_all(&self, path: &str) -> Result<()>;
async fn remove_file(&self, path: &str) -> Result<()>;
async fn remove_file_if_match(&self, path: &str, if_match: &str) -> Result<()> {
let actual = self.file_revision(path).await?;
if !revision_matches(if_match, actual.as_deref()) {
return Err(WorkspaceVersionConflict {
expected: if_match.to_string(),
actual,
}
.into());
}
self.remove_file(path).await
}
async fn exists(&self, path: &str) -> Result<bool>;
async fn is_dir(&self, path: &str) -> Result<bool>;
async fn list_dir(&self, path: &str) -> Result<Vec<DirEntry>>;
async fn file_size(&self, path: &str) -> Result<Option<u64>> {
let path = std::path::Path::new(path);
let Some(name) = path.file_name().and_then(|value| value.to_str()) else {
return Ok(None);
};
let parent = path.parent().unwrap_or_else(|| std::path::Path::new("."));
Ok(self
.list_dir(parent.to_string_lossy().as_ref())
.await?
.into_iter()
.find(|entry| !entry.is_dir && entry.name == name)
.and_then(|entry| entry.size))
}
async fn list_dir_with_options(
&self,
path: &str,
_options: FileVisibilityOptions,
) -> Result<Vec<DirEntry>> {
self.list_dir(path).await
}
async fn walk_tree(&self, path: &str, max_depth: usize) -> Result<Vec<String>>;
async fn walk_tree_with_options(
&self,
path: &str,
max_depth: usize,
_options: FileVisibilityOptions,
) -> Result<Vec<String>> {
self.walk_tree(path, max_depth).await
}
async fn find_files(&self, pattern: &str, path: &str) -> Result<Vec<String>>;
async fn find_files_with_options(
&self,
pattern: &str,
path: &str,
_options: FileVisibilityOptions,
) -> Result<Vec<String>> {
self.find_files(pattern, path).await
}
async fn grep(
&self,
pattern: &str,
path: &str,
include: Option<&str>,
) -> Result<Vec<GrepMatch>>;
async fn grep_with_options(
&self,
pattern: &str,
path: &str,
include: Option<&str>,
_options: FileVisibilityOptions,
) -> Result<Vec<GrepMatch>> {
self.grep(pattern, path, include).await
}
async fn exec(&self, command: &str, cwd: Option<&str>) -> Result<CmdOutput>;
async fn resource_usage(&self) -> Result<WorkspaceResourceUsage> {
Ok(WorkspaceResourceUsage::unavailable(
self.description(),
self.root(),
self.resolved_backend_id().map(|value| value.0),
"providerResourceUsageUnavailable",
))
}
async fn exec_detached(
&self,
command: &str,
cwd: Option<&str>,
_timeout_secs: u64,
) -> Result<CmdOutput> {
let _ = (command, cwd);
anyhow::bail!("detached command execution requires a provider-native executor")
}
async fn port_url(&self, port: u16) -> Result<PortUrl> {
anyhow::bail!(
"preview URL lookup is not supported for {} on port {}",
self.description(),
port
)
}
async fn port_url_with_ttl(&self, port: u16, _ttl_seconds: u64) -> Result<PortUrl> {
self.port_url(port).await
}
async fn start_static_preview(&self, directory: &str) -> Result<StaticPreviewServer> {
let _ = directory;
anyhow::bail!("static preview requires a provider-native preview adapter")
}
async fn static_preview_ready(&self, server: &StaticPreviewServer) -> Result<bool> {
let _ = server;
Ok(false)
}
async fn stop_static_preview(&self, id: &str) -> Result<()> {
let _ = id;
anyhow::bail!("static preview requires a provider-native preview adapter")
}
async fn keep_alive(&self) -> Result<()> {
Ok(())
}
async fn init(&self) -> Result<()> {
Ok(())
}
async fn cleanup(&self) -> Result<()> {
Ok(())
}
}