on master help sign in
use main application event loop
ee705dcSkyler Hawthorne
helix-core/src/auto_pairs.rs+8−7
@@ -4,7 +4,6 @@
4use crate::{graphemes, movement::Direction, Range, Rope, Selection, Tendril, Transaction};
4use crate::{graphemes, movement::Direction, Range, Rope, Selection, Tendril, Transaction};
5use std::collections::HashMap;
5use std::collections::HashMap;
6
6
7−use log::debug;
8use smallvec::SmallVec;
7use smallvec::SmallVec;
9
8
10// Heavily based on https://github.com/codemirror/closebrackets/
9// Heavily based on https://github.com/codemirror/closebrackets/
@@ -123,7 +122,7 @@ impl Default for AutoPairs {
123
122
124#[must_use]
123#[must_use]
125pub fn hook(doc: &Rope, selection: &Selection, ch: char, pairs: &AutoPairs) -> Option<Transaction> {
124pub fn hook(doc: &Rope, selection: &Selection, ch: char, pairs: &AutoPairs) -> Option<Transaction> {
126− debug!("autopairs hook selection: {:#?}", selection);
125+ log::trace!("autopairs hook selection: {:#?}", selection);
127
126
128 if let Some(pair) = pairs.get(ch) {
127 if let Some(pair) = pairs.get(ch) {
129 if pair.same() {
128 if pair.same() {
@@ -225,9 +224,11 @@ fn get_next_range(
225 // other end of the grapheme to get to where the new characters
224 // other end of the grapheme to get to where the new characters
226 // are inserted, then move the head to where it should be
225 // are inserted, then move the head to where it should be
227 let prev_bound = graphemes::prev_grapheme_boundary(doc_slice, start_range.head);
226 let prev_bound = graphemes::prev_grapheme_boundary(doc_slice, start_range.head);
228− debug!(
227+ log::trace!(
229 "prev_bound: {}, offset: {}, len_inserted: {}",
228 "prev_bound: {}, offset: {}, len_inserted: {}",
229+ prev_bound,
230+ offset,
230− prev_bound, offset, len_inserted
231+ len_inserted
231 );
232 );
232 prev_bound + offset + len_inserted
233 prev_bound + offset + len_inserted
233 };
234 };
@@ -302,7 +303,7 @@ fn handle_open(doc: &Rope, selection: &Selection, pair: &Pair) -> Transaction {
302 });
303 });
303
304
304 let t = transaction.with_selection(Selection::new(end_ranges, selection.primary_index()));
305 let t = transaction.with_selection(Selection::new(end_ranges, selection.primary_index()));
305− debug!("auto pair transaction: {:#?}", t);
306+ log::debug!("auto pair transaction: {:#?}", t);
306 t
307 t
307}
308}
308
309
@@ -334,7 +335,7 @@ fn handle_close(doc: &Rope, selection: &Selection, pair: &Pair) -> Transaction {
334 });
335 });
335
336
336 let t = transaction.with_selection(Selection::new(end_ranges, selection.primary_index()));
337 let t = transaction.with_selection(Selection::new(end_ranges, selection.primary_index()));
337− debug!("auto pair transaction: {:#?}", t);
338+ log::debug!("auto pair transaction: {:#?}", t);
338 t
339 t
339}
340}
340
341
@@ -374,7 +375,7 @@ fn handle_same(doc: &Rope, selection: &Selection, pair: &Pair) -> Transaction {
374 });
375 });
375
376
376 let t = transaction.with_selection(Selection::new(end_ranges, selection.primary_index()));
377 let t = transaction.with_selection(Selection::new(end_ranges, selection.primary_index()));
377− debug!("auto pair transaction: {:#?}", t);
378+ log::debug!("auto pair transaction: {:#?}", t);
378 t
379 t
379}
380}
380
381
helix-term/src/application.rs+20−10
@@ -1,4 +1,5 @@
1use arc_swap::{access::Map, ArcSwap};
1use arc_swap::{access::Map, ArcSwap};
2+use futures_util::Stream;
2use helix_core::{
3use helix_core::{
3 config::{default_syntax_loader, user_syntax_loader},
4 config::{default_syntax_loader, user_syntax_loader},
4 pos_at_coords, syntax, Selection,
5 pos_at_coords, syntax, Selection,
@@ -27,7 +28,7 @@ use std::{
27use anyhow::Error;
28use anyhow::Error;
28
29
29use crossterm::{
30use crossterm::{
30− event::{DisableMouseCapture, EnableMouseCapture, Event, EventStream},
31+ event::{DisableMouseCapture, EnableMouseCapture, Event},
31 execute, terminal,
32 execute, terminal,
32 tty::IsTty,
33 tty::IsTty,
33};
34};
@@ -68,7 +69,7 @@ fn setup_integration_logging() {
68 message
69 message
69 ))
70 ))
70 })
71 })
71− .level(log::LevelFilter::Info)
72+ .level(log::LevelFilter::Debug)
72 .chain(std::io::stdout())
73 .chain(std::io::stdout())
73 .apply();
74 .apply();
74}
75}
@@ -225,8 +226,10 @@ impl Application {
225 }
226 }
226 }
227 }
227
228
228− pub async fn event_loop(&mut self) {
229+ pub async fn event_loop<S>(&mut self, input_stream: &mut S)
229− let mut reader = EventStream::new();
230+ where
231+ S: Stream<Item = crossterm::Result<crossterm::event::Event>> + Unpin,
232+ {
230 let mut last_render = Instant::now();
233 let mut last_render = Instant::now();
231 let deadline = Duration::from_secs(1) / 60;
234 let deadline = Duration::from_secs(1) / 60;
232
235
@@ -242,7 +245,7 @@ impl Application {
242 tokio::select! {
245 tokio::select! {
243 biased;
246 biased;
244
247
245− Some(event) = reader.next() => {
248+ Some(event) = input_stream.next() => {
246 self.handle_terminal_events(event)
249 self.handle_terminal_events(event)
247 }
250 }
248 Some(signal) = self.signals.next() => {
251 Some(signal) = self.signals.next() => {
@@ -749,7 +752,10 @@ impl Application {
749 Ok(())
752 Ok(())
750 }
753 }
751
754
752− pub async fn run(&mut self) -> Result<i32, Error> {
755+ pub async fn run<S>(&mut self, input_stream: &mut S) -> Result<i32, Error>
756+ where
757+ S: Stream<Item = crossterm::Result<crossterm::event::Event>> + Unpin,
758+ {
753 self.claim_term().await?;
759 self.claim_term().await?;
754
760
755 // Exit the alternate screen and disable raw mode before panicking
761 // Exit the alternate screen and disable raw mode before panicking
@@ -764,16 +770,20 @@ impl Application {
764 hook(info);
770 hook(info);
765 }));
771 }));
766
772
767− self.event_loop().await;
773+ self.event_loop(input_stream).await;
774+ self.close().await?;
775+ self.restore_term()?;
776+
777+ Ok(self.editor.exit_code)
778+ }
768
779
780+ pub async fn close(&mut self) -> anyhow::Result<()> {
769 self.jobs.finish().await;
781 self.jobs.finish().await;
770
782
771 if self.editor.close_language_servers(None).await.is_err() {
783 if self.editor.close_language_servers(None).await.is_err() {
772 log::error!("Timed out waiting for language servers to shutdown");
784 log::error!("Timed out waiting for language servers to shutdown");
773 };
785 };
774
786
775− self.restore_term()?;
787+ Ok(())
776−
777− Ok(self.editor.exit_code)
778 }
788 }
779}
789}
helix-term/src/job.rs+1
@@ -95,6 +95,7 @@ impl Jobs {
95 /// Blocks until all the jobs that need to be waited on are done.
95 /// Blocks until all the jobs that need to be waited on are done.
96 pub async fn finish(&mut self) {
96 pub async fn finish(&mut self) {
97 let wait_futures = std::mem::take(&mut self.wait_futures);
97 let wait_futures = std::mem::take(&mut self.wait_futures);
98+ log::debug!("waiting on jobs...");
98 wait_futures.for_each(|_| future::ready(())).await
99 wait_futures.for_each(|_| future::ready(())).await
99 }
100 }
100}
101}
helix-term/src/main.rs+2−1
@@ -1,4 +1,5 @@
1use anyhow::{Context, Error, Result};
1use anyhow::{Context, Error, Result};
2+use crossterm::event::EventStream;
2use helix_term::application::Application;
3use helix_term::application::Application;
3use helix_term::args::Args;
4use helix_term::args::Args;
4use helix_term::config::Config;
5use helix_term::config::Config;
@@ -134,7 +135,7 @@ FLAGS:
134 // TODO: use the thread local executor to spawn the application task separately from the work pool
135 // TODO: use the thread local executor to spawn the application task separately from the work pool
135 let mut app = Application::new(args, config).context("unable to create new application")?;
136 let mut app = Application::new(args, config).context("unable to create new application")?;
136
137
137− let exit_code = app.run().await?;
138+ let exit_code = app.run(&mut EventStream::new()).await?;
138
139
139 Ok(exit_code)
140 Ok(exit_code)
140}
141}
helix-term/tests/integration.rs+2−1
@@ -17,7 +17,8 @@ mod integration {
17 Args::default(),
17 Args::default(),
18 Config::default(),
18 Config::default(),
19 ("#[\n|]#", "ihello world<esc>", "hello world#[|\n]#"),
19 ("#[\n|]#", "ihello world<esc>", "hello world#[|\n]#"),
20− )?;
20+ )
21+ .await?;
21
22
22 Ok(())
23 Ok(())
23 }
24 }
helix-term/tests/integration/auto_indent.rs+2−1
@@ -18,7 +18,8 @@ async fn auto_indent_c() -> anyhow::Result<()> {
18 }
18 }
19 "},
19 "},
20 ),
20 ),
21− )?;
21+ )
22+ .await?;
22
23
23 Ok(())
24 Ok(())
24}
25}
helix-term/tests/integration/auto_pairs.rs+4−2
@@ -6,7 +6,8 @@ async fn auto_pairs_basic() -> anyhow::Result<()> {
6 Args::default(),
6 Args::default(),
7 Config::default(),
7 Config::default(),
8 ("#[\n|]#", "i(<esc>", "(#[|)]#\n"),
8 ("#[\n|]#", "i(<esc>", "(#[|)]#\n"),
9− )?;
9+ )
10+ .await?;
10
11
11 test_key_sequence_text_result(
12 test_key_sequence_text_result(
12 Args::default(),
13 Args::default(),
@@ -18,7 +19,8 @@ async fn auto_pairs_basic() -> anyhow::Result<()> {
18 ..Default::default()
19 ..Default::default()
19 },
20 },
20 ("#[\n|]#", "i(<esc>", "(#[|\n]#"),
21 ("#[\n|]#", "i(<esc>", "(#[|\n]#"),
21− )?;
22+ )
23+ .await?;
22
24
23 Ok(())
25 Ok(())
24}
26}
helix-term/tests/integration/helpers.rs+45−19
@@ -1,9 +1,12 @@
1−use std::io::Write;
1+use std::{io::Write, time::Duration};
2
2
3+use anyhow::bail;
3use crossterm::event::{Event, KeyEvent};
4use crossterm::event::{Event, KeyEvent};
4use helix_core::{test, Selection, Transaction};
5use helix_core::{test, Selection, Transaction};
5use helix_term::{application::Application, args::Args, config::Config};
6use helix_term::{application::Application, args::Args, config::Config};
6use helix_view::{doc, input::parse_macro};
7use helix_view::{doc, input::parse_macro};
8+use tokio::time::timeout;
9+use tokio_stream::wrappers::UnboundedReceiverStream;
7
10
8#[derive(Clone, Debug)]
11#[derive(Clone, Debug)]
9pub struct TestCase {
12pub struct TestCase {
@@ -29,10 +32,44 @@ impl<S: Into<String>> From<(S, S, S)> for TestCase {
29 }
32 }
30}
33}
31
34
32−pub fn test_key_sequence<T: Into<TestCase>>(
35+pub async fn test_key_sequence(
36+ app: &mut Application,
37+ in_keys: &str,
38+ test_fn: Option<&dyn Fn(&Application)>,
39+) -> anyhow::Result<()> {
40+ let (tx, rx) = tokio::sync::mpsc::unbounded_channel();
41+
42+ for key_event in parse_macro(&in_keys)?.into_iter() {
43+ tx.send(Ok(Event::Key(KeyEvent::from(key_event))))?;
44+ }
45+
46+ let mut rx_stream = UnboundedReceiverStream::new(rx);
47+ let event_loop = app.event_loop(&mut rx_stream);
48+ let result = timeout(Duration::from_millis(500), event_loop).await;
49+
50+ if result.is_ok() {
51+ bail!("application exited before test function could run");
52+ }
53+
54+ if let Some(test) = test_fn {
55+ test(app);
56+ };
57+
58+ for key_event in parse_macro("<esc>:q!<ret>")?.into_iter() {
59+ tx.send(Ok(Event::Key(KeyEvent::from(key_event))))?;
60+ }
61+
62+ let event_loop = app.event_loop(&mut rx_stream);
63+ timeout(Duration::from_millis(5000), event_loop).await?;
64+ app.close().await?;
65+
66+ Ok(())
67+}
68+
69+pub async fn test_key_sequence_with_input_text<T: Into<TestCase>>(
33 app: Option<Application>,
70 app: Option<Application>,
34 test_case: T,
71 test_case: T,
35− test_fn: &dyn Fn(&mut Application),
72+ test_fn: &dyn Fn(&Application),
36) -> anyhow::Result<()> {
73) -> anyhow::Result<()> {
37 let test_case = test_case.into();
74 let test_case = test_case.into();
38 let mut app =
75 let mut app =
@@ -50,23 +87,13 @@ pub fn test_key_sequence<T: Into<TestCase>>(
50 view.id,
87 view.id,
51 );
88 );
52
89
53− let input_keys = parse_macro(&test_case.in_keys)?
90+ test_key_sequence(&mut app, &test_case.in_keys, Some(test_fn)).await
54− .into_iter()
55− .map(|key_event| Event::Key(KeyEvent::from(key_event)));
56−
57− for key in input_keys {
58− app.handle_terminal_events(Ok(key));
59− }
60−
61− test_fn(&mut app);
62−
63− Ok(())
64}
91}
65
92
66/// Use this for very simple test cases where there is one input
93/// Use this for very simple test cases where there is one input
67/// document, selection, and sequence of key presses, and you just
94/// document, selection, and sequence of key presses, and you just
68/// want to verify the resulting document and selection.
95/// want to verify the resulting document and selection.
69−pub fn test_key_sequence_text_result<T: Into<TestCase>>(
96+pub async fn test_key_sequence_text_result<T: Into<TestCase>>(
70 args: Args,
97 args: Args,
71 config: Config,
98 config: Config,
72 test_case: T,
99 test_case: T,
@@ -74,7 +101,7 @@ pub fn test_key_sequence_text_result<T: Into<TestCase>>(
74 let test_case = test_case.into();
101 let test_case = test_case.into();
75 let app = Application::new(args, config).unwrap();
102 let app = Application::new(args, config).unwrap();
76
103
77− test_key_sequence(Some(app), test_case.clone(), &|app| {
104+ test_key_sequence_with_input_text(Some(app), test_case.clone(), &|app| {
78 let doc = doc!(app.editor);
105 let doc = doc!(app.editor);
79 assert_eq!(&test_case.out_text, doc.text());
106 assert_eq!(&test_case.out_text, doc.text());
80
107
@@ -83,9 +110,8 @@ pub fn test_key_sequence_text_result<T: Into<TestCase>>(
83
110
84 let sel = selections.pop().unwrap();
111 let sel = selections.pop().unwrap();
85 assert_eq!(test_case.out_selection, sel);
112 assert_eq!(test_case.out_selection, sel);
86− })?;
113+ })
87−
114+ .await
88− Ok(())
89}
115}
90
116
91pub fn temp_file_with_contents<S: AsRef<str>>(content: S) -> tempfile::NamedTempFile {
117pub fn temp_file_with_contents<S: AsRef<str>>(content: S) -> tempfile::NamedTempFile {
helix-term/tests/integration/movement.rs+16−8
@@ -14,25 +14,29 @@ async fn insert_mode_cursor_position() -> anyhow::Result<()> {
14 out_text: String::new(),
14 out_text: String::new(),
15 out_selection: Selection::single(0, 0),
15 out_selection: Selection::single(0, 0),
16 },
16 },
17− )?;
17+ )
18+ .await?;
18
19
19 test_key_sequence_text_result(
20 test_key_sequence_text_result(
20 Args::default(),
21 Args::default(),
21 Config::default(),
22 Config::default(),
22 ("#[\n|]#", "i", "#[|\n]#"),
23 ("#[\n|]#", "i", "#[|\n]#"),
23− )?;
24+ )
25+ .await?;
24
26
25 test_key_sequence_text_result(
27 test_key_sequence_text_result(
26 Args::default(),
28 Args::default(),
27 Config::default(),
29 Config::default(),
28 ("#[\n|]#", "i<esc>", "#[|\n]#"),
30 ("#[\n|]#", "i<esc>", "#[|\n]#"),
29− )?;
31+ )
32+ .await?;
30
33
31 test_key_sequence_text_result(
34 test_key_sequence_text_result(
32 Args::default(),
35 Args::default(),
33 Config::default(),
36 Config::default(),
34 ("#[\n|]#", "i<esc>i", "#[|\n]#"),
37 ("#[\n|]#", "i<esc>i", "#[|\n]#"),
35− )?;
38+ )
39+ .await?;
36
40
37 Ok(())
41 Ok(())
38}
42}
@@ -44,7 +48,8 @@ async fn insert_to_normal_mode_cursor_position() -> anyhow::Result<()> {
44 Args::default(),
48 Args::default(),
45 Config::default(),
49 Config::default(),
46 ("#[f|]#oo\n", "vll<A-;><esc>", "#[|foo]#\n"),
50 ("#[f|]#oo\n", "vll<A-;><esc>", "#[|foo]#\n"),
47− )?;
51+ )
52+ .await?;
48
53
49 test_key_sequence_text_result(
54 test_key_sequence_text_result(
50 Args::default(),
55 Args::default(),
@@ -60,7 +65,8 @@ async fn insert_to_normal_mode_cursor_position() -> anyhow::Result<()> {
60 #(|bar)#"
65 #(|bar)#"
61 },
66 },
62 ),
67 ),
63− )?;
68+ )
69+ .await?;
64
70
65 test_key_sequence_text_result(
71 test_key_sequence_text_result(
66 Args::default(),
72 Args::default(),
@@ -76,7 +82,8 @@ async fn insert_to_normal_mode_cursor_position() -> anyhow::Result<()> {
76 #(ba|)#r"
82 #(ba|)#r"
77 },
83 },
78 ),
84 ),
79− )?;
85+ )
86+ .await?;
80
87
81 test_key_sequence_text_result(
88 test_key_sequence_text_result(
82 Args::default(),
89 Args::default(),
@@ -92,7 +99,8 @@ async fn insert_to_normal_mode_cursor_position() -> anyhow::Result<()> {
92 #(b|)#ar"
99 #(b|)#ar"
93 },
100 },
94 ),
101 ),
95− )?;
102+ )
103+ .await?;
96
104
97 Ok(())
105 Ok(())
98}
106}
helix-term/tests/integration/write.rsnew file+69
@@ -0,0 +1,69 @@
1+use std::{
2+ io::{Read, Write},
3+ ops::RangeInclusive,
4+};
5+
6+use helix_term::application::Application;
7+
8+use super::*;
9+
10+#[tokio::test]
11+async fn test_write() -> anyhow::Result<()> {
12+ let mut file = tempfile::NamedTempFile::new().unwrap();
13+
14+ test_key_sequence(
15+ &mut Application::new(
16+ Args {
17+ files: vec![(file.path().to_path_buf(), Position::default())],
18+ ..Default::default()
19+ },
20+ Config::default(),
21+ )?,
22+ "ii can eat glass, it will not hurt me<ret><esc>:w<ret>",
23+ None,
24+ )
25+ .await?;
26+
27+ file.as_file_mut().flush()?;
28+ file.as_file_mut().sync_all()?;
29+
30+ let mut file_content = String::new();
31+ file.as_file_mut().read_to_string(&mut file_content)?;
32+ assert_eq!("i can eat glass, it will not hurt me\n", file_content);
33+
34+ Ok(())
35+}
36+
37+#[tokio::test]
38+async fn test_write_concurrent() -> anyhow::Result<()> {
39+ let mut file = tempfile::NamedTempFile::new().unwrap();
40+ let mut command = String::new();
41+ const RANGE: RangeInclusive<i32> = 1..=1000;
42+
43+ for i in RANGE {
44+ let cmd = format!("%c{}<esc>:w<ret>", i);
45+ command.push_str(&cmd);
46+ }
47+
48+ test_key_sequence(
49+ &mut Application::new(
50+ Args {
51+ files: vec![(file.path().to_path_buf(), Position::default())],
52+ ..Default::default()
53+ },
54+ Config::default(),
55+ )?,
56+ &command,
57+ None,
58+ )
59+ .await?;
60+
61+ file.as_file_mut().flush()?;
62+ file.as_file_mut().sync_all()?;
63+
64+ let mut file_content = String::new();
65+ file.as_file_mut().read_to_string(&mut file_content)?;
66+ assert_eq!(RANGE.end().to_string(), file_content);
67+
68+ Ok(())
69+}