use super::mission::{AgentMission, key};
use sim_kernel::{Expr, Symbol};
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum WorkspaceLeaseKind {
File,
Directory,
Crate,
IdeObject,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum WorkspaceLeaseMode {
SharedRead,
ExclusiveWrite,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct WorkspaceLease {
pub kind: WorkspaceLeaseKind,
pub repo: Option<String>,
pub target: String,
pub mode: WorkspaceLeaseMode,
}
impl WorkspaceLease {
pub fn file(repo: impl Into<String>, path: impl Into<String>) -> Self {
Self::repo_target(WorkspaceLeaseKind::File, repo, path)
}
pub fn directory(repo: impl Into<String>, path: impl Into<String>) -> Self {
Self::repo_target(WorkspaceLeaseKind::Directory, repo, path)
}
pub fn crate_name(repo: impl Into<String>, crate_name: impl Into<String>) -> Self {
Self::repo_target(WorkspaceLeaseKind::Crate, repo, crate_name)
}
pub fn ide_object(object_id: Symbol) -> Self {
Self {
kind: WorkspaceLeaseKind::IdeObject,
repo: None,
target: object_id.to_string(),
mode: WorkspaceLeaseMode::ExclusiveWrite,
}
}
pub fn for_read(mut self) -> Self {
self.mode = WorkspaceLeaseMode::SharedRead;
self
}
pub fn for_write(mut self) -> Self {
self.mode = WorkspaceLeaseMode::ExclusiveWrite;
self
}
pub(crate) fn as_expr(&self) -> Expr {
let mut entries = vec![
key(
"kind",
Expr::Symbol(Symbol::new(lease_kind_label(&self.kind))),
),
key("target", Expr::String(self.target.clone())),
key(
"mode",
Expr::Symbol(Symbol::new(lease_mode_label(&self.mode))),
),
];
if let Some(repo) = &self.repo {
entries.push(key("repo", Expr::String(repo.clone())));
}
Expr::Map(entries)
}
fn repo_target(
kind: WorkspaceLeaseKind,
repo: impl Into<String>,
target: impl Into<String>,
) -> Self {
Self {
kind,
repo: Some(repo.into()),
target: normalize_path(&target.into()),
mode: WorkspaceLeaseMode::ExclusiveWrite,
}
}
fn conflicts_with(&self, other: &Self) -> bool {
if self.mode == WorkspaceLeaseMode::SharedRead
&& other.mode == WorkspaceLeaseMode::SharedRead
{
return false;
}
leases_overlap(self, other)
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct WorkspaceLeaseConflict {
pub left_mission: Symbol,
pub left: WorkspaceLease,
pub right_mission: Symbol,
pub right: WorkspaceLease,
}
pub fn detect_workspace_lease_conflicts(missions: &[AgentMission]) -> Vec<WorkspaceLeaseConflict> {
let mut conflicts = Vec::new();
for (left_index, left_mission) in missions.iter().enumerate() {
for right_mission in missions.iter().skip(left_index + 1) {
for left in left_mission.leases() {
for right in right_mission.leases() {
if left.conflicts_with(right) {
conflicts.push(WorkspaceLeaseConflict {
left_mission: left_mission.id().clone(),
left: left.clone(),
right_mission: right_mission.id().clone(),
right: right.clone(),
});
}
}
}
}
}
conflicts
}
pub(crate) fn leases_expr(leases: &[WorkspaceLease]) -> Expr {
Expr::List(leases.iter().map(WorkspaceLease::as_expr).collect())
}
fn leases_overlap(left: &WorkspaceLease, right: &WorkspaceLease) -> bool {
if left.kind == WorkspaceLeaseKind::IdeObject || right.kind == WorkspaceLeaseKind::IdeObject {
return left.kind == right.kind && left.target == right.target;
}
if left.repo != right.repo {
return false;
}
match (&left.kind, &right.kind) {
(WorkspaceLeaseKind::Crate, WorkspaceLeaseKind::Crate) => left.target == right.target,
(WorkspaceLeaseKind::File, WorkspaceLeaseKind::File) => left.target == right.target,
(WorkspaceLeaseKind::Directory, WorkspaceLeaseKind::Directory) => {
path_contains(&left.target, &right.target) || path_contains(&right.target, &left.target)
}
(WorkspaceLeaseKind::Directory, WorkspaceLeaseKind::File) => {
path_contains(&left.target, &right.target)
}
(WorkspaceLeaseKind::File, WorkspaceLeaseKind::Directory) => {
path_contains(&right.target, &left.target)
}
_ => false,
}
}
fn path_contains(directory: &str, path: &str) -> bool {
directory == "."
|| directory == path
|| path
.strip_prefix(directory)
.is_some_and(|rest| rest.starts_with('/'))
}
fn normalize_path(path: &str) -> String {
let normalized = path.trim_matches('/').replace('\\', "/");
if normalized.is_empty() {
".".to_owned()
} else {
normalized
}
}
fn lease_kind_label(kind: &WorkspaceLeaseKind) -> &'static str {
match kind {
WorkspaceLeaseKind::File => "file",
WorkspaceLeaseKind::Directory => "directory",
WorkspaceLeaseKind::Crate => "crate",
WorkspaceLeaseKind::IdeObject => "ide-object",
}
}
fn lease_mode_label(mode: &WorkspaceLeaseMode) -> &'static str {
match mode {
WorkspaceLeaseMode::SharedRead => "shared-read",
WorkspaceLeaseMode::ExclusiveWrite => "exclusive-write",
}
}