magi-code 0.96.1

Repository-aware CLI coding agent for terminal work
Documentation
use super::p4ignore::{P4Environment, P4Matcher};
use crate::cancellation::AgentCancellation;
use ignore::WalkBuilder;
use std::{
    collections::HashSet,
    fs,
    path::{Path, PathBuf},
    time::Instant,
};

#[derive(Debug, Default)]
pub(super) struct WorkspaceWalker {}

#[derive(Debug, Clone, Copy, Default)]
pub(super) struct WorkspaceVisitStatus {
    pub(super) completed: bool,
    pub(super) entries_visited: usize,
    pub(super) entry_limit_reached: bool,
    pub(super) timed_out: bool,
    pub(super) walk_errors: usize,
    pub(super) entries_omitted: usize,
}

fn classify_walk_entry(
    entry: Result<Option<fs::FileType>, ()>,
    status: &mut WorkspaceVisitStatus,
) -> Option<fs::FileType> {
    match entry {
        Ok(Some(file_type)) => Some(file_type),
        Ok(None) => {
            status.completed = false;
            status.entries_omitted = status.entries_omitted.saturating_add(1);
            None
        }
        Err(()) => {
            status.completed = false;
            status.walk_errors = status.walk_errors.saturating_add(1);
            status.entries_omitted = status.entries_omitted.saturating_add(1);
            None
        }
    }
}

pub(super) struct WorkspaceWalkOptions<'a> {
    pub(super) root: &'a Path,
    pub(super) skip_dirs: &'a [&'a str],
    pub(super) cancel_interval: usize,
}

#[derive(Debug, Clone, Copy, Default)]
struct WorkspaceWalkBounds {
    deadline: Option<Instant>,
    max_entries: Option<usize>,
}

impl WorkspaceWalker {
    pub(super) fn visit_sorted_files_with_diagnostics<F>(
        &self,
        options: WorkspaceWalkOptions<'_>,
        cancellation: Option<&AgentCancellation>,
        mut visitor: F,
    ) -> anyhow::Result<WorkspaceVisitStatus>
    where
        F: FnMut(PathBuf) -> anyhow::Result<bool>,
    {
        self.visit_entries_with_environment(
            options,
            cancellation,
            &P4Environment::process(),
            true,
            |path, ty| {
                if ty.is_file() {
                    visitor(path.to_path_buf())
                } else {
                    Ok(true)
                }
            },
        )
    }

    fn visit_entries_with_environment<F>(
        &self,
        options: WorkspaceWalkOptions<'_>,
        cancellation: Option<&AgentCancellation>,
        environment: &P4Environment,
        sorted: bool,
        visitor: F,
    ) -> anyhow::Result<WorkspaceVisitStatus>
    where
        F: FnMut(&Path, fs::FileType) -> anyhow::Result<bool>,
    {
        self.visit_entries_with_environment_and_bounds(
            options,
            cancellation,
            environment,
            sorted,
            WorkspaceWalkBounds::default(),
            visitor,
        )
    }

    fn visit_entries_with_environment_and_bounds<F>(
        &self,
        options: WorkspaceWalkOptions<'_>,
        cancellation: Option<&AgentCancellation>,
        environment: &P4Environment,
        sorted: bool,
        bounds: WorkspaceWalkBounds,
        mut visitor: F,
    ) -> anyhow::Result<WorkspaceVisitStatus>
    where
        F: FnMut(&Path, fs::FileType) -> anyhow::Result<bool>,
    {
        if let Some(c) = cancellation {
            c.check()?;
        }
        if bounds
            .deadline
            .is_some_and(|deadline| Instant::now() >= deadline)
        {
            return Ok(WorkspaceVisitStatus {
                timed_out: true,
                ..WorkspaceVisitStatus::default()
            });
        }
        let (walker, p4_matcher) = configured_walk(&options, environment, sorted, bounds.deadline)?;
        let mut status = WorkspaceVisitStatus {
            completed: true,
            ..WorkspaceVisitStatus::default()
        };
        let max_entries = bounds.max_entries.unwrap_or(usize::MAX);
        let mut visitor_stopped = false;
        for (index, entry) in walker.take(max_entries).enumerate() {
            status.entries_visited = status.entries_visited.saturating_add(1);
            if bounds
                .deadline
                .is_some_and(|deadline| Instant::now() >= deadline)
            {
                status.completed = false;
                status.timed_out = true;
                break;
            }
            if index % options.cancel_interval.max(1) == 0
                && let Some(c) = cancellation
            {
                c.check()?;
            }
            let entry = match entry {
                Ok(entry) => entry,
                Err(_) => {
                    let _ = classify_walk_entry(Err(()), &mut status);
                    continue;
                }
            };
            let Some(ty) = classify_walk_entry(Ok(entry.file_type()), &mut status) else {
                continue;
            };
            let path = entry.path();
            if path == options.root {
                continue;
            }
            if p4_matcher.is_ignored(path, ty.is_dir()) {
                continue;
            }
            if !visitor(path, ty)? {
                status.completed = false;
                visitor_stopped = true;
                break;
            }
        }
        if !visitor_stopped
            && bounds
                .max_entries
                .is_some_and(|max_entries| status.entries_visited >= max_entries)
        {
            // Do not pull one more directory entry merely to distinguish an exact-boundary
            // completion. Conservatively report a partial scan at the configured budget.
            status.completed = false;
            status.entry_limit_reached = true;
        }
        if let Some(c) = cancellation {
            c.check()?;
        }
        if let Some(error) = p4_matcher.take_error() {
            return Err(error);
        }
        Ok(status)
    }

