diff options
Diffstat (limited to 'helix-term/src/application.rs')
-rw-r--r-- | helix-term/src/application.rs | 157 |
1 files changed, 105 insertions, 52 deletions
diff --git a/helix-term/src/application.rs b/helix-term/src/application.rs index 0c8de2ab..48e9c275 100644 --- a/helix-term/src/application.rs +++ b/helix-term/src/application.rs @@ -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, @@ -24,10 +25,10 @@ use std::{ time::{Duration, Instant}, }; -use anyhow::Error; +use anyhow::{Context, Error}; use crossterm::{ - event::{DisableMouseCapture, EnableMouseCapture, Event, EventStream}, + event::{DisableMouseCapture, EnableMouseCapture, Event}, execute, terminal, tty::IsTty, }; @@ -39,9 +40,11 @@ use { #[cfg(windows)] type Signals = futures_util::stream::Empty<()>; +const LSP_DEADLINE: Duration = Duration::from_millis(16); + pub struct Application { compositor: Compositor, - editor: Editor, + pub editor: Editor, config: Arc<ArcSwap<Config>>, @@ -53,32 +56,39 @@ pub struct Application { signals: Signals, jobs: Jobs, lsp_progress: LspProgressMap, + last_render: Instant, +} + +#[cfg(feature = "integration")] +fn setup_integration_logging() { + let level = std::env::var("HELIX_LOG_LEVEL") + .map(|lvl| lvl.parse().unwrap()) + .unwrap_or(log::LevelFilter::Info); + + // Separate file config so we can include year, month and day in file logs + let _ = fern::Dispatch::new() + .format(|out, message, record| { + out.finish(format_args!( + "{} {} [{}] {}", + chrono::Local::now().format("%Y-%m-%dT%H:%M:%S%.3f"), + record.target(), + record.level(), + message + )) + }) + .level(level) + .chain(std::io::stdout()) + .apply(); } impl Application { - pub fn new(args: Args) -> Result<Self, Error> { + pub fn new(args: Args, config: Config) -> Result<Self, Error> { + #[cfg(feature = "integration")] + setup_integration_logging(); + use helix_view::editor::Action; let config_dir = helix_loader::config_dir(); - if !config_dir.exists() { - std::fs::create_dir_all(&config_dir).ok(); - } - - let config = match std::fs::read_to_string(config_dir.join("config.toml")) { - Ok(config) => toml::from_str(&config) - .map(crate::keymap::merge_keys) - .unwrap_or_else(|err| { - eprintln!("Bad config: {}", err); - eprintln!("Press <ENTER> to continue with default config"); - use std::io::Read; - // This waits for an enter press. - let _ = std::io::stdin().read(&mut []); - Config::default() - }), - Err(err) if err.kind() == std::io::ErrorKind::NotFound => Config::default(), - Err(err) => return Err(Error::new(err)), - }; - let theme_loader = std::sync::Arc::new(theme::Loader::new( &config_dir, &helix_loader::runtime_dir(), @@ -116,7 +126,7 @@ impl Application { }); let syn_loader = std::sync::Arc::new(syntax::Loader::new(syn_loader_conf)); - let mut compositor = Compositor::new()?; + let mut compositor = Compositor::new().context("build compositor")?; let config = Arc::new(ArcSwap::from_pointee(config)); let mut editor = Editor::new( compositor.size(), @@ -135,26 +145,28 @@ impl Application { if args.load_tutor { let path = helix_loader::runtime_dir().join("tutor.txt"); - editor.open(path, Action::VerticalSplit)?; + editor.open(&path, Action::VerticalSplit)?; // Unset path to prevent accidentally saving to the original tutor file. doc_mut!(editor).set_path(None)?; } else if !args.files.is_empty() { let first = &args.files[0].0; // we know it's not empty if first.is_dir() { - std::env::set_current_dir(&first)?; + std::env::set_current_dir(&first).context("set current dir")?; editor.new_file(Action::VerticalSplit); let picker = ui::file_picker(".".into(), &config.load().editor); compositor.push(Box::new(overlayed(picker))); } else { let nr_of_files = args.files.len(); - editor.open(first.to_path_buf(), Action::VerticalSplit)?; + editor.open(first, Action::VerticalSplit)?; for (file, pos) in args.files { if file.is_dir() { return Err(anyhow::anyhow!( "expected a path to file, found a directory. (to open a directory pass it as first argument)" )); } else { - let doc_id = editor.open(file, Action::Load)?; + let doc_id = editor + .open(&file, Action::Load) + .context(format!("open '{}'", file.to_string_lossy()))?; // with Action::Load all documents have the same view let view_id = editor.tree.focus; let doc = editor.document_mut(doc_id).unwrap(); @@ -168,7 +180,7 @@ impl Application { let (view, doc) = current!(editor); align_view(doc, view, Align::Center); } - } else if stdin().is_tty() { + } else if stdin().is_tty() || cfg!(feature = "integration") { editor.new_file(Action::VerticalSplit); } else if cfg!(target_os = "macos") { // On Linux and Windows, we allow the output of a command to be piped into the new buffer. @@ -186,7 +198,8 @@ impl Application { #[cfg(windows)] let signals = futures_util::stream::empty(); #[cfg(not(windows))] - let signals = Signals::new(&[signal::SIGTSTP, signal::SIGCONT])?; + let signals = + Signals::new(&[signal::SIGTSTP, signal::SIGCONT]).context("build signal handler")?; let app = Self { compositor, @@ -200,40 +213,57 @@ impl Application { signals, jobs: Jobs::new(), lsp_progress: LspProgressMap::new(), + last_render: Instant::now(), }; Ok(app) } fn render(&mut self) { + let compositor = &mut self.compositor; + let mut cx = crate::compositor::Context { editor: &mut self.editor, jobs: &mut self.jobs, scroll: None, }; - self.compositor.render(&mut cx); + compositor.render(&mut cx); } - pub async fn event_loop(&mut self) { - let mut reader = EventStream::new(); - let mut last_render = Instant::now(); - let deadline = Duration::from_secs(1) / 60; - + pub async fn event_loop<S>(&mut self, input_stream: &mut S) + where + S: Stream<Item = crossterm::Result<crossterm::event::Event>> + Unpin, + { self.render(); + self.last_render = Instant::now(); loop { - if self.editor.should_close() { + if !self.event_loop_until_idle(input_stream).await { break; } + } + } + + pub async fn event_loop_until_idle<S>(&mut self, input_stream: &mut S) -> bool + where + S: Stream<Item = crossterm::Result<crossterm::event::Event>> + Unpin, + { + #[cfg(feature = "integration")] + let mut idle_handled = false; + + loop { + if self.editor.should_close() { + return false; + } use futures_util::StreamExt; tokio::select! { biased; - event = reader.next() => { - self.handle_terminal_events(event) + Some(event) = input_stream.next() => { + self.handle_terminal_events(event); } Some(signal) = self.signals.next() => { self.handle_signals(signal).await; @@ -242,9 +272,10 @@ impl Application { self.handle_language_server_message(call, id).await; // limit render calls for fast language server messages let last = self.editor.language_servers.incoming.is_empty(); - if last || last_render.elapsed() > deadline { + + if last || self.last_render.elapsed() > LSP_DEADLINE { self.render(); - last_render = Instant::now(); + self.last_render = Instant::now(); } } Some(payload) = self.editor.debugger_events.next() => { @@ -269,8 +300,24 @@ impl Application { // idle timeout self.editor.clear_idle_timer(); self.handle_idle_timeout(); + + #[cfg(feature = "integration")] + { + idle_handled = true; + } } } + + // for integration tests only, reset the idle timer after every + // event to make a signal when test events are done processing + #[cfg(feature = "integration")] + { + if idle_handled { + return true; + } + + self.editor.reset_idle_timer(); + } } } @@ -370,7 +417,7 @@ impl Application { } } - pub fn handle_terminal_events(&mut self, event: Option<Result<Event, crossterm::ErrorKind>>) { + pub fn handle_terminal_events(&mut self, event: Result<Event, crossterm::ErrorKind>) { let mut cx = crate::compositor::Context { editor: &mut self.editor, jobs: &mut self.jobs, @@ -378,15 +425,14 @@ impl Application { }; // Handle key events let should_redraw = match event { - Some(Ok(Event::Resize(width, height))) => { + Ok(Event::Resize(width, height)) => { self.compositor.resize(width, height); self.compositor .handle_event(Event::Resize(width, height), &mut cx) } - Some(Ok(event)) => self.compositor.handle_event(event, &mut cx), - Some(Err(x)) => panic!("{}", x), - None => panic!(), + Ok(event) => self.compositor.handle_event(event, &mut cx), + Err(x) => panic!("{}", x), }; if should_redraw && !self.editor.should_close() { @@ -740,7 +786,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 @@ -755,16 +804,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) + } - self.jobs.finish().await; + 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(()) } } |