strop-engine 0.31.1

strop editor engine: documents, grammar dispatch, services, sessions — no terminal
Documentation
use super::*;

fn save(editor: &Editor) -> Result<bool, SessionError> {
    let Some(request) = capture_save(editor) else {
        return Ok(false);
    };
    request.persist()?;
    Ok(true)
}

#[test]
fn roundtrip_restores_buffers_and_position() {
    let dir = tempfile::tempdir().unwrap();
    let root = dir.path();
    std::fs::write(root.join("a.rs"), "fn a() {}\nfn b() {}\n").unwrap();
    let mut e = Editor::new(Buffer::open(root.join("a.rs")).unwrap());
    e.cwd = root.to_path_buf();
    e.state_dir = Some(root.join("state"));
    e.feed_text("jl");
    e.feed_text("ix");
    e.feed(crate::editor::Key::Esc);
    assert!(save(&e).unwrap());
    let mut e2 = Editor::new(Buffer::from_text(""));
    e2.cwd = root.to_path_buf();
    e2.state_dir = Some(root.join("state"));
    assert!(restore(&mut e2).unwrap());
    assert_eq!(e2.buf().path.as_deref(), Some(root.join("a.rs").as_path()));
    assert_eq!(e2.buf().line_of(e2.head()), 1);
    assert_eq!(e2.buf().col_of(e2.head()), 1);
    assert_eq!(
        e2.buf().history().depth(),
        1,
        "dirty history must not replay against the disk version"
    );
}

#[test]
fn empty_or_readonly_never_persist() {
    let dir = tempfile::tempdir().unwrap();
    let mut e = Editor::new(Buffer::from_text(""));
    e.cwd = dir.path().to_path_buf();
    e.state_dir = Some(dir.path().join("state"));
    assert!(!save(&e).unwrap());
    assert!(!dir.path().join("state").exists());
    let path = dir.path().join("a");
    std::fs::write(&path, "a").unwrap();
    let mut buf = Buffer::open(path).unwrap();
    buf.readonly = true;
    let mut e = Editor::new(buf);
    e.state_dir = Some(dir.path().join("state"));
    assert!(!save(&e).unwrap());
    assert!(!dir.path().join("state").exists());
}

#[test]
fn undo_history_crosses_when_disk_matches() {
    let dir = tempfile::tempdir().unwrap();
    let root = dir.path();
    std::fs::write(root.join("a.rs"), "fn a() {}\n").unwrap();
    let mut e = Editor::new(Buffer::open(root.join("a.rs")).unwrap());
    e.cwd = root.to_path_buf();
    e.state_dir = Some(root.join("state"));
    e.feed_text("o// note");
    e.feed(crate::editor::Key::Esc);
    e.feed_text(":w\r");
    e.wait_io().unwrap();
    assert!(save(&e).unwrap());
    let mut e2 = Editor::new(Buffer::from_text(""));
    e2.cwd = root.to_path_buf();
    e2.state_dir = Some(root.join("state"));
    assert!(restore(&mut e2).unwrap());
    assert!(
        e2.buf().text().to_string().contains("// note"),
        "restore starts from the saved revision"
    );
    e2.feed_text("u");
    assert_eq!(e2.buf().text().to_string(), "fn a() {}\n");
}

#[test]
fn trust_store_remembers_projects() {
    let dir = tempfile::tempdir().unwrap();
    let state = dir.path().join("state");
    let root = Path::new("/tmp/proj-a");
    assert!(!is_trusted(Some(&state), root).unwrap());
    trust(Some(&state), root).unwrap();
    assert!(is_trusted(Some(&state), root).unwrap());
    assert!(!is_trusted(Some(&state), Path::new("/tmp/proj-b")).unwrap());
    assert!(!is_trusted(None, root).unwrap());
}

#[test]
fn failed_restore_leaves_live_state_untouched() {
    let dir = tempfile::tempdir().unwrap();
    let path = dir.path().join("a");
    std::fs::write(&path, "a").unwrap();
    let e = Editor::new(Buffer::open(&path).unwrap());
    let mut session = capture(&e).unwrap();
    session.buffers.push(BufferState {
        path: dir.path().to_owned(),
        line: 0,
        col: 0,
        view_top: 0,
        undo: None,
        undo_hash: 0,
    });
    let mut live = Editor::new(Buffer::from_text("keep me"));
    let id = live.current();
    let generation = live.generation;
    assert!(session.restore(&mut live).is_err());
    assert_eq!(live.current(), id);
    assert_eq!(live.generation, generation);
    assert_eq!(live.buf().text().to_string(), "keep me");
}