    pub(super) fn visit_files_with_deadline<F>(
        &self,
        options: WorkspaceWalkOptions<'_>,
        cancellation: Option<&AgentCancellation>,
        deadline: Option<Instant>,
        mut visitor: F,
    ) -> anyhow::Result<WorkspaceVisitStatus>
    where
        F: FnMut(PathBuf) -> anyhow::Result<bool>,
    {
        self.visit_entries_with_environment_and_bounds(
            options,
            cancellation,
            &P4Environment::process(),
            false,
            WorkspaceWalkBounds {
                deadline,
                max_entries: None,
            },
            |path, ty| {
                if ty.is_file() {
                    visitor(path.to_path_buf())
                } else {
                    Ok(true)
                }
            },
        )
    }

    pub(super) fn visit_entries_with_bounds<F>(
        &self,
        options: WorkspaceWalkOptions<'_>,
        cancellation: Option<&AgentCancellation>,
        deadline: Instant,
        max_entries: usize,
        visitor: F,
    ) -> anyhow::Result<WorkspaceVisitStatus>
    where
        F: FnMut(&Path, fs::FileType) -> anyhow::Result<bool>,
    {
        self.visit_entries_with_environment_and_bounds(
            options,
            cancellation,
            &P4Environment::process(),
            false,
            WorkspaceWalkBounds {
                deadline: Some(deadline),
                max_entries: Some(max_entries),
            },
            visitor,
        )
    }
}

fn configured_walk(
    options: &WorkspaceWalkOptions<'_>,
    environment: &P4Environment,
    sorted: bool,
    deadline: Option<Instant>,
) -> anyhow::Result<(ignore::Walk, P4Matcher)> {
    let p4_matcher = P4Matcher::from_root(options.root, environment)?;
    let mut builder = workspace_walk_builder(
        options.root,
        options.skip_dirs,
        deadline,
        Some(&p4_matcher),
        false,
    );
    if sorted {
        builder.sort_by_file_path(|a, b| a.cmp(b));
    }
    Ok((builder.build(), p4_matcher))
}

fn workspace_walk_builder(
    root: &Path,
    skip_dirs: &[&str],
    deadline: Option<Instant>,
    p4_matcher: Option<&P4Matcher>,
    ordinary_paths_only: bool,
) -> WalkBuilder {
    let skip_dirs = skip_dirs
        .iter()
        .map(|name| (*name).to_string())
        .collect::<HashSet<_>>();
    let p4_filter = p4_matcher.cloned();
    let p4_root = root.to_path_buf();
    let mut builder = WalkBuilder::new(root);
    builder
        .standard_filters(p4_matcher.is_some())
        .current_dir(root)
        .follow_links(false);
    builder.filter_entry(move |entry| {
        // Yield an expired entry so the outer loop can stop, rather than
        // rejecting entries and making next() scan the remaining siblings.
        // ignore applies its built-in ignore filters before this callback.
        if deadline.is_some_and(|deadline| Instant::now() >= deadline) {
            return true;
        }
        let is_dir = entry.file_type().is_some_and(|x| x.is_dir());
        let name = entry.file_name().to_string_lossy();
        if name == ".git" || (is_dir && skip_dirs.contains(name.as_ref())) {
            return false;
        }
        if entry.path() == p4_root {
            return true;
        }
        !(is_dir
            && ((ordinary_paths_only && name.starts_with('.'))
                || p4_filter
                    .as_ref()
                    .is_some_and(|matcher| matcher.should_prune(entry.path()))))
    });
    builder
}

/// Collect ordinary paths first so hidden trees cannot consume their budget.
pub(crate) fn autocomplete_file_paths(
    root: &Path,
    respect_ignore_files: bool,
    max_candidates: usize,
    max_visited_entries: usize,
) -> anyhow::Result<Vec<PathBuf>> {
    autocomplete_file_paths_with_environment(
        root,
        respect_ignore_files,
        max_candidates,
        max_visited_entries,
        &P4Environment::process(),
    )
}

fn autocomplete_file_paths_with_environment(
    root: &Path,
    respect_ignore_files: bool,
    max_candidates: usize,
    max_visited_entries: usize,
    environment: &P4Environment,
) -> anyhow::Result<Vec<PathBuf>> {
    let p4_matcher = respect_ignore_files
        .then(|| P4Matcher::from_root(root, environment))
        .transpose()?;
    let mut paths = Vec::new();
    let mut visited = 0;
    for ordinary_paths_only in [true, false] {
        if paths.len() >= max_candidates || visited >= max_visited_entries {
            break;
        }
        let mut builder =
            workspace_walk_builder(root, &[], None, p4_matcher.as_ref(), ordinary_paths_only);
        builder
            .hidden(false)
            .require_git(false)
            .sort_by_file_path(|left, right| left.cmp(right));
        for entry in builder.build().take(max_visited_entries - visited) {
            visited += 1;
            let Ok(entry) = entry else { continue };
            if !entry.file_type().is_some_and(|ty| ty.is_file()) {
                continue;
            }
            let path = entry.path();
            let Ok(relative) = path.strip_prefix(root) else {
                continue;
            };
            let in_dotfolder = relative.parent().is_some_and(|parent| {
                parent
                    .components()
                    .any(|component| component.as_os_str().to_string_lossy().starts_with('.'))
            });
            if in_dotfolder == ordinary_paths_only
                || p4_matcher
                    .as_ref()
                    .is_some_and(|matcher| matcher.is_ignored(path, false))
            {
                continue;
            }
            paths.push(path.to_path_buf());
            if paths.len() >= max_candidates {
                break;
            }
        }
        if let Some(error) = p4_matcher.as_ref().and_then(|matcher| matcher.take_error()) {
            return Err(error);
        }
    }
    Ok(paths)
}