idet-core 0.4.0

Editing logic for text editors, without a frontend
Documentation
//! File dialogs and their document-state plumbing, shared by both frontends.

use std::{fmt::Debug, path::Path};

use document_state::{Document, FileFormat, SaveError};

/// What a save attempt did.
pub enum SaveOutcome {
    /// The document was written to disk.
    Saved,
    /// The user dismissed the dialog asking for a path.
    Cancelled,
    /// Writing failed, with a message ready to show.
    Failed(String),
}

/// What an open attempt did.
pub enum OpenOutcome<T: FileFormat + Clone> {
    /// The file was read and parsed.
    Opened(Box<Document<T>>),
    /// The user dismissed the dialog.
    Cancelled,
    /// Reading or parsing failed, with a message ready to show.
    Failed(String),
}

/// Saves `document` to `path`, adopting it as the document's own.
pub fn save_to<T: FileFormat + Clone>(document: &mut Document<T>, path: &Path) -> SaveOutcome {
    match document.save_as(path.to_path_buf()) {
        Ok(()) => SaveOutcome::Saved,
        Err(error) => SaveOutcome::Failed(format!("failed to save: {error}")),
    }
}

/// Saves `document` where it already lives, reporting a missing path as such.
pub fn save_in_place<T: FileFormat + Clone>(document: &mut Document<T>) -> Option<SaveOutcome> {
    match document.save() {
        Ok(()) => Some(SaveOutcome::Saved),
        Err(SaveError::NoPath) => None,
        Err(SaveError::Write(error)) => {
            Some(SaveOutcome::Failed(format!("failed to save: {error}")))
        }
    }
}

/// Saves `document`, asking for a path through a dialog when it has none.
#[cfg(feature = "dialogs")]
pub fn save<T: FileFormat + Clone>(document: &mut Document<T>) -> SaveOutcome {
    save_in_place(document).unwrap_or_else(|| save_as(document))
}

/// Asks for a path through a dialog and saves `document` there, adopting it.
#[cfg(feature = "dialogs")]
pub fn save_as<T: FileFormat + Clone>(document: &mut Document<T>) -> SaveOutcome {
    let Some(path) = rfd::FileDialog::new()
        .set_directory(document.dialog_directory())
        .save_file()
    else {
        return SaveOutcome::Cancelled;
    };
    save_to(document, &path)
}

/// Asks for a file through a dialog and opens it as a new document.
///
/// `current` only supplies the directory the dialog starts in.
#[cfg(feature = "dialogs")]
pub fn open<T: FileFormat + Clone>(current: &Document<T>) -> OpenOutcome<T>
where
    T::Error: Debug,
{
    let Some(path) = rfd::FileDialog::new()
        .set_directory(current.dialog_directory())
        .pick_file()
    else {
        return OpenOutcome::Cancelled;
    };
    open_path(&path)
}

/// Opens `path` as a new document, without any dialog.
#[must_use]
pub fn open_path<T: FileFormat + Clone>(path: &Path) -> OpenOutcome<T>
where
    T::Error: Debug,
{
    match Document::<T>::open(path) {
        Ok(document) => OpenOutcome::Opened(Box::new(document)),
        Err(error) => OpenOutcome::Failed(format!("failed to open {}: {error}", path.display())),
    }
}