use main application event loop

Use the Application's main event loop to allow LSP, file writes, etc
This commit is contained in:
Skyler Hawthorne 2022-04-19 01:21:31 -04:00
parent 36e5809f63
commit ee705dcb33
10 changed files with 169 additions and 49 deletions

View file

@ -4,7 +4,6 @@
use crate::{graphemes, movement::Direction, Range, Rope, Selection, Tendril, Transaction};
use std::collections::HashMap;
use log::debug;
use smallvec::SmallVec;
// Heavily based on https://github.com/codemirror/closebrackets/
@ -123,7 +122,7 @@ impl Default for AutoPairs {
#[must_use]
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 pair.same() {
@ -225,9 +224,11 @@ fn get_next_range(
// other end of the grapheme to get to where the new characters
// are inserted, then move the head to where it should be
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
};
@ -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()));
debug!("auto pair transaction: {:#?}", t);
log::debug!("auto pair transaction: {:#?}", 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()));
debug!("auto pair transaction: {:#?}", t);
log::debug!("auto pair transaction: {:#?}", 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()));
debug!("auto pair transaction: {:#?}", t);
log::debug!("auto pair transaction: {:#?}", t);
t
}

View file

@ -1,4 +1,5 @@
use arc_swap::{access::Map, ArcSwap};
use futures_util::Stream;
use helix_core::{
config::{default_syntax_loader, user_syntax_loader},
pos_at_coords, syntax, Selection,
@ -27,7 +28,7 @@ use std::{
use anyhow::Error;
use crossterm::{
event::{DisableMouseCapture, EnableMouseCapture, Event, EventStream},
event::{DisableMouseCapture, EnableMouseCapture, Event},
execute, terminal,
tty::IsTty,
};
@ -68,7 +69,7 @@ fn setup_integration_logging() {
message
))
})
.level(log::LevelFilter::Info)
.level(log::LevelFilter::Debug)
.chain(std::io::stdout())
.apply();
}
@ -225,8 +226,10 @@ impl Application {
}
}
pub async fn event_loop(&mut self) {
let mut reader = EventStream::new();
pub async fn event_loop<S>(&mut self, input_stream: &mut S)
where
S: Stream<Item = crossterm::Result<crossterm::event::Event>> + Unpin,
{
let mut last_render = Instant::now();
let deadline = Duration::from_secs(1) / 60;
@ -242,7 +245,7 @@ impl Application {
tokio::select! {
biased;
Some(event) = reader.next() => {
Some(event) = input_stream.next() => {
self.handle_terminal_events(event)
}
Some(signal) = self.signals.next() => {
@ -749,7 +752,10 @@ impl Application {
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?;
// Exit the alternate screen and disable raw mode before panicking
@ -764,16 +770,20 @@ impl Application {
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;
if self.editor.close_language_servers(None).await.is_err() {
log::error!("Timed out waiting for language servers to shutdown");
};
self.restore_term()?;
Ok(self.editor.exit_code)
Ok(())
}
}

View file

@ -95,6 +95,7 @@ impl Jobs {
/// Blocks until all the jobs that need to be waited on are done.
pub async fn finish(&mut self) {
let wait_futures = std::mem::take(&mut self.wait_futures);
log::debug!("waiting on jobs...");
wait_futures.for_each(|_| future::ready(())).await
}
}

View file

@ -1,4 +1,5 @@
use anyhow::{Context, Error, Result};
use crossterm::event::EventStream;
use helix_term::application::Application;
use helix_term::args::Args;
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
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)
}

View file

@ -17,7 +17,8 @@ mod integration {
Args::default(),
Config::default(),
("#[\n|]#", "ihello world<esc>", "hello world#[|\n]#"),
)?;
)
.await?;
Ok(())
}

View file

@ -18,7 +18,8 @@ async fn auto_indent_c() -> anyhow::Result<()> {
}
"},
),
)?;
)
.await?;
Ok(())
}

View file

@ -6,7 +6,8 @@ async fn auto_pairs_basic() -> anyhow::Result<()> {
Args::default(),
Config::default(),
("#[\n|]#", "i(<esc>", "(#[|)]#\n"),
)?;
)
.await?;
test_key_sequence_text_result(
Args::default(),
@ -18,7 +19,8 @@ async fn auto_pairs_basic() -> anyhow::Result<()> {
..Default::default()
},
("#[\n|]#", "i(<esc>", "(#[|\n]#"),
)?;
)
.await?;
Ok(())
}

View file

@ -1,9 +1,12 @@
use std::io::Write;
use std::{io::Write, time::Duration};
use anyhow::bail;
use crossterm::event::{Event, KeyEvent};
use helix_core::{test, Selection, Transaction};
use helix_term::{application::Application, args::Args, config::Config};
use helix_view::{doc, input::parse_macro};
use tokio::time::timeout;
use tokio_stream::wrappers::UnboundedReceiverStream;
#[derive(Clone, Debug)]
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>,
test_case: T,
test_fn: &dyn Fn(&mut Application),
test_fn: &dyn Fn(&Application),
) -> anyhow::Result<()> {
let test_case = test_case.into();
let mut app =
@ -50,23 +87,13 @@ pub fn test_key_sequence<T: Into<TestCase>>(
view.id,
);
let input_keys = parse_macro(&test_case.in_keys)?
.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(())
test_key_sequence(&mut app, &test_case.in_keys, Some(test_fn)).await
}
/// Use this for very simple test cases where there is one input
/// document, selection, and sequence of key presses, and you just
/// 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,
config: Config,
test_case: T,
@ -74,7 +101,7 @@ pub fn test_key_sequence_text_result<T: Into<TestCase>>(
let test_case = test_case.into();
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);
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();
assert_eq!(test_case.out_selection, sel);
})?;
Ok(())
})
.await
}
pub fn temp_file_with_contents<S: AsRef<str>>(content: S) -> tempfile::NamedTempFile {

View file

@ -14,25 +14,29 @@ async fn insert_mode_cursor_position() -> anyhow::Result<()> {
out_text: String::new(),
out_selection: Selection::single(0, 0),
},
)?;
)
.await?;
test_key_sequence_text_result(
Args::default(),
Config::default(),
("#[\n|]#", "i", "#[|\n]#"),
)?;
)
.await?;
test_key_sequence_text_result(
Args::default(),
Config::default(),
("#[\n|]#", "i<esc>", "#[|\n]#"),
)?;
)
.await?;
test_key_sequence_text_result(
Args::default(),
Config::default(),
("#[\n|]#", "i<esc>i", "#[|\n]#"),
)?;
)
.await?;
Ok(())
}
@ -44,7 +48,8 @@ async fn insert_to_normal_mode_cursor_position() -> anyhow::Result<()> {
Args::default(),
Config::default(),
("#[f|]#oo\n", "vll<A-;><esc>", "#[|foo]#\n"),
)?;
)
.await?;
test_key_sequence_text_result(
Args::default(),
@ -60,7 +65,8 @@ async fn insert_to_normal_mode_cursor_position() -> anyhow::Result<()> {
#(|bar)#"
},
),
)?;
)
.await?;
test_key_sequence_text_result(
Args::default(),
@ -76,7 +82,8 @@ async fn insert_to_normal_mode_cursor_position() -> anyhow::Result<()> {
#(ba|)#r"
},
),
)?;
)
.await?;
test_key_sequence_text_result(
Args::default(),
@ -92,7 +99,8 @@ async fn insert_to_normal_mode_cursor_position() -> anyhow::Result<()> {
#(b|)#ar"
},
),
)?;
)
.await?;
Ok(())
}

View file

@ -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(())
}