on 6c11708 help sign in
helix/helix-term/src/application.rs
678 lines 28 kB history raw
1use helix_core::{merge_toml_values, pos_at_coords, syntax, Selection};
2use helix_lsp::{lsp, util::lsp_pos_to_pos, LspProgressMap};
3use helix_view::{theme, Editor};
4use serde_json::json;
6use crate::{
7 args::Args,
8 commands::{align_view, apply_workspace_edit, Align},
9 compositor::Compositor,
10 config::Config,
11 job::Jobs,
12 ui,
13};
15use log::{error, warn};
17use std::{
18 io::{stdin, stdout, Write},
19 sync::Arc,
20 time::{Duration, Instant},
21};
23use anyhow::Error;
25use crossterm::{
26 event::{DisableMouseCapture, EnableMouseCapture, Event, EventStream},
27 execute, terminal,
28 tty::IsTty,
29};
30#[cfg(not(windows))]
31use {
32 signal_hook::{consts::signal, low_level},
33 signal_hook_tokio::Signals,
34};
35#[cfg(windows)]
36type Signals = futures_util::stream::Empty<()>;
38pub struct Application {
39 compositor: Compositor,
40 editor: Editor,
42 // TODO should be separate to take only part of the config
43 config: Config,
45 // Currently never read from. Remove the `allow(dead_code)` when
46 // that changes.
47 #[allow(dead_code)]
48 theme_loader: Arc<theme::Loader>,
50 // Currently never read from. Remove the `allow(dead_code)` when
51 // that changes.
52 #[allow(dead_code)]
53 syn_loader: Arc<syntax::Loader>,
55 signals: Signals,
56 jobs: Jobs,
57 lsp_progress: LspProgressMap,
60impl Application {
61 pub fn new(args: Args, mut config: Config) -> Result<Self, Error> {
62 use helix_view::editor::Action;
63 let mut compositor = Compositor::new()?;
64 let size = compositor.size();
66 let conf_dir = helix_core::config_dir();
68 let theme_loader =
69 std::sync::Arc::new(theme::Loader::new(&conf_dir, &helix_core::runtime_dir()));
71 // load default and user config, and merge both
72 let builtin_err_msg =
73 "Could not parse built-in languages.toml, something must be very wrong";
74 let def_lang_conf: toml::Value =
75 toml::from_slice(include_bytes!("../../languages.toml")).expect(builtin_err_msg);
76 let def_syn_loader_conf: helix_core::syntax::Configuration =
77 def_lang_conf.clone().try_into().expect(builtin_err_msg);
78 let user_lang_conf = std::fs::read(conf_dir.join("languages.toml"))
79 .ok()
80 .map(|raw| toml::from_slice(&raw));
81 let lang_conf = match user_lang_conf {
82 Some(Ok(value)) => Ok(merge_toml_values(def_lang_conf, value)),
83 Some(err @ Err(_)) => err,
84 None => Ok(def_lang_conf),
85 };
87 let true_color = config.editor.true_color || crate::true_color();
88 let theme = config
89 .theme
90 .as_ref()
91 .and_then(|theme| {
92 theme_loader
93 .load(theme)
94 .map_err(|e| {
95 log::warn!("failed to load theme `{}` - {}", theme, e);
96 e
97 })
98 .ok()
99 .filter(|theme| (true_color || theme.is_16_color()))
100 })
101 .unwrap_or_else(|| {
102 if true_color {
103 theme_loader.default()
104 } else {
105 theme_loader.base16_default()
106 }
107 });
109 let syn_loader_conf: helix_core::syntax::Configuration = lang_conf
110 .and_then(|conf| conf.try_into())
111 .unwrap_or_else(|err| {
112 eprintln!("Bad language config: {}", err);
113 eprintln!("Press <ENTER> to continue with default language config");
114 use std::io::Read;
115 // This waits for an enter press.
116 let _ = std::io::stdin().read(&mut []);
117 def_syn_loader_conf
118 });
119 let syn_loader = std::sync::Arc::new(syntax::Loader::new(syn_loader_conf));
121 let mut editor = Editor::new(
122 size,
123 theme_loader.clone(),
124 syn_loader.clone(),
125 config.editor.clone(),
126 );
128 let editor_view = Box::new(ui::EditorView::new(std::mem::take(&mut config.keys)));
129 compositor.push(editor_view);
131 if args.load_tutor {
132 let path = helix_core::runtime_dir().join("tutor.txt");
133 editor.open(path, Action::VerticalSplit)?;
134 // Unset path to prevent accidentally saving to the original tutor file.
135 doc_mut!(editor).set_path(None)?;
136 } else if !args.files.is_empty() {
137 let first = &args.files[0].0; // we know it's not empty
138 if first.is_dir() {
139 std::env::set_current_dir(&first)?;
140 editor.new_file(Action::VerticalSplit);
141 compositor.push(Box::new(ui::file_picker(".".into(), &config.editor)));
142 } else {
143 let nr_of_files = args.files.len();
144 editor.open(first.to_path_buf(), Action::VerticalSplit)?;
145 for (file, pos) in args.files {
146 if file.is_dir() {
147 return Err(anyhow::anyhow!(
148 "expected a path to file, found a directory. (to open a directory pass it as first argument)"
149 ));
150 } else {
151 let doc_id = editor.open(file, Action::Load)?;
152 // with Action::Load all documents have the same view
153 let view_id = editor.tree.focus;
154 let doc = editor.document_mut(doc_id).unwrap();
155 let pos = Selection::point(pos_at_coords(doc.text().slice(..), pos, true));
156 doc.set_selection(view_id, pos);
157 }
158 }
159 editor.set_status(format!("Loaded {} files.", nr_of_files));
160 // align the view to center after all files are loaded,
161 // does not affect views without pos since it is at the top
162 let (view, doc) = current!(editor);
163 align_view(doc, view, Align::Center);
164 }
165 } else if stdin().is_tty() {
166 editor.new_file(Action::VerticalSplit);
167 } else if cfg!(target_os = "macos") {
168 // On Linux and Windows, we allow the output of a command to be piped into the new buffer.
169 // This doesn't currently work on macOS because of the following issue:
170 // https://github.com/crossterm-rs/crossterm/issues/500
171 anyhow::bail!("Piping into helix-term is currently not supported on macOS");
172 } else {
173 editor
174 .new_file_from_stdin(Action::VerticalSplit)
175 .unwrap_or_else(|_| editor.new_file(Action::VerticalSplit));
176 }
178 editor.set_theme(theme);
180 #[cfg(windows)]
181 let signals = futures_util::stream::empty();
182 #[cfg(not(windows))]
183 let signals = Signals::new(&[signal::SIGTSTP, signal::SIGCONT])?;
185 let app = Self {
186 compositor,
187 editor,
189 config,
191 theme_loader,
192 syn_loader,
194 signals,
195 jobs: Jobs::new(),
196 lsp_progress: LspProgressMap::new(),
197 };
199 Ok(app)
200 }
202 fn render(&mut self) {
203 let editor = &mut self.editor;
204 let compositor = &mut self.compositor;
205 let jobs = &mut self.jobs;
207 let mut cx = crate::compositor::Context {
208 editor,
209 jobs,
210 scroll: None,
211 };
213 compositor.render(&mut cx);
214 }
216 pub async fn event_loop(&mut self) {
217 let mut reader = EventStream::new();
218 let mut last_render = Instant::now();
219 let deadline = Duration::from_secs(1) / 60;
221 self.render();
223 loop {
224 if self.editor.should_close() {
225 break;
226 }
228 use futures_util::StreamExt;
230 tokio::select! {
231 biased;
233 event = reader.next() => {
234 self.handle_terminal_events(event)
235 }
236 Some(signal) = self.signals.next() => {
237 self.handle_signals(signal).await;
238 }
239 Some((id, call)) = self.editor.language_servers.incoming.next() => {
240 self.handle_language_server_message(call, id).await;
241 // limit render calls for fast language server messages
242 let last = self.editor.language_servers.incoming.is_empty();
243 if last || last_render.elapsed() > deadline {
244 self.render();
245 last_render = Instant::now();
246 }
247 }
248 Some(callback) = self.jobs.futures.next() => {
249 self.jobs.handle_callback(&mut self.editor, &mut self.compositor, callback);
250 self.render();
251 }
252 Some(callback) = self.jobs.wait_futures.next() => {
253 self.jobs.handle_callback(&mut self.editor, &mut self.compositor, callback);
254 self.render();
255 }
256 _ = &mut self.editor.idle_timer => {
257 // idle timeout
258 self.editor.clear_idle_timer();
259 self.handle_idle_timeout();
260 }
261 }
262 }
263 }
265 #[cfg(windows)]
266 // no signal handling available on windows
267 pub async fn handle_signals(&mut self, _signal: ()) {}
269 #[cfg(not(windows))]
270 pub async fn handle_signals(&mut self, signal: i32) {
271 use helix_view::graphics::Rect;
272 match signal {
273 signal::SIGTSTP => {
274 self.compositor.save_cursor();
275 self.restore_term().unwrap();
276 low_level::emulate_default_handler(signal::SIGTSTP).unwrap();
277 }
278 signal::SIGCONT => {
279 self.claim_term().await.unwrap();
280 // redraw the terminal
281 let Rect { width, height, .. } = self.compositor.size();
282 self.compositor.resize(width, height);
283 self.compositor.load_cursor();
284 self.render();
285 }
286 _ => unreachable!(),
287 }
288 }
290 pub fn handle_idle_timeout(&mut self) {
291 use crate::commands::{insert::idle_completion, Context};
292 use helix_view::document::Mode;
294 if doc!(self.editor).mode != Mode::Insert || !self.config.editor.auto_completion {
295 return;
296 }
297 let editor_view = self
298 .compositor
299 .find::<ui::EditorView>()
300 .expect("expected at least one EditorView");
302 if editor_view.completion.is_some() {
303 return;
304 }
306 let mut cx = Context {
307 register: None,
308 editor: &mut self.editor,
309 jobs: &mut self.jobs,
310 count: None,
311 callback: None,
312 on_next_key_callback: None,
313 };
314 idle_completion(&mut cx);
315 self.render();
316 }
318 pub fn handle_terminal_events(&mut self, event: Option<Result<Event, crossterm::ErrorKind>>) {
319 let mut cx = crate::compositor::Context {
320 editor: &mut self.editor,
321 jobs: &mut self.jobs,
322 scroll: None,
323 };
324 // Handle key events
325 let should_redraw = match event {
326 Some(Ok(Event::Resize(width, height))) => {
327 self.compositor.resize(width, height);
329 self.compositor
330 .handle_event(Event::Resize(width, height), &mut cx)
331 }
332 Some(Ok(event)) => self.compositor.handle_event(event, &mut cx),
333 Some(Err(x)) => panic!("{}", x),
334 None => panic!(),
335 };
337 if should_redraw && !self.editor.should_close() {
338 self.render();
339 }
340 }
342 pub async fn handle_language_server_message(
343 &mut self,
344 call: helix_lsp::Call,
345 server_id: usize,
346 ) {
347 use helix_lsp::{Call, MethodCall, Notification};
349 match call {
350 Call::Notification(helix_lsp::jsonrpc::Notification { method, params, .. }) => {
351 let notification = match Notification::parse(&method, params) {
352 Some(notification) => notification,
353 None => return,
354 };
356 match notification {
357 Notification::Initialized => {
358 let language_server =
359 match self.editor.language_servers.get_by_id(server_id) {
360 Some(language_server) => language_server,
361 None => {
362 warn!("can't find language server with id `{}`", server_id);
363 return;
364 }
365 };
367 let docs = self.editor.documents().filter(|doc| {
368 doc.language_server().map(|server| server.id()) == Some(server_id)
369 });
371 // trigger textDocument/didOpen for docs that are already open
372 for doc in docs {
373 let language_id =
374 doc.language_id().map(ToOwned::to_owned).unwrap_or_default();
376 tokio::spawn(language_server.text_document_did_open(
377 doc.url().unwrap(),
378 doc.version(),
379 doc.text(),
380 language_id,
381 ));
382 }
383 }
384 Notification::PublishDiagnostics(params) => {
385 let path = params.uri.to_file_path().unwrap();
386 let doc = self.editor.document_by_path_mut(&path);
388 if let Some(doc) = doc {
389 let lang_conf = doc.language_config();
390 let text = doc.text();
392 let diagnostics = params
393 .diagnostics
394 .into_iter()
395 .filter_map(|diagnostic| {
396 use helix_core::{
397 diagnostic::{Range, Severity::*},
398 Diagnostic,
399 };
400 use lsp::DiagnosticSeverity;
402 let language_server = doc.language_server().unwrap();
404 // TODO: convert inside server
405 let start = if let Some(start) = lsp_pos_to_pos(
406 text,
407 diagnostic.range.start,
408 language_server.offset_encoding(),
409 ) {
410 start
411 } else {
412 log::warn!("lsp position out of bounds - {:?}", diagnostic);
413 return None;
414 };
416 let end = if let Some(end) = lsp_pos_to_pos(
417 text,
418 diagnostic.range.end,
419 language_server.offset_encoding(),
420 ) {
421 end
422 } else {
423 log::warn!("lsp position out of bounds - {:?}", diagnostic);
424 return None;
425 };
427 let severity =
428 diagnostic.severity.map(|severity| match severity {
429 DiagnosticSeverity::ERROR => Error,
430 DiagnosticSeverity::WARNING => Warning,
431 DiagnosticSeverity::INFORMATION => Info,
432 DiagnosticSeverity::HINT => Hint,
433 severity => unreachable!(
434 "unrecognized diagnostic severity: {:?}",
435 severity
436 ),
437 });
439 if let Some(lang_conf) = lang_conf {
440 if let Some(severity) = severity {
441 if severity < lang_conf.diagnostic_severity {
442 return None;
443 }
444 }
445 };
447 Some(Diagnostic {
448 range: Range { start, end },
449 line: diagnostic.range.start.line as usize,
450 message: diagnostic.message,
451 severity,
452 // code
453 // source
454 })
455 })
456 .collect();
458 doc.set_diagnostics(diagnostics);
459 }
460 }
461 Notification::ShowMessage(params) => {
462 log::warn!("unhandled window/showMessage: {:?}", params);
463 }
464 Notification::LogMessage(params) => {
465 log::info!("window/logMessage: {:?}", params);
466 }
467 Notification::ProgressMessage(params)
468 if !self
469 .compositor
470 .has_component(std::any::type_name::<ui::Prompt>()) =>
471 {
472 let editor_view = self
473 .compositor
474 .find::<ui::EditorView>()
475 .expect("expected at least one EditorView");
476 let lsp::ProgressParams { token, value } = params;
478 let lsp::ProgressParamsValue::WorkDone(work) = value;
479 let parts = match &work {
480 lsp::WorkDoneProgress::Begin(lsp::WorkDoneProgressBegin {
481 title,
482 message,
483 percentage,
484 ..
485 }) => (Some(title), message, percentage),
486 lsp::WorkDoneProgress::Report(lsp::WorkDoneProgressReport {
487 message,
488 percentage,
489 ..
490 }) => (None, message, percentage),
491 lsp::WorkDoneProgress::End(lsp::WorkDoneProgressEnd { message }) => {
492 if message.is_some() {
493 (None, message, &None)
494 } else {
495 self.lsp_progress.end_progress(server_id, &token);
496 if !self.lsp_progress.is_progressing(server_id) {
497 editor_view.spinners_mut().get_or_create(server_id).stop();
498 }
499 self.editor.clear_status();
501 // we want to render to clear any leftover spinners or messages
502 return;
503 }
504 }
505 };
507 let token_d: &dyn std::fmt::Display = match &token {
508 lsp::NumberOrString::Number(n) => n,
509 lsp::NumberOrString::String(s) => s,
510 };
512 let status = match parts {
513 (Some(title), Some(message), Some(percentage)) => {
514 format!("[{}] {}% {} - {}", token_d, percentage, title, message)
515 }
516 (Some(title), None, Some(percentage)) => {
517 format!("[{}] {}% {}", token_d, percentage, title)
518 }
519 (Some(title), Some(message), None) => {
520 format!("[{}] {} - {}", token_d, title, message)
521 }
522 (None, Some(message), Some(percentage)) => {
523 format!("[{}] {}% {}", token_d, percentage, message)
524 }
525 (Some(title), None, None) => {
526 format!("[{}] {}", token_d, title)
527 }
528 (None, Some(message), None) => {
529 format!("[{}] {}", token_d, message)
530 }
531 (None, None, Some(percentage)) => {
532 format!("[{}] {}%", token_d, percentage)
533 }
534 (None, None, None) => format!("[{}]", token_d),
535 };
537 if let lsp::WorkDoneProgress::End(_) = work {
538 self.lsp_progress.end_progress(server_id, &token);
539 if !self.lsp_progress.is_progressing(server_id) {
540 editor_view.spinners_mut().get_or_create(server_id).stop();
541 }
542 } else {
543 self.lsp_progress.update(server_id, token, work);
544 }
546 if self.config.lsp.display_messages {
547 self.editor.set_status(status);
548 }
549 }
550 Notification::ProgressMessage(_params) => {
551 // do nothing
552 }
553 }
554 }
555 Call::MethodCall(helix_lsp::jsonrpc::MethodCall {
556 method, params, id, ..
557 }) => {
558 let call = match MethodCall::parse(&method, params) {
559 Some(call) => call,
560 None => {
561 error!("Method not found {}", method);
562 // language_server.reply(
563 // call.id,
564 // // TODO: make a Into trait that can cast to Err(jsonrpc::Error)
565 // Err(helix_lsp::jsonrpc::Error {
566 // code: helix_lsp::jsonrpc::ErrorCode::MethodNotFound,
567 // message: "Method not found".to_string(),
568 // data: None,
569 // }),
570 // );
571 return;
572 }
573 };
575 match call {
576 MethodCall::WorkDoneProgressCreate(params) => {
577 self.lsp_progress.create(server_id, params.token);
579 let editor_view = self
580 .compositor
581 .find::<ui::EditorView>()
582 .expect("expected at least one EditorView");
583 let spinner = editor_view.spinners_mut().get_or_create(server_id);
584 if spinner.is_stopped() {
585 spinner.start();
586 }
587 let language_server =
588 match self.editor.language_servers.get_by_id(server_id) {
589 Some(language_server) => language_server,
590 None => {
591 warn!("can't find language server with id `{}`", server_id);
592 return;
593 }
594 };
596 tokio::spawn(language_server.reply(id, Ok(serde_json::Value::Null)));
597 }
598 MethodCall::ApplyWorkspaceEdit(params) => {
599 apply_workspace_edit(
600 &mut self.editor,
601 helix_lsp::OffsetEncoding::Utf8,
602 &params.edit,
603 );
605 let language_server =
606 match self.editor.language_servers.get_by_id(server_id) {
607 Some(language_server) => language_server,
608 None => {
609 warn!("can't find language server with id `{}`", server_id);
610 return;
611 }
612 };
614 tokio::spawn(language_server.reply(
615 id,
616 Ok(json!(lsp::ApplyWorkspaceEditResponse {
617 applied: true,
618 failure_reason: None,
619 failed_change: None,
620 })),
621 ));
622 }
623 }
624 }
625 e => unreachable!("{:?}", e),
626 }
627 }
629 async fn claim_term(&mut self) -> Result<(), Error> {
630 terminal::enable_raw_mode()?;
631 let mut stdout = stdout();
632 execute!(stdout, terminal::EnterAlternateScreen)?;
633 if self.config.editor.mouse {
634 execute!(stdout, EnableMouseCapture)?;
635 }
636 Ok(())
637 }
639 fn restore_term(&mut self) -> Result<(), Error> {
640 let mut stdout = stdout();
641 // reset cursor shape
642 write!(stdout, "\x1B[2 q")?;
643 // Ignore errors on disabling, this might trigger on windows if we call
644 // disable without calling enable previously
645 let _ = execute!(stdout, DisableMouseCapture);
646 execute!(stdout, terminal::LeaveAlternateScreen)?;
647 terminal::disable_raw_mode()?;
648 Ok(())
649 }
651 pub async fn run(&mut self) -> Result<i32, Error> {
652 self.claim_term().await?;
654 // Exit the alternate screen and disable raw mode before panicking
655 let hook = std::panic::take_hook();
656 std::panic::set_hook(Box::new(move |info| {
657 // We can't handle errors properly inside this closure. And it's
658 // probably not a good idea to `unwrap()` inside a panic handler.
659 // So we just ignore the `Result`s.
660 let _ = execute!(std::io::stdout(), DisableMouseCapture);
661 let _ = execute!(std::io::stdout(), terminal::LeaveAlternateScreen);
662 let _ = terminal::disable_raw_mode();
663 hook(info);
664 }));
666 self.event_loop().await;
668 self.jobs.finish().await;
670 if self.editor.close_language_servers(None).await.is_err() {
671 log::error!("Timed out waiting for language servers to shutdown");
672 };
674 self.restore_term()?;
676 Ok(self.editor.exit_code)
677 }