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};
8 commands::{align_view, apply_workspace_edit, Align},
9 compositor::Compositor,
15use log::{error, warn};
18 io::{stdin, stdout, Write},
20 time::{Duration, Instant},
26 event::{DisableMouseCapture, EnableMouseCapture, Event, EventStream},
32 signal_hook::{consts::signal, low_level},
33 signal_hook_tokio::Signals,
36type Signals = futures_util::stream::Empty<()>;
38pub struct Application {
39 compositor: Compositor,
42 // TODO should be separate to take only part of the config
45 // Currently never read from. Remove the `allow(dead_code)` when
48 theme_loader: Arc<theme::Loader>,
50 // Currently never read from. Remove the `allow(dead_code)` when
53 syn_loader: Arc<syntax::Loader>,
57 lsp_progress: LspProgressMap,
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();
69 std::sync::Arc::new(theme::Loader::new(&conf_dir, &helix_core::runtime_dir()));
71 // load default and user config, and merge both
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"))
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),
87 let true_color = config.editor.true_color || crate::true_color();
95 log::warn!("failed to load theme `{}` - {}", theme, e);
99 .filter(|theme| (true_color || theme.is_16_color()))
103 theme_loader.default()
105 theme_loader.base16_default()
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");
115 // This waits for an enter press.
116 let _ = std::io::stdin().read(&mut []);
119 let syn_loader = std::sync::Arc::new(syntax::Loader::new(syn_loader_conf));
121 let mut editor = Editor::new(
123 theme_loader.clone(),
125 config.editor.clone(),
128 let editor_view = Box::new(ui::EditorView::new(std::mem::take(&mut config.keys)));
129 compositor.push(editor_view);
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
139 std::env::set_current_dir(&first)?;
140 editor.new_file(Action::VerticalSplit);
141 compositor.push(Box::new(ui::file_picker(".".into(), &config.editor)));
143 let nr_of_files = args.files.len();
144 editor.open(first.to_path_buf(), Action::VerticalSplit)?;
145 for (file, pos) in args.files {
147 return Err(anyhow::anyhow!(
148 "expected a path to file, found a directory. (to open a directory pass it as first argument)"
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);
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);
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");
174 .new_file_from_stdin(Action::VerticalSplit)
175 .unwrap_or_else(|_| editor.new_file(Action::VerticalSplit));
178 editor.set_theme(theme);
181 let signals = futures_util::stream::empty();
183 let signals = Signals::new(&[signal::SIGTSTP, signal::SIGCONT])?;
196 lsp_progress: LspProgressMap::new(),
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 {
213 compositor.render(&mut cx);
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;
224 if self.editor.should_close() {
228 use futures_util::StreamExt;
233 event = reader.next() => {
234 self.handle_terminal_events(event)
236 Some(signal) = self.signals.next() => {
237 self.handle_signals(signal).await;
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 {
245 last_render = Instant::now();
248 Some(callback) = self.jobs.futures.next() => {
249 self.jobs.handle_callback(&mut self.editor, &mut self.compositor, callback);
252 Some(callback) = self.jobs.wait_futures.next() => {
253 self.jobs.handle_callback(&mut self.editor, &mut self.compositor, callback);
256 _ = &mut self.editor.idle_timer => {
258 self.editor.clear_idle_timer();
259 self.handle_idle_timeout();
266 // no signal handling available on windows
267 pub async fn handle_signals(&mut self, _signal: ()) {}
270 pub async fn handle_signals(&mut self, signal: i32) {
271 use helix_view::graphics::Rect;
274 self.compositor.save_cursor();
275 self.restore_term().unwrap();
276 low_level::emulate_default_handler(signal::SIGTSTP).unwrap();
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();
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 {
297 let editor_view = self
299 .find::<ui::EditorView>()
300 .expect("expected at least one EditorView");
302 if editor_view.completion.is_some() {
306 let mut cx = Context {
308 editor: &mut self.editor,
309 jobs: &mut self.jobs,
312 on_next_key_callback: None,
314 idle_completion(&mut cx);
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,
325 let should_redraw = match event {
326 Some(Ok(Event::Resize(width, height))) => {
327 self.compositor.resize(width, height);
330 .handle_event(Event::Resize(width, height), &mut cx)
332 Some(Ok(event)) => self.compositor.handle_event(event, &mut cx),
333 Some(Err(x)) => panic!("{}", x),
337 if should_redraw && !self.editor.should_close() {
342 pub async fn handle_language_server_message(
344 call: helix_lsp::Call,
347 use helix_lsp::{Call, MethodCall, Notification};
350 Call::Notification(helix_lsp::jsonrpc::Notification { method, params, .. }) => {
351 let notification = match Notification::parse(&method, params) {
352 Some(notification) => 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,
362 warn!("can't find language server with id `{}`", server_id);
367 let docs = self.editor.documents().filter(|doc| {
368 doc.language_server().map(|server| server.id()) == Some(server_id)
371 // trigger textDocument/didOpen for docs that are already open
374 doc.language_id().map(ToOwned::to_owned).unwrap_or_default();
376 tokio::spawn(language_server.text_document_did_open(
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
395 .filter_map(|diagnostic| {
397 diagnostic::{Range, Severity::*},
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(
407 diagnostic.range.start,
408 language_server.offset_encoding(),
412 log::warn!("lsp position out of bounds - {:?}", diagnostic);
416 let end = if let Some(end) = lsp_pos_to_pos(
418 diagnostic.range.end,
419 language_server.offset_encoding(),
423 log::warn!("lsp position out of bounds - {:?}", diagnostic);
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: {:?}",
439 if let Some(lang_conf) = lang_conf {
440 if let Some(severity) = severity {
441 if severity < lang_conf.diagnostic_severity {
448 range: Range { start, end },
449 line: diagnostic.range.start.line as usize,
450 message: diagnostic.message,
458 doc.set_diagnostics(diagnostics);
461 Notification::ShowMessage(params) => {
462 log::warn!("unhandled window/showMessage: {:?}", params);
464 Notification::LogMessage(params) => {
465 log::info!("window/logMessage: {:?}", params);
467 Notification::ProgressMessage(params)
470 .has_component(std::any::type_name::<ui::Prompt>()) =>
472 let editor_view = self
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 {
485 }) => (Some(title), message, percentage),
486 lsp::WorkDoneProgress::Report(lsp::WorkDoneProgressReport {
490 }) => (None, message, percentage),
491 lsp::WorkDoneProgress::End(lsp::WorkDoneProgressEnd { message }) => {
492 if message.is_some() {
493 (None, message, &None)
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();
499 self.editor.clear_status();
501 // we want to render to clear any leftover spinners or messages
507 let token_d: &dyn std::fmt::Display = match &token {
508 lsp::NumberOrString::Number(n) => n,
509 lsp::NumberOrString::String(s) => s,
512 let status = match parts {
513 (Some(title), Some(message), Some(percentage)) => {
514 format!("[{}] {}% {} - {}", token_d, percentage, title, message)
516 (Some(title), None, Some(percentage)) => {
517 format!("[{}] {}% {}", token_d, percentage, title)
519 (Some(title), Some(message), None) => {
520 format!("[{}] {} - {}", token_d, title, message)
522 (None, Some(message), Some(percentage)) => {
523 format!("[{}] {}% {}", token_d, percentage, message)
525 (Some(title), None, None) => {
526 format!("[{}] {}", token_d, title)
528 (None, Some(message), None) => {
529 format!("[{}] {}", token_d, message)
531 (None, None, Some(percentage)) => {
532 format!("[{}] {}%", token_d, percentage)
534 (None, None, None) => format!("[{}]", token_d),
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();
543 self.lsp_progress.update(server_id, token, work);
546 if self.config.lsp.display_messages {
547 self.editor.set_status(status);
550 Notification::ProgressMessage(_params) => {
555 Call::MethodCall(helix_lsp::jsonrpc::MethodCall {
556 method, params, id, ..
558 let call = match MethodCall::parse(&method, params) {
561 error!("Method not found {}", method);
562 // language_server.reply(
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(),
576 MethodCall::WorkDoneProgressCreate(params) => {
577 self.lsp_progress.create(server_id, params.token);
579 let editor_view = self
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() {
587 let language_server =
588 match self.editor.language_servers.get_by_id(server_id) {
589 Some(language_server) => language_server,
591 warn!("can't find language server with id `{}`", server_id);
596 tokio::spawn(language_server.reply(id, Ok(serde_json::Value::Null)));
598 MethodCall::ApplyWorkspaceEdit(params) => {
599 apply_workspace_edit(
601 helix_lsp::OffsetEncoding::Utf8,
605 let language_server =
606 match self.editor.language_servers.get_by_id(server_id) {
607 Some(language_server) => language_server,
609 warn!("can't find language server with id `{}`", server_id);
614 tokio::spawn(language_server.reply(
616 Ok(json!(lsp::ApplyWorkspaceEditResponse {
618 failure_reason: None,
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 // 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()?;
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();
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");
674 self.restore_term()?;
676 Ok(self.editor.exit_code)
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