on 2158366 help sign in
helix/helix-term/src/application.rs
674 lines 27 kB history raw
1use helix_core::{syntax, Range, Selection};
2use helix_dap::Payload;
3use helix_lsp::{lsp, util::lsp_pos_to_pos, LspProgressMap};
4use helix_view::{theme, Editor};
6use crate::{
7 args::Args,
8 commands::{align_view, Align},
9 compositor::Compositor,
10 config::Config,
11 job::Jobs,
12 ui,
13};
15use log::error;
16use smallvec::smallvec;
17use std::{
18 io::{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};
29#[cfg(not(windows))]
30use {
31 signal_hook::{consts::signal, low_level},
32 signal_hook_tokio::Signals,
33};
34#[cfg(windows)]
35type Signals = futures_util::stream::Empty<()>;
37pub struct Application {
38 compositor: Compositor,
39 editor: Editor,
41 // TODO should be separate to take only part of the config
42 config: Config,
44 // Currently never read from. Remove the `allow(dead_code)` when
45 // that changes.
46 #[allow(dead_code)]
47 theme_loader: Arc<theme::Loader>,
49 // Currently never read from. Remove the `allow(dead_code)` when
50 // that changes.
51 #[allow(dead_code)]
52 syn_loader: Arc<syntax::Loader>,
54 signals: Signals,
55 jobs: Jobs,
56 lsp_progress: LspProgressMap,
59impl Application {
60 pub fn new(args: Args, mut config: Config) -> Result<Self, Error> {
61 use helix_view::editor::Action;
62 let mut compositor = Compositor::new()?;
63 let size = compositor.size();
65 let conf_dir = helix_core::config_dir();
67 let theme_loader =
68 std::sync::Arc::new(theme::Loader::new(&conf_dir, &helix_core::runtime_dir()));
70 // load $HOME/.config/helix/languages.toml, fallback to default config
71 let lang_conf = std::fs::read(conf_dir.join("languages.toml"));
72 let lang_conf = lang_conf
73 .as_deref()
74 .unwrap_or(include_bytes!("../../languages.toml"));
76 let theme = if let Some(theme) = &config.theme {
77 match theme_loader.load(theme) {
78 Ok(theme) => theme,
79 Err(e) => {
80 log::warn!("failed to load theme `{}` - {}", theme, e);
81 theme_loader.default()
82 }
83 }
84 } else {
85 theme_loader.default()
86 };
88 let syn_loader_conf = toml::from_slice(lang_conf).expect("Could not parse languages.toml");
89 let syn_loader = std::sync::Arc::new(syntax::Loader::new(syn_loader_conf));
91 let mut editor = Editor::new(
92 size,
93 theme_loader.clone(),
94 syn_loader.clone(),
95 config.editor.clone(),
96 );
98 let editor_view = Box::new(ui::EditorView::new(std::mem::take(&mut config.keys)));
99 compositor.push(editor_view);
101 if !args.files.is_empty() {
102 let first = &args.files[0]; // we know it's not empty
103 if first.is_dir() {
104 editor.new_file(Action::VerticalSplit);
105 compositor.push(Box::new(ui::file_picker(first.clone())));
106 } else {
107 let nr_of_files = args.files.len();
108 editor.open(first.to_path_buf(), Action::VerticalSplit)?;
109 for file in args.files {
110 if file.is_dir() {
111 return Err(anyhow::anyhow!(
112 "expected a path to file, found a directory. (to open a directory pass it as first argument)"
113 ));
114 } else {
115 editor.open(file.to_path_buf(), Action::Load)?;
116 }
117 }
118 editor.set_status(format!("Loaded {} files.", nr_of_files));
119 }
120 } else {
121 editor.new_file(Action::VerticalSplit);
122 }
124 editor.set_theme(theme);
126 #[cfg(windows)]
127 let signals = futures_util::stream::empty();
128 #[cfg(not(windows))]
129 let signals = Signals::new(&[signal::SIGTSTP, signal::SIGCONT])?;
131 let app = Self {
132 compositor,
133 editor,
135 config,
137 theme_loader,
138 syn_loader,
140 signals,
141 jobs: Jobs::new(),
142 lsp_progress: LspProgressMap::new(),
143 };
145 Ok(app)
146 }
148 fn render(&mut self) {
149 let editor = &mut self.editor;
150 let compositor = &mut self.compositor;
151 let jobs = &mut self.jobs;
153 let mut cx = crate::compositor::Context {
154 editor,
155 jobs,
156 scroll: None,
157 };
159 compositor.render(&mut cx);
160 }
162 pub async fn event_loop(&mut self) {
163 let mut reader = EventStream::new();
164 let mut last_render = Instant::now();
165 let deadline = Duration::from_secs(1) / 60;
167 self.render();
169 loop {
170 if self.editor.should_close() {
171 self.jobs.finish();
172 break;
173 }
175 use futures_util::StreamExt;
177 tokio::select! {
178 biased;
180 event = reader.next() => {
181 self.handle_terminal_events(event)
182 }
183 Some(signal) = self.signals.next() => {
184 self.handle_signals(signal).await;
185 }
186 Some((id, call)) = self.editor.language_servers.incoming.next() => {
187 self.handle_language_server_message(call, id).await;
188 // limit render calls for fast language server messages
189 let last = self.editor.language_servers.incoming.is_empty();
190 if last || last_render.elapsed() > deadline {
191 self.render();
192 last_render = Instant::now();
193 }
194 }
195 Some(payload) = self.editor.debugger_events.next() => {
196 self.handle_debugger_message(payload).await;
197 }
198 Some(callback) = self.jobs.futures.next() => {
199 self.jobs.handle_callback(&mut self.editor, &mut self.compositor, callback);
200 self.render();
201 }
202 Some(callback) = self.jobs.wait_futures.next() => {
203 self.jobs.handle_callback(&mut self.editor, &mut self.compositor, callback);
204 self.render();
205 }
206 }
207 }
208 }
210 #[cfg(windows)]
211 // no signal handling available on windows
212 pub async fn handle_signals(&mut self, _signal: ()) {}
214 #[cfg(not(windows))]
215 pub async fn handle_signals(&mut self, signal: i32) {
216 use helix_view::graphics::Rect;
217 match signal {
218 signal::SIGTSTP => {
219 self.compositor.save_cursor();
220 self.restore_term().unwrap();
221 low_level::emulate_default_handler(signal::SIGTSTP).unwrap();
222 }
223 signal::SIGCONT => {
224 self.claim_term().await.unwrap();
225 // redraw the terminal
226 let Rect { width, height, .. } = self.compositor.size();
227 self.compositor.resize(width, height);
228 self.compositor.load_cursor();
229 self.render();
230 }
231 _ => unreachable!(),
232 }
233 }
235 pub fn handle_terminal_events(&mut self, event: Option<Result<Event, crossterm::ErrorKind>>) {
236 let mut cx = crate::compositor::Context {
237 editor: &mut self.editor,
238 jobs: &mut self.jobs,
239 scroll: None,
240 };
241 // Handle key events
242 let should_redraw = match event {
243 Some(Ok(Event::Resize(width, height))) => {
244 self.compositor.resize(width, height);
246 self.compositor
247 .handle_event(Event::Resize(width, height), &mut cx)
248 }
249 Some(Ok(event)) => self.compositor.handle_event(event, &mut cx),
250 Some(Err(x)) => panic!("{}", x),
251 None => panic!(),
252 };
254 if should_redraw && !self.editor.should_close() {
255 self.render();
256 }
257 }
259 pub async fn handle_debugger_message(&mut self, payload: helix_dap::Payload) {
260 use helix_dap::{events, Event};
261 let mut debugger = match self.editor.debugger.as_mut() {
262 Some(debugger) => debugger,
263 None => return,
264 };
266 match payload {
267 Payload::Event(ev) => match ev {
268 Event::Stopped(events::Stopped {
269 thread_id,
270 description,
271 text,
272 reason,
273 all_threads_stopped,
274 ..
275 }) => {
276 debugger.is_running = false;
277 let main = debugger
278 .threads()
279 .await
280 .ok()
281 .and_then(|threads| threads.get(0).cloned());
283 if let Some(main) = main {
284 let (bt, _) = debugger.stack_trace(main.id).await.unwrap();
285 debugger.stack_pointer = bt.get(0).cloned();
286 debugger.stopped_thread = Some(main.id);
287 }
289 let scope = match thread_id {
290 Some(id) => format!("Thread {}", id),
291 None => "Target".to_owned(),
292 };
294 let mut status = format!("{} stopped because of {}", scope, reason);
295 if let Some(desc) = description {
296 status.push_str(&format!(" {}", desc));
297 }
298 if let Some(text) = text {
299 status.push_str(&format!(" {}", text));
300 }
301 if all_threads_stopped.unwrap_or_default() {
302 status.push_str(" (all threads stopped)");
303 }
305 if let Some(helix_dap::StackFrame {
306 source:
307 Some(helix_dap::Source {
308 path: Some(src), ..
309 }),
310 line,
311 column,
312 end_line,
313 end_column,
314 ..
315 }) = &debugger.stack_pointer
316 {
317 let path = src.clone();
318 let line = *line;
319 let column = *column;
320 let end_line = *end_line;
321 let end_column = *end_column;
322 self.editor
323 .open(path, helix_view::editor::Action::Replace)
324 .unwrap();
326 let (view, doc) = current!(self.editor);
328 let text_end = doc.text().len_chars().saturating_sub(1);
329 let start = doc.text().try_line_to_char(line - 1).unwrap_or(0) + column;
330 if let Some(end_line) = end_line {
331 let end = doc.text().try_line_to_char(end_line - 1).unwrap_or(0)
332 + end_column.unwrap_or(0);
333 doc.set_selection(
334 view.id,
335 Selection::new(
336 smallvec![Range::new(start.min(text_end), end.min(text_end))],
337 0,
338 ),
339 );
340 } else {
341 doc.set_selection(view.id, Selection::point(start.min(text_end)));
342 }
343 align_view(doc, view, Align::Center);
344 }
345 self.editor.set_status(status);
346 }
347 Event::Output(events::Output {
348 category, output, ..
349 }) => {
350 let prefix = match category {
351 Some(category) => {
352 if &category == "telemetry" {
353 return;
354 }
355 format!("Debug ({}):", category)
356 }
357 None => "Debug:".to_owned(),
358 };
360 self.editor.set_status(format!("{} {}", prefix, output));
361 }
362 Event::Initialized => {
363 self.editor
364 .set_status("Debugged application started".to_owned());
365 }
366 Event::Continued(_) => {
367 if let Some(debugger) = self.editor.debugger.as_mut() {
368 debugger.stopped_thread = None;
369 debugger.stack_pointer = None;
370 debugger.is_running = true;
371 }
372 }
373 ev => {
374 log::warn!("Unhandled event {:?}", ev);
375 return; // return early to skip render
376 }
377 },
378 Payload::Response(_) => unreachable!(),
379 Payload::Request(_) => todo!(),
380 }
381 self.render();
382 }
384 pub async fn handle_language_server_message(
385 &mut self,
386 call: helix_lsp::Call,
387 server_id: usize,
388 ) {
389 use helix_lsp::{Call, MethodCall, Notification};
390 let editor_view = self
391 .compositor
392 .find(std::any::type_name::<ui::EditorView>())
393 .expect("expected at least one EditorView");
394 let editor_view = editor_view
395 .as_any_mut()
396 .downcast_mut::<ui::EditorView>()
397 .unwrap();
399 match call {
400 Call::Notification(helix_lsp::jsonrpc::Notification { method, params, .. }) => {
401 let notification = match Notification::parse(&method, params) {
402 Some(notification) => notification,
403 None => return,
404 };
406 match notification {
407 Notification::PublishDiagnostics(params) => {
408 let path = Some(params.uri.to_file_path().unwrap());
410 let doc = self
411 .editor
412 .documents
413 .iter_mut()
414 .find(|(_, doc)| doc.path() == path.as_ref());
416 if let Some((_, doc)) = doc {
417 let text = doc.text();
419 let diagnostics = params
420 .diagnostics
421 .into_iter()
422 .filter_map(|diagnostic| {
423 use helix_core::{
424 diagnostic::{Range, Severity::*},
425 Diagnostic,
426 };
427 use lsp::DiagnosticSeverity;
429 let language_server = doc.language_server().unwrap();
431 // TODO: convert inside server
432 let start = if let Some(start) = lsp_pos_to_pos(
433 text,
434 diagnostic.range.start,
435 language_server.offset_encoding(),
436 ) {
437 start
438 } else {
439 log::warn!("lsp position out of bounds - {:?}", diagnostic);
440 return None;
441 };
443 let end = if let Some(end) = lsp_pos_to_pos(
444 text,
445 diagnostic.range.end,
446 language_server.offset_encoding(),
447 ) {
448 end
449 } else {
450 log::warn!("lsp position out of bounds - {:?}", diagnostic);
451 return None;
452 };
454 Some(Diagnostic {
455 range: Range { start, end },
456 line: diagnostic.range.start.line as usize,
457 message: diagnostic.message,
458 severity: diagnostic.severity.map(
459 |severity| match severity {
460 DiagnosticSeverity::Error => Error,
461 DiagnosticSeverity::Warning => Warning,
462 DiagnosticSeverity::Information => Info,
463 DiagnosticSeverity::Hint => Hint,
464 },
465 ),
466 // code
467 // source
468 })
469 })
470 .collect();
472 doc.set_diagnostics(diagnostics);
473 }
474 }
475 Notification::ShowMessage(params) => {
476 log::warn!("unhandled window/showMessage: {:?}", params);
477 }
478 Notification::LogMessage(params) => {
479 log::warn!("unhandled window/logMessage: {:?}", params);
480 }
481 Notification::ProgressMessage(params) => {
482 let lsp::ProgressParams { token, value } = params;
484 let lsp::ProgressParamsValue::WorkDone(work) = value;
485 let parts = match &work {
486 lsp::WorkDoneProgress::Begin(lsp::WorkDoneProgressBegin {
487 title,
488 message,
489 percentage,
490 ..
491 }) => (Some(title), message, percentage),
492 lsp::WorkDoneProgress::Report(lsp::WorkDoneProgressReport {
493 message,
494 percentage,
495 ..
496 }) => (None, message, percentage),
497 lsp::WorkDoneProgress::End(lsp::WorkDoneProgressEnd { message }) => {
498 if message.is_some() {
499 (None, message, &None)
500 } else {
501 self.lsp_progress.end_progress(server_id, &token);
502 if !self.lsp_progress.is_progressing(server_id) {
503 editor_view.spinners_mut().get_or_create(server_id).stop();
504 }
505 self.editor.clear_status();
507 // we want to render to clear any leftover spinners or messages
508 return;
509 }
510 }
511 };
513 let token_d: &dyn std::fmt::Display = match &token {
514 lsp::NumberOrString::Number(n) => n,
515 lsp::NumberOrString::String(s) => s,
516 };
518 let status = match parts {
519 (Some(title), Some(message), Some(percentage)) => {
520 format!("[{}] {}% {} - {}", token_d, percentage, title, message)
521 }
522 (Some(title), None, Some(percentage)) => {
523 format!("[{}] {}% {}", token_d, percentage, title)
524 }
525 (Some(title), Some(message), None) => {
526 format!("[{}] {} - {}", token_d, title, message)
527 }
528 (None, Some(message), Some(percentage)) => {
529 format!("[{}] {}% {}", token_d, percentage, message)
530 }
531 (Some(title), None, None) => {
532 format!("[{}] {}", token_d, title)
533 }
534 (None, Some(message), None) => {
535 format!("[{}] {}", token_d, message)
536 }
537 (None, None, Some(percentage)) => {
538 format!("[{}] {}%", token_d, percentage)
539 }
540 (None, None, None) => format!("[{}]", token_d),
541 };
543 if let lsp::WorkDoneProgress::End(_) = work {
544 self.lsp_progress.end_progress(server_id, &token);
545 if !self.lsp_progress.is_progressing(server_id) {
546 editor_view.spinners_mut().get_or_create(server_id).stop();
547 }
548 } else {
549 self.lsp_progress.update(server_id, token, work);
550 }
552 if self.config.lsp.display_messages {
553 self.editor.set_status(status);
554 }
555 }
556 }
557 }
558 Call::MethodCall(helix_lsp::jsonrpc::MethodCall {
559 method, params, id, ..
560 }) => {
561 let call = match MethodCall::parse(&method, params) {
562 Some(call) => call,
563 None => {
564 error!("Method not found {}", method);
565 return;
566 }
567 };
569 match call {
570 MethodCall::WorkDoneProgressCreate(params) => {
571 self.lsp_progress.create(server_id, params.token);
573 let spinner = editor_view.spinners_mut().get_or_create(server_id);
574 if spinner.is_stopped() {
575 spinner.start();
576 }
578 let doc = self.editor.documents().find(|doc| {
579 doc.language_server()
580 .map(|server| server.id() == server_id)
581 .unwrap_or_default()
582 });
583 match doc {
584 Some(doc) => {
585 // it's ok to unwrap, we check for the language server before
586 let server = doc.language_server().unwrap();
587 tokio::spawn(server.reply(id, Ok(serde_json::Value::Null)));
588 }
589 None => {
590 if let Some(server) =
591 self.editor.language_servers.get_by_id(server_id)
592 {
593 log::warn!(
594 "missing document with language server id `{}`",
595 server_id
596 );
597 tokio::spawn(server.reply(
598 id,
599 Err(helix_lsp::jsonrpc::Error {
600 code: helix_lsp::jsonrpc::ErrorCode::InternalError,
601 message: "document missing".to_string(),
602 data: None,
603 }),
604 ));
605 } else {
606 log::warn!(
607 "can't find language server with id `{}`",
608 server_id
609 );
610 }
611 }
612 }
613 }
614 }
615 // self.language_server.reply(
616 // call.id,
617 // // TODO: make a Into trait that can cast to Err(jsonrpc::Error)
618 // Err(helix_lsp::jsonrpc::Error {
619 // code: helix_lsp::jsonrpc::ErrorCode::MethodNotFound,
620 // message: "Method not found".to_string(),
621 // data: None,
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 execute!(stdout, DisableMouseCapture)?;
644 execute!(stdout, terminal::LeaveAlternateScreen)?;
645 terminal::disable_raw_mode()?;
646 Ok(())
647 }
649 pub async fn run(&mut self) -> Result<(), Error> {
650 self.claim_term().await?;
652 // Exit the alternate screen and disable raw mode before panicking
653 let hook = std::panic::take_hook();
654 std::panic::set_hook(Box::new(move |info| {
655 // We can't handle errors properly inside this closure. And it's
656 // probably not a good idea to `unwrap()` inside a panic handler.
657 // So we just ignore the `Result`s.
658 let _ = execute!(std::io::stdout(), DisableMouseCapture);
659 let _ = execute!(std::io::stdout(), terminal::LeaveAlternateScreen);
660 let _ = terminal::disable_raw_mode();
661 hook(info);
662 }));
664 self.event_loop().await;
666 if self.editor.close_language_servers(None).await.is_err() {
667 log::error!("Timed out waiting for language servers to shutdown");
668 };
670 self.restore_term()?;
672 Ok(())
673 }