aboutsummaryrefslogtreecommitdiff
path: root/helix-term/src/application.rs
diff options
context:
space:
mode:
Diffstat (limited to 'helix-term/src/application.rs')
-rw-r--r--helix-term/src/application.rs157
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(())
}
}