607 lines
22 KiB
Rust
607 lines
22 KiB
Rust
use std::{
|
|
collections::BTreeMap,
|
|
io,
|
|
path::{Path, PathBuf},
|
|
};
|
|
|
|
use crate::{
|
|
editor::{Editor, Input, Invocation},
|
|
ruby::{Config, RubyPlugin, load_config},
|
|
};
|
|
|
|
/// Owns command eligibility and execution hosts together. Frontends can inspect
|
|
/// the editor, but cannot independently mutate its plugin registry.
|
|
pub struct EditorSession {
|
|
editor: Editor,
|
|
plugins: BTreeMap<String, RubyPlugin>,
|
|
}
|
|
|
|
impl EditorSession {
|
|
/// File errors fail opening; config and plugin errors leave core editing
|
|
/// available and are reported in the editor's message.
|
|
pub fn open(
|
|
file: Option<PathBuf>,
|
|
capabilities: Vec<String>,
|
|
config: Option<&Path>,
|
|
) -> io::Result<Self> {
|
|
let mut editor = Editor::open(file, capabilities)?;
|
|
let mut notices = Vec::new();
|
|
let config = match config {
|
|
Some(path) => load_config(path).unwrap_or_else(|error| {
|
|
notices.push(format!("config disabled: {error}"));
|
|
Config::default()
|
|
}),
|
|
None => Config::default(),
|
|
};
|
|
let mut plugins = BTreeMap::new();
|
|
for path in config.plugins {
|
|
match RubyPlugin::start(&path) {
|
|
Ok((plugin, feature)) => {
|
|
let name = feature.name.clone();
|
|
match editor.register(feature) {
|
|
Ok(()) => {
|
|
plugins.insert(name, plugin);
|
|
}
|
|
Err(error) => notices.push(format!("{}: {error}", path.display())),
|
|
}
|
|
}
|
|
Err(error) => notices.push(format!("{}: {error}", path.display())),
|
|
}
|
|
}
|
|
for binding in config.bindings {
|
|
if let Err(error) = editor.bind(&binding.mode, &binding.key, binding.command) {
|
|
notices.push(format!(
|
|
"binding {} {:?}: {error}",
|
|
binding.mode, binding.key
|
|
));
|
|
}
|
|
}
|
|
if notices.is_empty() {
|
|
editor.set_message("Ready • --no-config uses default keybindings");
|
|
} else {
|
|
editor.set_message(notices.join(" | "));
|
|
}
|
|
Ok(Self { editor, plugins })
|
|
}
|
|
|
|
pub fn editor(&self) -> &Editor {
|
|
&self.editor
|
|
}
|
|
|
|
pub fn handle(&mut self, input: Input) {
|
|
if let Some(invocation) = self.editor.handle(input) {
|
|
self.dispatch(invocation);
|
|
}
|
|
}
|
|
|
|
pub fn execute(&mut self, command: &str) {
|
|
if let Some(invocation) = self.editor.execute(command) {
|
|
self.dispatch(invocation);
|
|
}
|
|
}
|
|
|
|
fn dispatch(&mut self, invocation: Invocation) {
|
|
let result = match self.plugins.get_mut(&invocation.plugin) {
|
|
Some(plugin) => plugin
|
|
.invoke(
|
|
&invocation.command,
|
|
&invocation.args,
|
|
&self.editor.context(),
|
|
)
|
|
.and_then(|effects| self.editor.apply(effects)),
|
|
None => Err("plugin host is unavailable".into()),
|
|
};
|
|
if let Err(error) = result {
|
|
// Drop also stops a live host whose protocol-valid edits were rejected.
|
|
drop(self.plugins.remove(&invocation.plugin));
|
|
self.editor.disable_plugin(&invocation.plugin, &error);
|
|
}
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
use std::{
|
|
fs,
|
|
sync::atomic::{AtomicU64, Ordering},
|
|
};
|
|
|
|
static NEXT_FIXTURE: AtomicU64 = AtomicU64::new(0);
|
|
|
|
struct Fixture(PathBuf);
|
|
|
|
impl Fixture {
|
|
fn new() -> Self {
|
|
let path = std::env::temp_dir().join(format!(
|
|
"vamp-session-{}-{}",
|
|
std::process::id(),
|
|
NEXT_FIXTURE.fetch_add(1, Ordering::Relaxed),
|
|
));
|
|
fs::create_dir(&path).unwrap();
|
|
Self(path)
|
|
}
|
|
|
|
fn file(&self, name: &str, text: &str) -> PathBuf {
|
|
let path = self.0.join(name);
|
|
fs::write(&path, text).unwrap();
|
|
path
|
|
}
|
|
|
|
fn open(&self, config: &str) -> EditorSession {
|
|
let config = self.file("config.rb", config);
|
|
EditorSession::open(
|
|
Some(self.0.join("buffer.txt")),
|
|
vec!["text".into()],
|
|
Some(&config),
|
|
)
|
|
.unwrap()
|
|
}
|
|
|
|
fn healthy(&self) {
|
|
self.file(
|
|
"healthy.rb",
|
|
r#"
|
|
Vamp.plugin "healthy", requires: ["text"]
|
|
Vamp.command("healthy") { |editor, args| editor.replace "healthy #{args}" }
|
|
"#,
|
|
);
|
|
}
|
|
}
|
|
|
|
impl Drop for Fixture {
|
|
fn drop(&mut self) {
|
|
let _ = fs::remove_dir_all(&self.0);
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn startup_rejection_installs_no_commands_and_keeps_other_plugins_and_core() {
|
|
let fixture = Fixture::new();
|
|
fixture.healthy();
|
|
fixture.file(
|
|
"graphics.rb",
|
|
r#"
|
|
Vamp.plugin "graphics", requires: ["graphics"]
|
|
Vamp.command("draw") { |editor, args| editor.insert "incompatible" }
|
|
"#,
|
|
);
|
|
fixture.file(
|
|
"collision.rb",
|
|
r#"
|
|
Vamp.plugin "collision", requires: ["text"]
|
|
Vamp.command("leaked") { |editor, args| editor.insert "collision" }
|
|
Vamp.command("q") { |editor, args| editor.insert "override" }
|
|
"#,
|
|
);
|
|
let mut session = fixture.open(
|
|
r#"
|
|
Vamp.configure do |config|
|
|
config.plugin "graphics.rb"
|
|
config.plugin "collision.rb"
|
|
config.plugin "missing.rb"
|
|
config.plugin "healthy.rb"
|
|
config.bind "g", "healthy"
|
|
end
|
|
"#,
|
|
);
|
|
assert!(
|
|
session
|
|
.editor()
|
|
.message()
|
|
.contains("unavailable capability: graphics")
|
|
);
|
|
assert!(
|
|
session
|
|
.editor()
|
|
.message()
|
|
.contains("Command already registered: q")
|
|
);
|
|
for command in ["draw", "leaked"] {
|
|
session.execute(command);
|
|
assert_eq!(
|
|
session.editor().message(),
|
|
format!("Unknown command: {command}")
|
|
);
|
|
assert_eq!(session.editor().lines(), &[String::new()]);
|
|
}
|
|
session.handle(Input::Char('g'));
|
|
assert_eq!(session.editor().lines(), &["healthy "]);
|
|
session.execute("w");
|
|
assert_eq!(
|
|
fs::read_to_string(fixture.0.join("buffer.txt")).unwrap(),
|
|
"healthy "
|
|
);
|
|
session.execute("q");
|
|
assert!(session.editor().should_quit());
|
|
}
|
|
|
|
#[test]
|
|
fn rejected_effects_discard_entire_transaction_and_retire_commands_and_bindings() {
|
|
let fixture = Fixture::new();
|
|
fixture.healthy();
|
|
fixture.file(
|
|
"invalid.rb",
|
|
r#"
|
|
Vamp.plugin "invalid", requires: ["text"]
|
|
Vamp.command "invalid" do |editor, args|
|
|
editor.insert "partial"
|
|
editor.message "\e"
|
|
end
|
|
Vamp.command("sibling") { |editor, args| editor.insert "must be retired too" }
|
|
"#,
|
|
);
|
|
let mut session = fixture.open(
|
|
r#"
|
|
Vamp.configure do |config|
|
|
config.plugin "invalid.rb"
|
|
config.plugin "healthy.rb"
|
|
config.bind "g", "invalid"
|
|
end
|
|
"#,
|
|
);
|
|
session.handle(Input::Char('g'));
|
|
assert_eq!(session.editor().lines(), &[String::new()]);
|
|
assert!(!session.editor().dirty());
|
|
assert!(
|
|
session
|
|
.editor()
|
|
.message()
|
|
.contains("Plugin invalid disabled: Unsupported control character")
|
|
);
|
|
let message = session.editor().message().to_owned();
|
|
session.handle(Input::Char('g'));
|
|
assert_eq!(session.editor().message(), message);
|
|
for command in ["invalid", "sibling"] {
|
|
session.execute(command);
|
|
assert_eq!(
|
|
session.editor().message(),
|
|
format!("Unknown command: {command}")
|
|
);
|
|
}
|
|
session.execute("healthy survivor");
|
|
assert_eq!(session.editor().lines(), &["healthy survivor"]);
|
|
session.handle(Input::Char('u'));
|
|
assert_eq!(session.editor().lines(), &[String::new()]);
|
|
session.handle(Input::Char('u'));
|
|
assert_eq!(session.editor().message(), "Nothing to undo");
|
|
}
|
|
|
|
#[test]
|
|
fn callback_crash_and_timeout_retire_plugin_without_losing_core_editing() {
|
|
for failure in ["raise 'callback failed'", "Process.exit!(7)", "sleep 30"] {
|
|
let fixture = Fixture::new();
|
|
fixture.healthy();
|
|
fixture.file(
|
|
"failing.rb",
|
|
&format!(
|
|
r#"
|
|
Vamp.plugin "failing", requires: ["text"]
|
|
Vamp.command "fail" do |editor, args|
|
|
editor.insert "never commit"
|
|
{failure}
|
|
end
|
|
"#
|
|
),
|
|
);
|
|
let mut session = fixture.open(
|
|
r#"
|
|
Vamp.configure do |config|
|
|
config.plugin "failing.rb"
|
|
config.plugin "healthy.rb"
|
|
config.bind "g", "fail"
|
|
end
|
|
"#,
|
|
);
|
|
session.execute("fail");
|
|
assert_eq!(session.editor().lines(), &[String::new()]);
|
|
assert!(
|
|
session
|
|
.editor()
|
|
.message()
|
|
.contains("Plugin failing disabled:")
|
|
);
|
|
session.execute("fail");
|
|
assert_eq!(session.editor().message(), "Unknown command: fail");
|
|
session.handle(Input::Char('g'));
|
|
assert_eq!(session.editor().lines(), &[String::new()]);
|
|
session.execute("healthy survivor");
|
|
session.execute("w");
|
|
session.handle(Input::Char('i'));
|
|
session.handle(Input::Char('x'));
|
|
session.handle(Input::Escape);
|
|
session.execute("q");
|
|
assert!(!session.editor().should_quit());
|
|
session.execute("wq");
|
|
assert!(session.editor().should_quit());
|
|
assert_eq!(
|
|
fs::read_to_string(fixture.0.join("buffer.txt")).unwrap(),
|
|
"xhealthy survivor"
|
|
);
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn configured_bindings_preserve_modal_editing_history_and_literal_paste() {
|
|
let fixture = Fixture::new();
|
|
fixture.healthy();
|
|
let mut session = fixture.open(
|
|
r#"
|
|
Vamp.configure do |config|
|
|
config.plugin "healthy.rb"
|
|
config.bind "n", config.core.insert_first_nonblank
|
|
config.bind "i", nil
|
|
config.bind "ctrl+r", config.core.redo
|
|
config.bind "ctrl+c", config.core.normal, mode: config.modes.insert
|
|
config.bind "esc", nil, mode: config.modes.insert
|
|
config.bind "!", config.core.newline, mode: config.modes.insert
|
|
config.bind "shift+x", nil, mode: config.modes.insert
|
|
config.bind ":", nil
|
|
config.bind "space", config.core.command
|
|
config.bind "enter", nil, mode: config.modes.command
|
|
config.bind "ctrl+x", config.core.submit, mode: config.modes.command
|
|
config.bind "ctrl+c", config.core.normal, mode: config.modes.command
|
|
config.bind "ctrl+h", config.core.backspace, mode: config.modes.command
|
|
config.bind "g", config.core.undo
|
|
config.bind "g", "healthy"
|
|
config.bind "n", config.core.newline
|
|
config.bind "unknown", config.core.undo
|
|
config.bind "ctrl+f", "healthy", mode: config.modes.insert
|
|
end
|
|
"#,
|
|
);
|
|
session.handle(Input::Char('i'));
|
|
assert_eq!(session.editor().mode(), crate::editor::Mode::Normal);
|
|
session.handle(Input::Char('n'));
|
|
for ch in "界Xab!".chars() {
|
|
session.handle(Input::Char(ch));
|
|
}
|
|
assert_eq!(session.editor().lines(), &["界ab", ""]);
|
|
session.handle(Input::Escape);
|
|
assert_eq!(session.editor().mode(), crate::editor::Mode::Insert);
|
|
session.handle(Input::Paste("X!".into()));
|
|
assert_eq!(session.editor().lines(), &["界ab", "X!"]);
|
|
session.handle(Input::Ctrl('C'));
|
|
assert_eq!(session.editor().mode(), crate::editor::Mode::Normal);
|
|
session.handle(Input::Char('u'));
|
|
assert_eq!(session.editor().lines(), &["界ab", ""]);
|
|
session.handle(Input::Char('u'));
|
|
assert_eq!(session.editor().lines(), &[String::new()]);
|
|
session.handle(Input::Ctrl('r'));
|
|
session.handle(Input::Ctrl('r'));
|
|
assert_eq!(session.editor().lines(), &["界ab", "X!"]);
|
|
session.handle(Input::Char(':'));
|
|
assert_eq!(session.editor().mode(), crate::editor::Mode::Normal);
|
|
session.handle(Input::Char(' '));
|
|
for ch in "w界".chars() {
|
|
session.handle(Input::Char(ch));
|
|
}
|
|
session.handle(Input::Backspace);
|
|
assert_eq!(session.editor().command_line(), "w");
|
|
session.handle(Input::Enter);
|
|
assert_eq!(session.editor().mode(), crate::editor::Mode::Command);
|
|
session.handle(Input::Ctrl('x'));
|
|
assert_eq!(
|
|
fs::read_to_string(fixture.0.join("buffer.txt")).unwrap(),
|
|
"界ab\nX!"
|
|
);
|
|
session.handle(Input::Char(' '));
|
|
session.handle(Input::Char('q'));
|
|
session.handle(Input::Ctrl('c'));
|
|
assert_eq!(session.editor().mode(), crate::editor::Mode::Normal);
|
|
assert!(session.editor().command_line().is_empty());
|
|
assert!(!session.editor().should_quit());
|
|
session.handle(Input::Char('g'));
|
|
assert_eq!(session.editor().lines(), &["healthy "]);
|
|
session.handle(Input::Char(' '));
|
|
session.handle(Input::Char('q'));
|
|
session.handle(Input::Ctrl('x'));
|
|
assert!(!session.editor().should_quit());
|
|
session.handle(Input::Char(' '));
|
|
session.handle(Input::Char('q'));
|
|
session.handle(Input::Char('!'));
|
|
session.handle(Input::Ctrl('x'));
|
|
assert!(session.editor().should_quit());
|
|
}
|
|
|
|
#[test]
|
|
fn typed_visual_actions_copy_cut_paste_and_cancel_without_splitting_history() {
|
|
let fixture = Fixture::new();
|
|
fixture.file("buffer.txt", "one two");
|
|
let mut session = fixture.open(
|
|
r#"
|
|
Vamp.configure do |config|
|
|
config.bind "v", nil
|
|
config.bind "ctrl+v", config.core.select
|
|
config.bind "ctrl+c", config.core.copy, mode: config.modes.visual
|
|
config.bind "ctrl+x", config.core.cut, mode: config.modes.visual
|
|
config.bind "ctrl+g", config.core.normal, mode: config.modes.visual
|
|
config.bind "ctrl+f", config.core.word_end, mode: config.modes.visual
|
|
config.bind "ctrl+b", config.core.paste_before
|
|
config.bind "ctrl+p", config.core.paste_after
|
|
end
|
|
"#,
|
|
);
|
|
session.handle(Input::Char('v'));
|
|
assert_eq!(session.editor().mode(), crate::editor::Mode::Normal);
|
|
session.handle(Input::Ctrl('v'));
|
|
assert_eq!(session.editor().mode(), crate::editor::Mode::Visual);
|
|
session.handle(Input::Ctrl('f'));
|
|
assert_eq!(session.editor().selection().unwrap().end.column, 3);
|
|
session.handle(Input::Ctrl('c'));
|
|
assert_eq!(session.editor().mode(), crate::editor::Mode::Normal);
|
|
assert!(!session.editor().dirty());
|
|
session.handle(Input::Ctrl('p'));
|
|
assert_eq!(session.editor().lines(), &["oonene two"]);
|
|
session.handle(Input::Char('u'));
|
|
assert_eq!(session.editor().lines(), &["one two"]);
|
|
session.handle(Input::Ctrl('v'));
|
|
session.handle(Input::Ctrl('f'));
|
|
session.handle(Input::Ctrl('x'));
|
|
assert_eq!(session.editor().lines(), &[" two"]);
|
|
assert_eq!(session.editor().mode(), crate::editor::Mode::Normal);
|
|
session.handle(Input::Ctrl('b'));
|
|
assert_eq!(session.editor().lines(), &["one two"]);
|
|
session.handle(Input::Char('u'));
|
|
assert_eq!(session.editor().lines(), &[" two"]);
|
|
session.handle(Input::Char('u'));
|
|
assert_eq!(session.editor().lines(), &["one two"]);
|
|
assert!(!session.editor().dirty());
|
|
session.handle(Input::Ctrl('v'));
|
|
session.handle(Input::Ctrl('g'));
|
|
assert_eq!(session.editor().mode(), crate::editor::Mode::Normal);
|
|
assert_eq!(session.editor().selection(), None);
|
|
}
|
|
|
|
#[test]
|
|
fn typed_word_motions_remap_keys_and_keep_insert_typing_in_one_undo_group() {
|
|
let fixture = Fixture::new();
|
|
fixture.file("buffer.txt", "foo...bar baz");
|
|
let mut session = fixture.open(
|
|
r#"
|
|
Vamp.configure do |config|
|
|
config.bind "w", nil
|
|
config.bind "ctrl+f", config.core.word_next
|
|
config.bind "ctrl+p", config.core.word_previous
|
|
config.bind "ctrl+e", config.core.word_end
|
|
config.bind "ctrl+a", config.core.big_word_next
|
|
config.bind "ctrl+z", config.core.big_word_previous
|
|
config.bind "ctrl+t", config.core.big_word_end
|
|
config.bind "ctrl+b", config.core.word_previous, mode: config.modes.insert
|
|
end
|
|
"#,
|
|
);
|
|
session.handle(Input::Char('w'));
|
|
assert_eq!(session.editor().cursor().column, 0);
|
|
for (key, column) in [
|
|
('f', 3),
|
|
('e', 5),
|
|
('f', 6),
|
|
('p', 3),
|
|
('z', 0),
|
|
('a', 10),
|
|
('t', 12),
|
|
('z', 10),
|
|
('z', 0),
|
|
] {
|
|
session.handle(Input::Ctrl(key));
|
|
assert_eq!(session.editor().cursor().column, column, "{key}");
|
|
}
|
|
assert!(!session.editor().dirty());
|
|
session.handle(Input::Char('i'));
|
|
for ch in "wbeWBE".chars() {
|
|
session.handle(Input::Char(ch));
|
|
}
|
|
session.handle(Input::Ctrl('b'));
|
|
assert_eq!(session.editor().cursor().column, 0);
|
|
session.handle(Input::Char('X'));
|
|
session.handle(Input::Escape);
|
|
assert_eq!(session.editor().lines(), &["XwbeWBEfoo...bar baz"]);
|
|
session.handle(Input::Char('u'));
|
|
assert_eq!(session.editor().lines(), &["foo...bar baz"]);
|
|
assert!(!session.editor().dirty());
|
|
}
|
|
|
|
#[test]
|
|
fn typed_search_bindings_drive_navigation_and_the_search_prompt_only() {
|
|
let fixture = Fixture::new();
|
|
fixture.file("buffer.txt", "cat\ncat\ncat");
|
|
let mut session = fixture.open(
|
|
r#"
|
|
Vamp.configure do |config|
|
|
config.bind "ctrl+f", config.core.search
|
|
config.bind "ctrl+n", config.core.search_next
|
|
config.bind "ctrl+p", config.core.search_previous
|
|
config.bind "ctrl+x", config.core.submit, mode: config.modes.search
|
|
config.bind "ctrl+c", config.core.normal, mode: config.modes.search
|
|
config.bind "ctrl+h", config.core.backspace, mode: config.modes.search
|
|
config.bind "enter", nil, mode: config.modes.search
|
|
config.bind "/", nil
|
|
end
|
|
"#,
|
|
);
|
|
session.handle(Input::Char('/'));
|
|
assert_eq!(session.editor().mode(), crate::editor::Mode::Normal);
|
|
session.handle(Input::Ctrl('f'));
|
|
assert_eq!(session.editor().mode(), crate::editor::Mode::Search);
|
|
for ch in "cat界".chars() {
|
|
session.handle(Input::Char(ch));
|
|
}
|
|
session.handle(Input::Backspace);
|
|
session.handle(Input::Enter);
|
|
assert_eq!(session.editor().mode(), crate::editor::Mode::Search);
|
|
session.handle(Input::Ctrl('x'));
|
|
assert_eq!(session.editor().cursor().row, 1);
|
|
session.handle(Input::Ctrl('n'));
|
|
assert_eq!(session.editor().cursor().row, 2);
|
|
session.handle(Input::Ctrl('p'));
|
|
assert_eq!(session.editor().cursor().row, 1);
|
|
session.handle(Input::Ctrl('f'));
|
|
session.handle(Input::Char('q'));
|
|
session.handle(Input::Ctrl('c'));
|
|
session.handle(Input::Ctrl('n'));
|
|
assert_eq!(session.editor().cursor().row, 2);
|
|
assert!(!session.editor().dirty());
|
|
assert!(!session.editor().should_quit());
|
|
}
|
|
|
|
#[test]
|
|
fn typed_config_errors_discard_partial_bindings_and_leave_default_editing_available() {
|
|
let fixture = Fixture::new();
|
|
for (expression, error) in [
|
|
("config.core.reod", "NoMethodError"),
|
|
("config.modes.insret", "NoMethodError"),
|
|
(r#"config.bind "n", "core.insert""#, "TypeError"),
|
|
(r#"config.bind "n", nil, mode: "insert""#, "TypeError"),
|
|
(r#"config.bind "n", config.modes.insert"#, "TypeError"),
|
|
(
|
|
r#"config.bind "n", nil, mode: config.core.insert"#,
|
|
"TypeError",
|
|
),
|
|
(r#"config.core.redo.name = "core.undo""#, "FrozenError"),
|
|
(
|
|
r#"config.core.redo.name.replace("core.undo")"#,
|
|
"FrozenError",
|
|
),
|
|
(r#"config.modes.insert.name = "normal""#, "FrozenError"),
|
|
(
|
|
r#"config.modes.insert.name.replace("normal")"#,
|
|
"FrozenError",
|
|
),
|
|
] {
|
|
let mut session = fixture.open(&format!(
|
|
"Vamp.configure do |config|\n config.bind \"i\", nil\n {expression}\nend"
|
|
));
|
|
assert!(session.editor().message().contains(error), "{expression}");
|
|
session.handle(Input::Char('i'));
|
|
session.handle(Input::Char('界'));
|
|
session.handle(Input::Escape);
|
|
assert_eq!(session.editor().mode(), crate::editor::Mode::Normal);
|
|
assert_eq!(session.editor().lines(), &["界"], "{expression}");
|
|
session.handle(Input::Char('u'));
|
|
assert_eq!(session.editor().lines(), &[String::new()], "{expression}");
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn broken_config_falls_back_to_core_without_failing_the_session() {
|
|
let fixture = Fixture::new();
|
|
let mut session = fixture.open("raise 'broken config'");
|
|
assert!(
|
|
session
|
|
.editor()
|
|
.message()
|
|
.contains("config disabled: Ruby config: RuntimeError: broken config")
|
|
);
|
|
session.handle(Input::Char('i'));
|
|
session.handle(Input::Char('x'));
|
|
session.handle(Input::Escape);
|
|
session.execute("wq");
|
|
assert!(session.editor().should_quit());
|
|
assert_eq!(
|
|
fs::read_to_string(fixture.0.join("buffer.txt")).unwrap(),
|
|
"x"
|
|
);
|
|
}
|
|
}
|