knave 0.14.5

A Lisp programming language for humans and agents that infers types and ownership and compiles to readable Rust
Documentation
use std::fs;
use std::path::Path;

use super::crate_section::CrateSection;
use super::exit_status::ExitStatus;
use super::library_walk::FetchingStore;
use super::located_project::LocatedProject;
use super::program_file::ProgramFile;
use super::standard_error::StandardError;
use super::terminal::Terminal;
use crate::format::{ModuleLookup, SourceFormatter};
use crate::language::Language;
use crate::project::{CrateDependency, ProjectEditor};

pub struct ProjectFileEdit {
    project: LocatedProject,
}

impl ProjectFileEdit {
    const STAGED_SUFFIX: &str = ".tmp";

    pub fn of_working_directory(terminal: &Terminal, command: &str) -> Result<Self, ExitStatus> {
        let working_directory = Path::new(Terminal::WORKING_DIRECTORY);
        match LocatedProject::search_from(working_directory, terminal, FetchingStore::default())? {
            Some(project) => Ok(Self { project }),
            None => Err(Self::report_no_project(command)),
        }
    }

    pub fn listed_crate(&self, section: CrateSection, name: &str) -> Option<&CrateDependency> {
        self.project.own_crate(section, name)
    }

    pub fn apply(
        &self,
        terminal: &Terminal,
        edit: impl Fn(&ProjectEditor<'_>) -> Option<String>,
    ) -> Result<bool, ExitStatus> {
        let path = self.project.project_file();
        let mut scratch = terminal.scratch();
        let source = scratch.load(&path)?;
        let text = scratch.source_file(source).text().to_string();
        let forms = scratch.read_source(source)?;
        let editor =
            ProjectEditor::of(&text, &forms).expect("a loaded project file holds a project form");
        let Some(edited) = edit(&editor) else {
            return Ok(false);
        };
        let formatted = self.formatted(&mut scratch, &path, edited);
        Self::replace(&path, &formatted)?;
        Ok(true)
    }

    fn replace(path: &Path, text: &str) -> Result<(), ExitStatus> {
        Self::replace_target(path, text)
            .map_err(|error| ProgramFile::report_unwritable(path, &error))
    }

    fn replace_target(path: &Path, text: &str) -> std::io::Result<()> {
        let path = &fs::canonicalize(path)?;
        let mut staged = path.as_os_str().to_os_string();
        staged.push(Self::STAGED_SUFFIX);
        let staged = Path::new(&staged);
        fs::write(staged, text)?;
        fs::set_permissions(staged, fs::metadata(path)?.permissions())?;
        fs::rename(staged, path)
    }

    fn formatted(&self, scratch: &mut Terminal, path: &Path, text: String) -> String {
        let directory = ProgramFile::directory_of(path);
        let root = ProgramFile::canonical(&directory).unwrap_or(directory);
        let source = scratch.add_source(path, text);
        SourceFormatter::default()
            .format(
                scratch.source_file(source),
                &ModuleLookup::beside_project_file(&root),
            )
            .unwrap_or_else(|failure| {
                unreachable!("the formatter reads and keeps an edited project file: {failure:?}")
            })
    }

    fn report_no_project(command: &str) -> ExitStatus {
        StandardError::write_line(format_args!(
            "error: no `{}` in this directory or a parent\nhelp: run `{} {command}` inside a project",
            Language::project_file_name(),
            Language::NAME
        ));
        ExitStatus::UsageError
    }
}