bb7f8ae79d
Explorer: foldable calendar, hand-laid toolbar, no empty Bookmarks block, Move to... picker, folder notes, Trash tab. Menu bar as flat text, title bar drawn whole each frame. New Outline pane, scrolling Related tab, performance overlay (fps, frame time, memory, CPU), first-run UI scale from the desktop on Linux. Editor: Extract to a new note, Merge into..., reading mode, indent settings, table row/column controls, tags in notes searchable, search order and collapse, excluded search folders, user themes in the settings folder. Engine: note.extract, note.merge, trash.list/restore/empty. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
265 lines
14 KiB
Rust
265 lines
14 KiB
Rust
//! Vim preferences and the engine toggle command (SPEC 6 item 29).
|
|
|
|
mod common;
|
|
|
|
use common::{TempVault, temp_config, temp_vault};
|
|
use notescraft_engine::{AppSettings, Error, Registry, Session, load_at, register_core, save_at};
|
|
use serde_json::{Value, json};
|
|
|
|
/// An isolated registry, read-only vault and application settings folder.
|
|
fn fixture(name: &str) -> (Registry<Session>, Session, TempVault, TempVault) {
|
|
let vault = temp_vault(name);
|
|
let config = temp_config(name);
|
|
let mut registry = Registry::new();
|
|
register_core(&mut registry).expect("register");
|
|
let mut session = Session::open_read_only(vault.path()).expect("open read-only");
|
|
session.use_settings_dir(config.path());
|
|
(registry, session, vault, config)
|
|
}
|
|
|
|
#[test]
|
|
fn vim_defaults_off_in_new_and_older_documents() {
|
|
for document in [json!({}), json!({ "editor": {} })] {
|
|
let mut settings: AppSettings = serde_json::from_value(document).expect("missing keys use defaults");
|
|
assert_eq!(settings.validate(), Vec::<String>::new());
|
|
let value = serde_json::to_value(settings).expect("serialize settings");
|
|
assert_eq!(value.pointer("/editor/vim"), Some(&json!(false)));
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn vim_accepts_only_boolean_values() {
|
|
for value in [json!("true"), json!(1), Value::Null, json!([]), json!({})] {
|
|
assert!(serde_json::from_value::<AppSettings>(json!({ "editor": { "vim": value } })).is_err(), "{value}");
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn vim_survives_toml_save_and_reload() {
|
|
let config = temp_config("vim-round-trip");
|
|
let mut before: AppSettings = serde_json::from_value(json!({ "editor": { "vim": true } })).expect("boolean preference");
|
|
assert_eq!(before.validate(), Vec::<String>::new());
|
|
save_at(config.path(), &before).expect("save");
|
|
let (after, notes) = load_at(config.path());
|
|
assert!(notes.is_empty(), "{notes:?}");
|
|
assert_eq!(after, before);
|
|
let value = serde_json::to_value(after).expect("serialize settings");
|
|
assert_eq!(value.pointer("/editor/vim"), Some(&json!(true)));
|
|
}
|
|
|
|
#[test]
|
|
fn invalid_editor_section_recovers_without_losing_other_settings() {
|
|
let config = temp_config("vim-invalid-file");
|
|
std::fs::write(config.path().join("config.toml"), "schema_version = 1\n\n[editor]\nvim = \"true\"\n\n[session]\ntheme = \"latte\"\n")
|
|
.expect("write malformed editor section");
|
|
let (settings, notes) = load_at(config.path());
|
|
let value = serde_json::to_value(&settings).expect("serialize settings");
|
|
assert_eq!(value.pointer("/editor/vim"), Some(&json!(false)));
|
|
assert_eq!(settings.session.theme, "latte");
|
|
assert!(notes.iter().any(|note| note.starts_with("the `editor` section was replaced by its defaults")), "{notes:?}");
|
|
}
|
|
|
|
#[test]
|
|
fn settings_commands_set_validate_and_reset_vim() {
|
|
let (registry, mut session, _vault, config) = fixture("vim-settings-commands");
|
|
registry.run("settings.set", &mut session, &json!({ "key": "editor.vim", "value": true })).expect("enable Vim");
|
|
let settings = registry.run("settings.get", &mut session, &Value::Null).expect("get settings");
|
|
assert_eq!(settings.pointer("/settings/editor/vim"), Some(&json!(true)));
|
|
|
|
for (key, value, code) in [("editor.vim", json!("true"), "invalid_value"), ("editor.unknown", json!(true), "unknown_key")] {
|
|
let result = registry.run("settings.set", &mut session, &json!({ "key": key, "value": value }));
|
|
assert!(matches!(result, Err(Error::Command { ref source, .. }) if source.code == code), "{result:?}");
|
|
}
|
|
let (loaded, _) = load_at(config.path());
|
|
assert_eq!(serde_json::to_value(loaded).expect("serialize settings").pointer("/editor/vim"), Some(&json!(true)));
|
|
|
|
registry.run("settings.reset", &mut session, &json!({ "key": "editor.vim" })).expect("reset Vim");
|
|
let settings = registry.run("settings.get", &mut session, &Value::Null).expect("get settings");
|
|
assert_eq!(settings.pointer("/settings/editor/vim"), Some(&json!(false)));
|
|
}
|
|
|
|
#[test]
|
|
fn toggle_vim_persists_each_direction_even_with_read_only_vault() {
|
|
let (registry, mut session, vault, config) = fixture("vim-toggle");
|
|
assert!(registry.is_enabled("editor.toggle_vim", &session));
|
|
registry.run("settings.set", &mut session, &json!({ "key": "session.theme", "value": "latte" })).expect("preserve unrelated setting");
|
|
for enabled in [true, false] {
|
|
let result = registry.run("editor.toggle_vim", &mut session, &Value::Null).expect("toggle Vim");
|
|
assert_eq!(result.get("vim"), Some(&json!(enabled)));
|
|
let (settings, notes) = load_at(config.path());
|
|
assert!(notes.is_empty(), "{notes:?}");
|
|
assert_eq!(settings.session.theme, "latte");
|
|
assert_eq!(serde_json::to_value(settings).expect("serialize settings").pointer("/editor/vim"), Some(&json!(enabled)));
|
|
}
|
|
assert!(!vault.path().join("config.toml").exists());
|
|
}
|
|
|
|
#[test]
|
|
fn toggle_vim_is_listed_in_the_edit_menu() {
|
|
let (registry, session, _vault, _config) = fixture("vim-toggle-menu");
|
|
let commands = registry.list(&session);
|
|
let command = commands.iter().find(|command| command.id == "editor.toggle_vim").expect("Vim command metadata");
|
|
assert_eq!(command.label, "Vim mode");
|
|
assert_eq!(command.menu_path, vec!["Edit"]);
|
|
}
|
|
|
|
#[test]
|
|
fn toggle_vim_refuses_to_overwrite_a_newer_schema() {
|
|
let (registry, mut session, _vault, config) = fixture("vim-toggle-newer-schema");
|
|
let original = "schema_version = 2\n\n[editor]\nvim = false\n";
|
|
let path = config.path().join("config.toml");
|
|
std::fs::write(&path, original).expect("write newer schema");
|
|
let result = registry.run("editor.toggle_vim", &mut session, &Value::Null);
|
|
assert!(matches!(result, Err(Error::Command { ref source, .. }) if source.code == "settings"), "{result:?}");
|
|
assert_eq!(std::fs::read_to_string(path).expect("read unchanged settings"), original);
|
|
}
|
|
|
|
/// The editor defaults the document holds, as JSON.
|
|
fn editor_defaults() -> serde_json::Value {
|
|
serde_json::json!({
|
|
"default_view": "visual",
|
|
"font_size": 16,
|
|
"readable_line_length": true,
|
|
"line_numbers": false,
|
|
"strict_line_breaks": false,
|
|
})
|
|
}
|
|
|
|
/// The defaults of the layout keys, with the switches the file may already have.
|
|
fn editor_defaults_document() -> serde_json::Value {
|
|
serde_json::json!({
|
|
"blocks": { "grip": true, "toolbar": true, "slash_preview": true },
|
|
"default_view": "visual",
|
|
"font_size": 16,
|
|
"indent_width": 4,
|
|
"indent_with_tabs": false,
|
|
"line_numbers": false,
|
|
"previews": { "colors": true, "named_colors": false },
|
|
"readable_line_length": true,
|
|
"strict_line_breaks": false,
|
|
"vim": false,
|
|
})
|
|
}
|
|
|
|
/// The editor section as it sits in `config.toml`.
|
|
fn saved_editor(config: &TempVault) -> Value {
|
|
let (settings, notes) = load_at(config.path());
|
|
assert_eq!(notes, &[] as &[String], "{notes:?}");
|
|
serde_json::to_value(&settings).expect("serialize settings").pointer("/editor").cloned().unwrap_or_default()
|
|
}
|
|
|
|
/// Every layout key defaults the way the design says, and a `config.toml`
|
|
/// written before they existed loads unchanged.
|
|
#[test]
|
|
fn editor_layout_defaults_and_older_documents() {
|
|
let mut settings = AppSettings::default();
|
|
assert_eq!(settings.validate(), Vec::<String>::new());
|
|
let editor = serde_json::to_value(&settings).expect("serialize settings").pointer("/editor").cloned().unwrap_or_default();
|
|
assert_eq!(editor, editor_defaults_document(), "the whole editor section is at its defaults");
|
|
|
|
let config = temp_config("editor-layout-old-file");
|
|
std::fs::write(config.path().join("config.toml"), b"schema_version = 1\n\n[editor]\nvim = false\n\n[editor.blocks]\ngrip = false\n")
|
|
.expect("write the old file");
|
|
let (loaded, notes) = load_at(config.path());
|
|
assert_eq!(notes, &[] as &[String], "{notes:?}");
|
|
assert!(!loaded.editor.blocks.grip, "the keys the file has are kept");
|
|
let editor = serde_json::to_value(&loaded).expect("serialize settings").pointer("/editor").cloned().unwrap_or_default();
|
|
assert_eq!(editor.pointer("/default_view"), Some(&serde_json::json!("visual")), "{editor}");
|
|
assert_eq!(editor.pointer("/font_size"), Some(&serde_json::json!(16)), "{editor}");
|
|
assert_eq!(editor.pointer("/readable_line_length"), Some(&serde_json::json!(true)), "{editor}");
|
|
assert_eq!(editor.pointer("/line_numbers"), Some(&serde_json::json!(false)), "{editor}");
|
|
assert_eq!(editor.pointer("/strict_line_breaks"), Some(&serde_json::json!(false)), "{editor}");
|
|
}
|
|
|
|
/// Every layout key round-trips through `settings.set`, comes back out of
|
|
/// `config.toml`, and `settings.reset` puts it back.
|
|
#[test]
|
|
fn editor_layout_keys_round_trip_and_reset() {
|
|
let (registry, mut session, _vault, config) = fixture("editor-layout-round-trip");
|
|
let set = |session: &mut Session, key: &str, value: Value| {
|
|
let params = serde_json::json!({ "key": key, "value": value });
|
|
registry.run("settings.set", session, ¶ms)
|
|
};
|
|
let changed = [
|
|
("editor.default_view", serde_json::json!("source")),
|
|
("editor.font_size", serde_json::json!(22)),
|
|
("editor.readable_line_length", serde_json::json!(false)),
|
|
("editor.line_numbers", serde_json::json!(true)),
|
|
("editor.strict_line_breaks", serde_json::json!(true)),
|
|
];
|
|
for (key, value) in &changed {
|
|
assert!(set(&mut session, key, value.clone()).is_ok(), "{key} must be settable");
|
|
}
|
|
|
|
let got = registry.run("settings.get", &mut session, &Value::Null).expect("get settings");
|
|
let editor = got.pointer("/settings/editor").cloned().unwrap_or_default();
|
|
for (key, value) in &changed {
|
|
let field = key.trim_start_matches("editor.");
|
|
assert_eq!(editor.pointer(&format!("/{field}")), Some(value), "{key} in settings.get: {editor}");
|
|
assert_eq!(saved_editor(&config).pointer(&format!("/{field}")), Some(value), "{key} in config.toml");
|
|
}
|
|
|
|
for (key, _) in &changed {
|
|
let params = serde_json::json!({ "key": key });
|
|
assert!(registry.run("settings.reset", &mut session, ¶ms).is_ok(), "{key} must be resettable");
|
|
let field = key.trim_start_matches("editor.");
|
|
let default = editor_defaults().pointer(&format!("/{field}")).cloned().unwrap_or_default();
|
|
assert_eq!(saved_editor(&config).pointer(&format!("/{field}")), Some(&default), "{key} back to its default");
|
|
}
|
|
}
|
|
|
|
/// A font size outside `10..=32` is clamped with a note, both from a file and
|
|
/// through `settings.set` (the same convention as `git.auto_commit_minutes`).
|
|
#[test]
|
|
fn editor_font_size_is_clamped_with_a_note() {
|
|
let config = temp_config("editor-font-clamp-file");
|
|
std::fs::write(config.path().join("config.toml"), "schema_version = 1\n\n[editor]\nfont_size = 3\n").expect("write a tiny font");
|
|
let (settings, notes) = load_at(config.path());
|
|
assert_eq!(settings.editor.font_size, 10, "3 is clamped to the smallest legible size");
|
|
let joined = notes.join("\n");
|
|
assert!(joined.contains("editor.font_size: 3 is outside 10..=32"), "{joined}");
|
|
|
|
let (registry, mut session, _vault, config) = fixture("editor-font-clamp-set");
|
|
let result =
|
|
registry.run("settings.set", &mut session, &serde_json::json!({ "key": "editor.font_size", "value": 200 })).expect("the set succeeds, repaired");
|
|
assert!(result.to_string().contains("editor.font_size: 200"), "{result}");
|
|
assert_eq!(saved_editor(&config).pointer("/font_size"), Some(&serde_json::json!(32)));
|
|
|
|
// A value that does not even fit a `u8` is refused, not clamped.
|
|
let too_big = registry.run("settings.set", &mut session, &serde_json::json!({ "key": "editor.font_size", "value": 300 }));
|
|
assert!(matches!(too_big, Err(Error::Command { ref source, .. }) if source.code == "invalid_value"), "{too_big:?}");
|
|
assert_eq!(saved_editor(&config).pointer("/font_size"), Some(&serde_json::json!(32)), "the clamped value survives the refusal");
|
|
}
|
|
|
|
/// Wrong types, unknown keys and unknown view names are refused, and a refused
|
|
/// set leaves `config.toml` byte-identical.
|
|
#[test]
|
|
fn editor_layout_refuses_bad_values_without_touching_the_file() {
|
|
let (registry, mut session, _vault, config) = fixture("editor-layout-refusals");
|
|
let set = |session: &mut Session, key: &str, value: Value| {
|
|
let params = serde_json::json!({ "key": key, "value": value });
|
|
registry.run("settings.set", session, ¶ms)
|
|
};
|
|
assert!(set(&mut session, "editor.font_size", serde_json::json!(20)).is_ok(), "one good value first");
|
|
let before = std::fs::read(config.path().join("config.toml")).expect("read the config file");
|
|
|
|
for (key, value, code) in [
|
|
("editor.default_view", serde_json::json!("rendering"), "invalid_value"),
|
|
("editor.default_view", serde_json::json!("Visual"), "invalid_value"),
|
|
("editor.default_view", serde_json::json!(1), "invalid_value"),
|
|
("editor.font_size", serde_json::json!("20"), "invalid_value"),
|
|
("editor.font_size", serde_json::json!(-1), "invalid_value"),
|
|
("editor.readable_line_length", serde_json::json!("yes"), "invalid_value"),
|
|
("editor.line_numbers", serde_json::json!(0), "invalid_value"),
|
|
("editor.strict_line_breaks", serde_json::json!(null), "invalid_value"),
|
|
("editor.unknown", serde_json::json!(true), "unknown_key"),
|
|
("editor.line_numbers.too", serde_json::json!(true), "unknown_key"),
|
|
] {
|
|
let result = set(&mut session, key, value.clone());
|
|
assert!(matches!(result, Err(Error::Command { ref source, .. }) if source.code == code), "`{key}` = {value} must fail with {code}, got {result:?}");
|
|
}
|
|
let after = std::fs::read(config.path().join("config.toml")).expect("read the config file again");
|
|
assert_eq!(before, after, "a refused value leaves config.toml byte-identical");
|
|
assert_eq!(saved_editor(&config).pointer("/font_size"), Some(&serde_json::json!(20)), "the last good value survives");
|
|
}
|