1use helix_core::{syntax, Range, Selection};
3use helix_lsp::{lsp, util::lsp_pos_to_pos, LspProgressMap};
4use helix_view::{theme, Editor};
8 commands::{align_view, Align},
9 compositor::Compositor,
16use smallvec::smallvec;
20 time::{Duration, Instant},
26 event::{DisableMouseCapture, EnableMouseCapture, Event, EventStream},
31 signal_hook::{consts::signal, low_level},
32 signal_hook_tokio::Signals,
35type Signals = futures_util::stream::Empty<()>;
37pub struct Application {
38 compositor: Compositor,
41 // TODO should be separate to take only part of the config
44 // Currently never read from. Remove the `allow(dead_code)` when
47 theme_loader: Arc<theme::Loader>,
49 // Currently never read from. Remove the `allow(dead_code)` when
52 syn_loader: Arc<syntax::Loader>,
56 lsp_progress: LspProgressMap,
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();
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
74 .unwrap_or(include_bytes!("../../languages.toml"));
76 let theme = if let Some(theme) = &config.theme {
77 match theme_loader.load(theme) {
80 log::warn!("failed to load theme `{}` - {}", theme, e);
81 theme_loader.default()
85 theme_loader.default()
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(
95 config.editor.clone(),
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
104 editor.new_file(Action::VerticalSplit);
105 compositor.push(Box::new(ui::file_picker(first.clone())));
107 let nr_of_files = args.files.len();
108 editor.open(first.to_path_buf(), Action::VerticalSplit)?;
109 for file in args.files {
111 return Err(anyhow::anyhow!(
112 "expected a path to file, found a directory. (to open a directory pass it as first argument)"
115 editor.open(file.to_path_buf(), Action::Load)?;
118 editor.set_status(format!("Loaded {} files.", nr_of_files));
121 editor.new_file(Action::VerticalSplit);
124 editor.set_theme(theme);
127 let signals = futures_util::stream::empty();
129 let signals = Signals::new(&[signal::SIGTSTP, signal::SIGCONT])?;
142 lsp_progress: LspProgressMap::new(),
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 {
159 compositor.render(&mut cx);
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;
170 if self.editor.should_close() {
175 use futures_util::StreamExt;
180 event = reader.next() => {
181 self.handle_terminal_events(event)
183 Some(signal) = self.signals.next() => {
184 self.handle_signals(signal).await;
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 {
192 last_render = Instant::now();
195 Some(payload) = self.editor.debugger_events.next() => {
196 self.handle_debugger_message(payload).await;
198 Some(callback) = self.jobs.futures.next() => {
199 self.jobs.handle_callback(&mut self.editor, &mut self.compositor, callback);
202 Some(callback) = self.jobs.wait_futures.next() => {
203 self.jobs.handle_callback(&mut self.editor, &mut self.compositor, callback);
211 // no signal handling available on windows
212 pub async fn handle_signals(&mut self, _signal: ()) {}
215 pub async fn handle_signals(&mut self, signal: i32) {
216 use helix_view::graphics::Rect;
219 self.compositor.save_cursor();
220 self.restore_term().unwrap();
221 low_level::emulate_default_handler(signal::SIGTSTP).unwrap();
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();
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,
242 let should_redraw = match event {
243 Some(Ok(Event::Resize(width, height))) => {
244 self.compositor.resize(width, height);
247 .handle_event(Event::Resize(width, height), &mut cx)
249 Some(Ok(event)) => self.compositor.handle_event(event, &mut cx),
250 Some(Err(x)) => panic!("{}", x),
254 if should_redraw && !self.editor.should_close() {
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,
267 Payload::Event(ev) => match ev {
268 Event::Stopped(events::Stopped {
276 debugger.is_running = false;
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);
289 let scope = match thread_id {
290 Some(id) => format!("Thread {}", id),
291 None => "Target".to_owned(),
294 let mut status = format!("{} stopped because of {}", scope, reason);
295 if let Some(desc) = description {
296 status.push_str(&format!(" {}", desc));
298 if let Some(text) = text {
299 status.push_str(&format!(" {}", text));
301 if all_threads_stopped.unwrap_or_default() {
302 status.push_str(" (all threads stopped)");
305 if let Some(helix_dap::StackFrame {
307 Some(helix_dap::Source {
315 }) = &debugger.stack_pointer
317 let path = src.clone();
319 let column = *column;
320 let end_line = *end_line;
321 let end_column = *end_column;
323 .open(path, helix_view::editor::Action::Replace)
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);
336 smallvec![Range::new(start.min(text_end), end.min(text_end))],
341 doc.set_selection(view.id, Selection::point(start.min(text_end)));
343 align_view(doc, view, Align::Center);
345 self.editor.set_status(status);
347 Event::Output(events::Output {
350 let prefix = match category {
352 if &category == "telemetry" {
355 format!("Debug ({}):", category)
357 None => "Debug:".to_owned(),
360 self.editor.set_status(format!("{} {}", prefix, output));
362 Event::Initialized => {
364 .set_status("Debugged application started".to_owned());
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;
374 log::warn!("Unhandled event {:?}", ev);
375 return; // return early to skip render
378 Payload::Response(_) => unreachable!(),
379 Payload::Request(_) => todo!(),
384 pub async fn handle_language_server_message(
386 call: helix_lsp::Call,
389 use helix_lsp::{Call, MethodCall, Notification};
390 let editor_view = self
392 .find(std::any::type_name::<ui::EditorView>())
393 .expect("expected at least one EditorView");
394 let editor_view = editor_view
396 .downcast_mut::<ui::EditorView>()
400 Call::Notification(helix_lsp::jsonrpc::Notification { method, params, .. }) => {
401 let notification = match Notification::parse(&method, params) {
402 Some(notification) => notification,
407 Notification::PublishDiagnostics(params) => {
408 let path = Some(params.uri.to_file_path().unwrap());
414 .find(|(_, doc)| doc.path() == path.as_ref());
416 if let Some((_, doc)) = doc {
417 let text = doc.text();
419 let diagnostics = params
422 .filter_map(|diagnostic| {
424 diagnostic::{Range, Severity::*},
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(
434 diagnostic.range.start,
435 language_server.offset_encoding(),
439 log::warn!("lsp position out of bounds - {:?}", diagnostic);
443 let end = if let Some(end) = lsp_pos_to_pos(
445 diagnostic.range.end,
446 language_server.offset_encoding(),
450 log::warn!("lsp position out of bounds - {:?}", 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,
472 doc.set_diagnostics(diagnostics);
475 Notification::ShowMessage(params) => {
476 log::warn!("unhandled window/showMessage: {:?}", params);
478 Notification::LogMessage(params) => {
479 log::warn!("unhandled window/logMessage: {:?}", params);
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 {
491 }) => (Some(title), message, percentage),
492 lsp::WorkDoneProgress::Report(lsp::WorkDoneProgressReport {
496 }) => (None, message, percentage),
497 lsp::WorkDoneProgress::End(lsp::WorkDoneProgressEnd { message }) => {
498 if message.is_some() {
499 (None, message, &None)
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();
505 self.editor.clear_status();
507 // we want to render to clear any leftover spinners or messages
513 let token_d: &dyn std::fmt::Display = match &token {
514 lsp::NumberOrString::Number(n) => n,
515 lsp::NumberOrString::String(s) => s,
518 let status = match parts {
519 (Some(title), Some(message), Some(percentage)) => {
520 format!("[{}] {}% {} - {}", token_d, percentage, title, message)
522 (Some(title), None, Some(percentage)) => {
523 format!("[{}] {}% {}", token_d, percentage, title)
525 (Some(title), Some(message), None) => {
526 format!("[{}] {} - {}", token_d, title, message)
528 (None, Some(message), Some(percentage)) => {
529 format!("[{}] {}% {}", token_d, percentage, message)
531 (Some(title), None, None) => {
532 format!("[{}] {}", token_d, title)
534 (None, Some(message), None) => {
535 format!("[{}] {}", token_d, message)
537 (None, None, Some(percentage)) => {
538 format!("[{}] {}%", token_d, percentage)
540 (None, None, None) => format!("[{}]", token_d),
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();
549 self.lsp_progress.update(server_id, token, work);
552 if self.config.lsp.display_messages {
553 self.editor.set_status(status);
558 Call::MethodCall(helix_lsp::jsonrpc::MethodCall {
559 method, params, id, ..
561 let call = match MethodCall::parse(&method, params) {
564 error!("Method not found {}", method);
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() {
578 let doc = self.editor.documents().find(|doc| {
579 doc.language_server()
580 .map(|server| server.id() == server_id)
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)));
590 if let Some(server) =
591 self.editor.language_servers.get_by_id(server_id)
594 "missing document with language server id `{}`",
597 tokio::spawn(server.reply(
599 Err(helix_lsp::jsonrpc::Error {
600 code: helix_lsp::jsonrpc::ErrorCode::InternalError,
601 message: "document missing".to_string(),
607 "can't find language server with id `{}`",
615 // self.language_server.reply(
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(),
625 e => unreachable!("{:?}", e),
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)?;
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()?;
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();
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");
670 self.restore_term()?;
keysesc to close
- j k
- move down, up
- enter
- open
- esc
- back, or up a level
- 1 2 3 4
- readme, files, commits, refs
- /
- filter
- y
- copy the clone command
- c
- compare the chosen commit with another (or shift-click both)
- ?
- show or hide this list