mirror of https://github.com/helix-editor/helix
use main application event loop
Use the Application's main event loop to allow LSP, file writes, etcpull/2359/head
parent
36e5809f63
commit
ee705dcb33
|
@ -4,7 +4,6 @@
|
||||||
use crate::{graphemes, movement::Direction, Range, Rope, Selection, Tendril, Transaction};
|
use crate::{graphemes, movement::Direction, Range, Rope, Selection, Tendril, Transaction};
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
|
|
||||||
use log::debug;
|
|
||||||
use smallvec::SmallVec;
|
use smallvec::SmallVec;
|
||||||
|
|
||||||
// Heavily based on https://github.com/codemirror/closebrackets/
|
// Heavily based on https://github.com/codemirror/closebrackets/
|
||||||
|
@ -123,7 +122,7 @@ impl Default for AutoPairs {
|
||||||
|
|
||||||
#[must_use]
|
#[must_use]
|
||||||
pub fn hook(doc: &Rope, selection: &Selection, ch: char, pairs: &AutoPairs) -> Option<Transaction> {
|
pub fn hook(doc: &Rope, selection: &Selection, ch: char, pairs: &AutoPairs) -> Option<Transaction> {
|
||||||
debug!("autopairs hook selection: {:#?}", selection);
|
log::trace!("autopairs hook selection: {:#?}", selection);
|
||||||
|
|
||||||
if let Some(pair) = pairs.get(ch) {
|
if let Some(pair) = pairs.get(ch) {
|
||||||
if pair.same() {
|
if pair.same() {
|
||||||
|
@ -225,9 +224,11 @@ fn get_next_range(
|
||||||
// other end of the grapheme to get to where the new characters
|
// other end of the grapheme to get to where the new characters
|
||||||
// are inserted, then move the head to where it should be
|
// are inserted, then move the head to where it should be
|
||||||
let prev_bound = graphemes::prev_grapheme_boundary(doc_slice, start_range.head);
|
let prev_bound = graphemes::prev_grapheme_boundary(doc_slice, start_range.head);
|
||||||
debug!(
|
log::trace!(
|
||||||
"prev_bound: {}, offset: {}, len_inserted: {}",
|
"prev_bound: {}, offset: {}, len_inserted: {}",
|
||||||
prev_bound, offset, len_inserted
|
prev_bound,
|
||||||
|
offset,
|
||||||
|
len_inserted
|
||||||
);
|
);
|
||||||
prev_bound + offset + len_inserted
|
prev_bound + offset + len_inserted
|
||||||
};
|
};
|
||||||
|
@ -302,7 +303,7 @@ fn handle_open(doc: &Rope, selection: &Selection, pair: &Pair) -> Transaction {
|
||||||
});
|
});
|
||||||
|
|
||||||
let t = transaction.with_selection(Selection::new(end_ranges, selection.primary_index()));
|
let t = transaction.with_selection(Selection::new(end_ranges, selection.primary_index()));
|
||||||
debug!("auto pair transaction: {:#?}", t);
|
log::debug!("auto pair transaction: {:#?}", t);
|
||||||
t
|
t
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -334,7 +335,7 @@ fn handle_close(doc: &Rope, selection: &Selection, pair: &Pair) -> Transaction {
|
||||||
});
|
});
|
||||||
|
|
||||||
let t = transaction.with_selection(Selection::new(end_ranges, selection.primary_index()));
|
let t = transaction.with_selection(Selection::new(end_ranges, selection.primary_index()));
|
||||||
debug!("auto pair transaction: {:#?}", t);
|
log::debug!("auto pair transaction: {:#?}", t);
|
||||||
t
|
t
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -374,7 +375,7 @@ fn handle_same(doc: &Rope, selection: &Selection, pair: &Pair) -> Transaction {
|
||||||
});
|
});
|
||||||
|
|
||||||
let t = transaction.with_selection(Selection::new(end_ranges, selection.primary_index()));
|
let t = transaction.with_selection(Selection::new(end_ranges, selection.primary_index()));
|
||||||
debug!("auto pair transaction: {:#?}", t);
|
log::debug!("auto pair transaction: {:#?}", t);
|
||||||
t
|
t
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
use arc_swap::{access::Map, ArcSwap};
|
use arc_swap::{access::Map, ArcSwap};
|
||||||
|
use futures_util::Stream;
|
||||||
use helix_core::{
|
use helix_core::{
|
||||||
config::{default_syntax_loader, user_syntax_loader},
|
config::{default_syntax_loader, user_syntax_loader},
|
||||||
pos_at_coords, syntax, Selection,
|
pos_at_coords, syntax, Selection,
|
||||||
|
@ -27,7 +28,7 @@ use std::{
|
||||||
use anyhow::Error;
|
use anyhow::Error;
|
||||||
|
|
||||||
use crossterm::{
|
use crossterm::{
|
||||||
event::{DisableMouseCapture, EnableMouseCapture, Event, EventStream},
|
event::{DisableMouseCapture, EnableMouseCapture, Event},
|
||||||
execute, terminal,
|
execute, terminal,
|
||||||
tty::IsTty,
|
tty::IsTty,
|
||||||
};
|
};
|
||||||
|
@ -68,7 +69,7 @@ fn setup_integration_logging() {
|
||||||
message
|
message
|
||||||
))
|
))
|
||||||
})
|
})
|
||||||
.level(log::LevelFilter::Info)
|
.level(log::LevelFilter::Debug)
|
||||||
.chain(std::io::stdout())
|
.chain(std::io::stdout())
|
||||||
.apply();
|
.apply();
|
||||||
}
|
}
|
||||||
|
@ -225,8 +226,10 @@ impl Application {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn event_loop(&mut self) {
|
pub async fn event_loop<S>(&mut self, input_stream: &mut S)
|
||||||
let mut reader = EventStream::new();
|
where
|
||||||
|
S: Stream<Item = crossterm::Result<crossterm::event::Event>> + Unpin,
|
||||||
|
{
|
||||||
let mut last_render = Instant::now();
|
let mut last_render = Instant::now();
|
||||||
let deadline = Duration::from_secs(1) / 60;
|
let deadline = Duration::from_secs(1) / 60;
|
||||||
|
|
||||||
|
@ -242,7 +245,7 @@ impl Application {
|
||||||
tokio::select! {
|
tokio::select! {
|
||||||
biased;
|
biased;
|
||||||
|
|
||||||
Some(event) = reader.next() => {
|
Some(event) = input_stream.next() => {
|
||||||
self.handle_terminal_events(event)
|
self.handle_terminal_events(event)
|
||||||
}
|
}
|
||||||
Some(signal) = self.signals.next() => {
|
Some(signal) = self.signals.next() => {
|
||||||
|
@ -749,7 +752,10 @@ impl Application {
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn run(&mut self) -> Result<i32, Error> {
|
pub async fn run<S>(&mut self, input_stream: &mut S) -> Result<i32, Error>
|
||||||
|
where
|
||||||
|
S: Stream<Item = crossterm::Result<crossterm::event::Event>> + Unpin,
|
||||||
|
{
|
||||||
self.claim_term().await?;
|
self.claim_term().await?;
|
||||||
|
|
||||||
// Exit the alternate screen and disable raw mode before panicking
|
// Exit the alternate screen and disable raw mode before panicking
|
||||||
|
@ -764,16 +770,20 @@ impl Application {
|
||||||
hook(info);
|
hook(info);
|
||||||
}));
|
}));
|
||||||
|
|
||||||
self.event_loop().await;
|
self.event_loop(input_stream).await;
|
||||||
|
self.close().await?;
|
||||||
|
self.restore_term()?;
|
||||||
|
|
||||||
|
Ok(self.editor.exit_code)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn close(&mut self) -> anyhow::Result<()> {
|
||||||
self.jobs.finish().await;
|
self.jobs.finish().await;
|
||||||
|
|
||||||
if self.editor.close_language_servers(None).await.is_err() {
|
if self.editor.close_language_servers(None).await.is_err() {
|
||||||
log::error!("Timed out waiting for language servers to shutdown");
|
log::error!("Timed out waiting for language servers to shutdown");
|
||||||
};
|
};
|
||||||
|
|
||||||
self.restore_term()?;
|
Ok(())
|
||||||
|
|
||||||
Ok(self.editor.exit_code)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -95,6 +95,7 @@ impl Jobs {
|
||||||
/// Blocks until all the jobs that need to be waited on are done.
|
/// Blocks until all the jobs that need to be waited on are done.
|
||||||
pub async fn finish(&mut self) {
|
pub async fn finish(&mut self) {
|
||||||
let wait_futures = std::mem::take(&mut self.wait_futures);
|
let wait_futures = std::mem::take(&mut self.wait_futures);
|
||||||
|
log::debug!("waiting on jobs...");
|
||||||
wait_futures.for_each(|_| future::ready(())).await
|
wait_futures.for_each(|_| future::ready(())).await
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
use anyhow::{Context, Error, Result};
|
use anyhow::{Context, Error, Result};
|
||||||
|
use crossterm::event::EventStream;
|
||||||
use helix_term::application::Application;
|
use helix_term::application::Application;
|
||||||
use helix_term::args::Args;
|
use helix_term::args::Args;
|
||||||
use helix_term::config::Config;
|
use helix_term::config::Config;
|
||||||
|
@ -134,7 +135,7 @@ FLAGS:
|
||||||
// TODO: use the thread local executor to spawn the application task separately from the work pool
|
// TODO: use the thread local executor to spawn the application task separately from the work pool
|
||||||
let mut app = Application::new(args, config).context("unable to create new application")?;
|
let mut app = Application::new(args, config).context("unable to create new application")?;
|
||||||
|
|
||||||
let exit_code = app.run().await?;
|
let exit_code = app.run(&mut EventStream::new()).await?;
|
||||||
|
|
||||||
Ok(exit_code)
|
Ok(exit_code)
|
||||||
}
|
}
|
||||||
|
|
|
@ -17,7 +17,8 @@ mod integration {
|
||||||
Args::default(),
|
Args::default(),
|
||||||
Config::default(),
|
Config::default(),
|
||||||
("#[\n|]#", "ihello world<esc>", "hello world#[|\n]#"),
|
("#[\n|]#", "ihello world<esc>", "hello world#[|\n]#"),
|
||||||
)?;
|
)
|
||||||
|
.await?;
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
|
@ -18,7 +18,8 @@ async fn auto_indent_c() -> anyhow::Result<()> {
|
||||||
}
|
}
|
||||||
"},
|
"},
|
||||||
),
|
),
|
||||||
)?;
|
)
|
||||||
|
.await?;
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,7 +6,8 @@ async fn auto_pairs_basic() -> anyhow::Result<()> {
|
||||||
Args::default(),
|
Args::default(),
|
||||||
Config::default(),
|
Config::default(),
|
||||||
("#[\n|]#", "i(<esc>", "(#[|)]#\n"),
|
("#[\n|]#", "i(<esc>", "(#[|)]#\n"),
|
||||||
)?;
|
)
|
||||||
|
.await?;
|
||||||
|
|
||||||
test_key_sequence_text_result(
|
test_key_sequence_text_result(
|
||||||
Args::default(),
|
Args::default(),
|
||||||
|
@ -18,7 +19,8 @@ async fn auto_pairs_basic() -> anyhow::Result<()> {
|
||||||
..Default::default()
|
..Default::default()
|
||||||
},
|
},
|
||||||
("#[\n|]#", "i(<esc>", "(#[|\n]#"),
|
("#[\n|]#", "i(<esc>", "(#[|\n]#"),
|
||||||
)?;
|
)
|
||||||
|
.await?;
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,9 +1,12 @@
|
||||||
use std::io::Write;
|
use std::{io::Write, time::Duration};
|
||||||
|
|
||||||
|
use anyhow::bail;
|
||||||
use crossterm::event::{Event, KeyEvent};
|
use crossterm::event::{Event, KeyEvent};
|
||||||
use helix_core::{test, Selection, Transaction};
|
use helix_core::{test, Selection, Transaction};
|
||||||
use helix_term::{application::Application, args::Args, config::Config};
|
use helix_term::{application::Application, args::Args, config::Config};
|
||||||
use helix_view::{doc, input::parse_macro};
|
use helix_view::{doc, input::parse_macro};
|
||||||
|
use tokio::time::timeout;
|
||||||
|
use tokio_stream::wrappers::UnboundedReceiverStream;
|
||||||
|
|
||||||
#[derive(Clone, Debug)]
|
#[derive(Clone, Debug)]
|
||||||
pub struct TestCase {
|
pub struct TestCase {
|
||||||
|
@ -29,10 +32,44 @@ impl<S: Into<String>> From<(S, S, S)> for TestCase {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn test_key_sequence<T: Into<TestCase>>(
|
pub async fn test_key_sequence(
|
||||||
|
app: &mut Application,
|
||||||
|
in_keys: &str,
|
||||||
|
test_fn: Option<&dyn Fn(&Application)>,
|
||||||
|
) -> anyhow::Result<()> {
|
||||||
|
let (tx, rx) = tokio::sync::mpsc::unbounded_channel();
|
||||||
|
|
||||||
|
for key_event in parse_macro(&in_keys)?.into_iter() {
|
||||||
|
tx.send(Ok(Event::Key(KeyEvent::from(key_event))))?;
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut rx_stream = UnboundedReceiverStream::new(rx);
|
||||||
|
let event_loop = app.event_loop(&mut rx_stream);
|
||||||
|
let result = timeout(Duration::from_millis(500), event_loop).await;
|
||||||
|
|
||||||
|
if result.is_ok() {
|
||||||
|
bail!("application exited before test function could run");
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(test) = test_fn {
|
||||||
|
test(app);
|
||||||
|
};
|
||||||
|
|
||||||
|
for key_event in parse_macro("<esc>:q!<ret>")?.into_iter() {
|
||||||
|
tx.send(Ok(Event::Key(KeyEvent::from(key_event))))?;
|
||||||
|
}
|
||||||
|
|
||||||
|
let event_loop = app.event_loop(&mut rx_stream);
|
||||||
|
timeout(Duration::from_millis(5000), event_loop).await?;
|
||||||
|
app.close().await?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn test_key_sequence_with_input_text<T: Into<TestCase>>(
|
||||||
app: Option<Application>,
|
app: Option<Application>,
|
||||||
test_case: T,
|
test_case: T,
|
||||||
test_fn: &dyn Fn(&mut Application),
|
test_fn: &dyn Fn(&Application),
|
||||||
) -> anyhow::Result<()> {
|
) -> anyhow::Result<()> {
|
||||||
let test_case = test_case.into();
|
let test_case = test_case.into();
|
||||||
let mut app =
|
let mut app =
|
||||||
|
@ -50,23 +87,13 @@ pub fn test_key_sequence<T: Into<TestCase>>(
|
||||||
view.id,
|
view.id,
|
||||||
);
|
);
|
||||||
|
|
||||||
let input_keys = parse_macro(&test_case.in_keys)?
|
test_key_sequence(&mut app, &test_case.in_keys, Some(test_fn)).await
|
||||||
.into_iter()
|
|
||||||
.map(|key_event| Event::Key(KeyEvent::from(key_event)));
|
|
||||||
|
|
||||||
for key in input_keys {
|
|
||||||
app.handle_terminal_events(Ok(key));
|
|
||||||
}
|
|
||||||
|
|
||||||
test_fn(&mut app);
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Use this for very simple test cases where there is one input
|
/// Use this for very simple test cases where there is one input
|
||||||
/// document, selection, and sequence of key presses, and you just
|
/// document, selection, and sequence of key presses, and you just
|
||||||
/// want to verify the resulting document and selection.
|
/// want to verify the resulting document and selection.
|
||||||
pub fn test_key_sequence_text_result<T: Into<TestCase>>(
|
pub async fn test_key_sequence_text_result<T: Into<TestCase>>(
|
||||||
args: Args,
|
args: Args,
|
||||||
config: Config,
|
config: Config,
|
||||||
test_case: T,
|
test_case: T,
|
||||||
|
@ -74,7 +101,7 @@ pub fn test_key_sequence_text_result<T: Into<TestCase>>(
|
||||||
let test_case = test_case.into();
|
let test_case = test_case.into();
|
||||||
let app = Application::new(args, config).unwrap();
|
let app = Application::new(args, config).unwrap();
|
||||||
|
|
||||||
test_key_sequence(Some(app), test_case.clone(), &|app| {
|
test_key_sequence_with_input_text(Some(app), test_case.clone(), &|app| {
|
||||||
let doc = doc!(app.editor);
|
let doc = doc!(app.editor);
|
||||||
assert_eq!(&test_case.out_text, doc.text());
|
assert_eq!(&test_case.out_text, doc.text());
|
||||||
|
|
||||||
|
@ -83,9 +110,8 @@ pub fn test_key_sequence_text_result<T: Into<TestCase>>(
|
||||||
|
|
||||||
let sel = selections.pop().unwrap();
|
let sel = selections.pop().unwrap();
|
||||||
assert_eq!(test_case.out_selection, sel);
|
assert_eq!(test_case.out_selection, sel);
|
||||||
})?;
|
})
|
||||||
|
.await
|
||||||
Ok(())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn temp_file_with_contents<S: AsRef<str>>(content: S) -> tempfile::NamedTempFile {
|
pub fn temp_file_with_contents<S: AsRef<str>>(content: S) -> tempfile::NamedTempFile {
|
||||||
|
|
|
@ -14,25 +14,29 @@ async fn insert_mode_cursor_position() -> anyhow::Result<()> {
|
||||||
out_text: String::new(),
|
out_text: String::new(),
|
||||||
out_selection: Selection::single(0, 0),
|
out_selection: Selection::single(0, 0),
|
||||||
},
|
},
|
||||||
)?;
|
)
|
||||||
|
.await?;
|
||||||
|
|
||||||
test_key_sequence_text_result(
|
test_key_sequence_text_result(
|
||||||
Args::default(),
|
Args::default(),
|
||||||
Config::default(),
|
Config::default(),
|
||||||
("#[\n|]#", "i", "#[|\n]#"),
|
("#[\n|]#", "i", "#[|\n]#"),
|
||||||
)?;
|
)
|
||||||
|
.await?;
|
||||||
|
|
||||||
test_key_sequence_text_result(
|
test_key_sequence_text_result(
|
||||||
Args::default(),
|
Args::default(),
|
||||||
Config::default(),
|
Config::default(),
|
||||||
("#[\n|]#", "i<esc>", "#[|\n]#"),
|
("#[\n|]#", "i<esc>", "#[|\n]#"),
|
||||||
)?;
|
)
|
||||||
|
.await?;
|
||||||
|
|
||||||
test_key_sequence_text_result(
|
test_key_sequence_text_result(
|
||||||
Args::default(),
|
Args::default(),
|
||||||
Config::default(),
|
Config::default(),
|
||||||
("#[\n|]#", "i<esc>i", "#[|\n]#"),
|
("#[\n|]#", "i<esc>i", "#[|\n]#"),
|
||||||
)?;
|
)
|
||||||
|
.await?;
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
@ -44,7 +48,8 @@ async fn insert_to_normal_mode_cursor_position() -> anyhow::Result<()> {
|
||||||
Args::default(),
|
Args::default(),
|
||||||
Config::default(),
|
Config::default(),
|
||||||
("#[f|]#oo\n", "vll<A-;><esc>", "#[|foo]#\n"),
|
("#[f|]#oo\n", "vll<A-;><esc>", "#[|foo]#\n"),
|
||||||
)?;
|
)
|
||||||
|
.await?;
|
||||||
|
|
||||||
test_key_sequence_text_result(
|
test_key_sequence_text_result(
|
||||||
Args::default(),
|
Args::default(),
|
||||||
|
@ -60,7 +65,8 @@ async fn insert_to_normal_mode_cursor_position() -> anyhow::Result<()> {
|
||||||
#(|bar)#"
|
#(|bar)#"
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
)?;
|
)
|
||||||
|
.await?;
|
||||||
|
|
||||||
test_key_sequence_text_result(
|
test_key_sequence_text_result(
|
||||||
Args::default(),
|
Args::default(),
|
||||||
|
@ -76,7 +82,8 @@ async fn insert_to_normal_mode_cursor_position() -> anyhow::Result<()> {
|
||||||
#(ba|)#r"
|
#(ba|)#r"
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
)?;
|
)
|
||||||
|
.await?;
|
||||||
|
|
||||||
test_key_sequence_text_result(
|
test_key_sequence_text_result(
|
||||||
Args::default(),
|
Args::default(),
|
||||||
|
@ -92,7 +99,8 @@ async fn insert_to_normal_mode_cursor_position() -> anyhow::Result<()> {
|
||||||
#(b|)#ar"
|
#(b|)#ar"
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
)?;
|
)
|
||||||
|
.await?;
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,69 @@
|
||||||
|
use std::{
|
||||||
|
io::{Read, Write},
|
||||||
|
ops::RangeInclusive,
|
||||||
|
};
|
||||||
|
|
||||||
|
use helix_term::application::Application;
|
||||||
|
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_write() -> anyhow::Result<()> {
|
||||||
|
let mut file = tempfile::NamedTempFile::new().unwrap();
|
||||||
|
|
||||||
|
test_key_sequence(
|
||||||
|
&mut Application::new(
|
||||||
|
Args {
|
||||||
|
files: vec![(file.path().to_path_buf(), Position::default())],
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
Config::default(),
|
||||||
|
)?,
|
||||||
|
"ii can eat glass, it will not hurt me<ret><esc>:w<ret>",
|
||||||
|
None,
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
file.as_file_mut().flush()?;
|
||||||
|
file.as_file_mut().sync_all()?;
|
||||||
|
|
||||||
|
let mut file_content = String::new();
|
||||||
|
file.as_file_mut().read_to_string(&mut file_content)?;
|
||||||
|
assert_eq!("i can eat glass, it will not hurt me\n", file_content);
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_write_concurrent() -> anyhow::Result<()> {
|
||||||
|
let mut file = tempfile::NamedTempFile::new().unwrap();
|
||||||
|
let mut command = String::new();
|
||||||
|
const RANGE: RangeInclusive<i32> = 1..=1000;
|
||||||
|
|
||||||
|
for i in RANGE {
|
||||||
|
let cmd = format!("%c{}<esc>:w<ret>", i);
|
||||||
|
command.push_str(&cmd);
|
||||||
|
}
|
||||||
|
|
||||||
|
test_key_sequence(
|
||||||
|
&mut Application::new(
|
||||||
|
Args {
|
||||||
|
files: vec![(file.path().to_path_buf(), Position::default())],
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
Config::default(),
|
||||||
|
)?,
|
||||||
|
&command,
|
||||||
|
None,
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
file.as_file_mut().flush()?;
|
||||||
|
file.as_file_mut().sync_all()?;
|
||||||
|
|
||||||
|
let mut file_content = String::new();
|
||||||
|
file.as_file_mut().read_to_string(&mut file_content)?;
|
||||||
|
assert_eq!(RANGE.end().to_string(), file_content);
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
Loading…
Reference in New Issue