#[test]
fn legacy_paths_migrate_and_ambiguous_paths_fail() {
    let make = |path: &str| {
        serde_json::json!({"buffers": [{
        "path": path, "line": 0, "col": 0, "view_top": 0, "undo": null
    }], "current": 0})
    };
    let session: Session = serde_json::from_value(make("safe.rs")).unwrap();
    let value = serde_json::to_value(session).unwrap();
    assert_eq!(value["buffers"][0]["path"]["version"], 1);
    assert!(serde_json::from_value::<Session>(make("bad\u{fffd}.rs")).is_err());
}

#[cfg(unix)]
#[test]
fn native_paths_roundtrip_without_loss() {
    use std::os::unix::ffi::OsStrExt;
    let dir = tempfile::tempdir().unwrap();
    let path = dir.path().join(std::ffi::OsStr::from_bytes(b"\xff.rs"));
    std::fs::write(&path, "native\n").unwrap();
    std::fs::write(dir.path().join("\u{fffd}.rs"), "wrong alias\n").unwrap();
    let mut e = Editor::new(Buffer::open(&path).unwrap());
    e.cwd = dir.path().to_owned();
    e.state_dir = Some(dir.path().join("state"));
    let request = capture_save(&e).unwrap();
    drop(e);
    std::thread::spawn(move || request.persist())
        .join()
        .unwrap()
        .unwrap();
    let mut live = Editor::new(Buffer::from_text(""));
    live.cwd = dir.path().to_owned();
    live.state_dir = Some(dir.path().join("state"));
    assert!(restore(&mut live).unwrap());
    assert_eq!(live.buf().path.as_deref(), Some(path.as_path()));
    assert_eq!(live.buf().text().to_string(), "native\n");
    use std::os::unix::fs::PermissionsExt;
    let target = session_path(live.state_dir.as_deref(), &live.cwd).unwrap();
    assert_eq!(
        std::fs::metadata(target).unwrap().permissions().mode() & 0o777,
        0o600
    );
}

#[test]
fn failed_publication_preserves_previous_session_and_cleans_staging() {
    use std::io::Write;
    let directory = tempfile::tempdir().unwrap();
    let path = directory.path().join("session.json");
    std::fs::write(&path, b"previous session").unwrap();
    let result = persistence::publish(&path, |file| {
        #[cfg(unix)]
        {
            use std::os::unix::fs::PermissionsExt;
            assert_eq!(file.metadata()?.permissions().mode() & 0o777, 0o600);
        }
        file.write_all(b"partial private content")?;
        Err(std::io::Error::other("injected sync failure"))
    });
    assert!(matches!(result, Err(SessionError::Io { .. })));
    assert_eq!(std::fs::read(&path).unwrap(), b"previous session");
    assert_eq!(
        std::fs::read_dir(directory.path()).unwrap().count(),
        1,
        "no abandoned private staging file"
    );
    persistence::publish(&path, |file| {
        file.write_all(b"next session")
            .and_then(|()| file.sync_all())
    })
    .unwrap();
    assert_eq!(std::fs::read(&path).unwrap(), b"next session");
}

#[cfg(unix)]
#[test]
fn atomic_publication_replaces_destination_symlink_without_touching_target() {
    use std::io::Write;
    use std::os::unix::fs::{symlink, PermissionsExt};
    let directory = tempfile::tempdir().unwrap();
    let outside = directory.path().join("outside");
    let path = directory.path().join("session");
    std::fs::write(&outside, b"untouched").unwrap();
    symlink(&outside, &path).unwrap();
    persistence::publish(&path, |file| {
        assert_eq!(file.metadata()?.permissions().mode() & 0o777, 0o600);
        file.write_all(b"private session")?;
        file.sync_all()
    })
    .unwrap();
    assert_eq!(std::fs::read(&outside).unwrap(), b"untouched");
    assert!(!std::fs::symlink_metadata(&path)
        .unwrap()
        .file_type()
        .is_symlink());
    assert_eq!(std::fs::read(&path).unwrap(), b"private session");
    assert_eq!(
        std::fs::metadata(&path).unwrap().permissions().mode() & 0o777,
        0o600
    );
}