|
1 | | -use futures_util::{stream::FuturesOrdered, FutureExt}; |
| 1 | +use futures_util::{ |
| 2 | + stream::{FuturesOrdered, FuturesUnordered}, |
| 3 | + FutureExt, |
| 4 | +}; |
2 | 5 | use helix_lsp::{ |
3 | 6 | block_on, |
4 | 7 | lsp::{ |
5 | | - self, CodeAction, CodeActionOrCommand, CodeActionTriggerKind, DiagnosticSeverity, |
6 | | - NumberOrString, |
| 8 | + self, CodeAction, CodeActionKind, CodeActionOrCommand, CodeActionTriggerKind, |
| 9 | + DiagnosticSeverity, NumberOrString, |
7 | 10 | }, |
8 | 11 | util::{diagnostic_to_lsp_diagnostic, lsp_range_to_range, range_to_lsp_range}, |
9 | 12 | Client, LanguageServerId, OffsetEncoding, |
10 | 13 | }; |
| 14 | +use serde_json::Value; |
11 | 15 | use tokio_stream::StreamExt; |
12 | 16 | use tui::{text::Span, widgets::Row}; |
13 | 17 |
|
14 | 18 | use super::{align_view, push_jump, Align, Context, Editor}; |
15 | 19 |
|
16 | 20 | use helix_core::{ |
17 | | - syntax::LanguageServerFeature, text_annotations::InlineAnnotation, Selection, Uri, |
| 21 | + syntax::LanguageServerFeature, text_annotations::InlineAnnotation, Range, Selection, Uri, |
18 | 22 | }; |
19 | 23 | use helix_stdx::path; |
20 | 24 | use helix_view::{ |
21 | 25 | document::{DocumentInlayHints, DocumentInlayHintsId}, |
22 | 26 | editor::Action, |
23 | 27 | handlers::lsp::SignatureHelpInvoked, |
24 | 28 | theme::Style, |
25 | | - Document, View, |
| 29 | + Document, DocumentId, View, |
26 | 30 | }; |
27 | 31 |
|
28 | 32 | use crate::{ |
@@ -542,9 +546,9 @@ pub fn workspace_diagnostics_picker(cx: &mut Context) { |
542 | 546 | cx.push_layer(Box::new(overlaid(picker))); |
543 | 547 | } |
544 | 548 |
|
545 | | -struct CodeActionOrCommandItem { |
546 | | - lsp_item: lsp::CodeActionOrCommand, |
547 | | - language_server_id: LanguageServerId, |
| 549 | +pub struct CodeActionOrCommandItem { |
| 550 | + pub lsp_item: lsp::CodeActionOrCommand, |
| 551 | + pub language_server_id: LanguageServerId, |
548 | 552 | } |
549 | 553 |
|
550 | 554 | impl ui::menu::Item for CodeActionOrCommandItem { |
@@ -619,34 +623,8 @@ pub fn code_action(cx: &mut Context) { |
619 | 623 |
|
620 | 624 | let selection_range = doc.selection(view.id).primary(); |
621 | 625 |
|
622 | | - let mut seen_language_servers = HashSet::new(); |
623 | | - |
624 | | - let mut futures: FuturesOrdered<_> = doc |
625 | | - .language_servers_with_feature(LanguageServerFeature::CodeAction) |
626 | | - .filter(|ls| seen_language_servers.insert(ls.id())) |
627 | | - // TODO this should probably already been filtered in something like "language_servers_with_feature" |
628 | | - .filter_map(|language_server| { |
629 | | - let offset_encoding = language_server.offset_encoding(); |
630 | | - let language_server_id = language_server.id(); |
631 | | - let range = range_to_lsp_range(doc.text(), selection_range, offset_encoding); |
632 | | - // Filter and convert overlapping diagnostics |
633 | | - let code_action_context = lsp::CodeActionContext { |
634 | | - diagnostics: doc |
635 | | - .diagnostics() |
636 | | - .iter() |
637 | | - .filter(|&diag| { |
638 | | - selection_range |
639 | | - .overlaps(&helix_core::Range::new(diag.range.start, diag.range.end)) |
640 | | - }) |
641 | | - .map(|diag| diagnostic_to_lsp_diagnostic(doc.text(), diag, offset_encoding)) |
642 | | - .collect(), |
643 | | - only: None, |
644 | | - trigger_kind: Some(CodeActionTriggerKind::INVOKED), |
645 | | - }; |
646 | | - let code_action_request = |
647 | | - language_server.code_actions(doc.identifier(), range, code_action_context)?; |
648 | | - Some((code_action_request, language_server_id)) |
649 | | - }) |
| 626 | + let mut futures: FuturesUnordered<_> = code_actions_for_range(doc, selection_range, None) |
| 627 | + .into_iter() |
650 | 628 | .map(|(request, ls_id)| async move { |
651 | 629 | let json = request.await?; |
652 | 630 | let response: Option<lsp::CodeActionResponse> = serde_json::from_value(json)?; |
@@ -734,59 +712,174 @@ pub fn code_action(cx: &mut Context) { |
734 | 712 |
|
735 | 713 | // always present here |
736 | 714 | let action = action.unwrap(); |
737 | | - let Some(language_server) = editor.language_server_by_id(action.language_server_id) |
738 | | - else { |
739 | | - editor.set_error("Language Server disappeared"); |
740 | | - return; |
741 | | - }; |
742 | | - let offset_encoding = language_server.offset_encoding(); |
743 | 715 |
|
744 | | - match &action.lsp_item { |
745 | | - lsp::CodeActionOrCommand::Command(command) => { |
746 | | - log::debug!("code action command: {:?}", command); |
747 | | - execute_lsp_command(editor, action.language_server_id, command.clone()); |
748 | | - } |
749 | | - lsp::CodeActionOrCommand::CodeAction(code_action) => { |
750 | | - log::debug!("code action: {:?}", code_action); |
751 | | - // we support lsp "codeAction/resolve" for `edit` and `command` fields |
752 | | - let mut resolved_code_action = None; |
753 | | - if code_action.edit.is_none() || code_action.command.is_none() { |
754 | | - if let Some(future) = |
755 | | - language_server.resolve_code_action(code_action.clone()) |
| 716 | + apply_code_action(editor, action); |
| 717 | + }); |
| 718 | + picker.move_down(); // pre-select the first item |
| 719 | + |
| 720 | + let popup = Popup::new("code-action", picker).with_scrollbar(false); |
| 721 | + |
| 722 | + compositor.replace_or_push("code-action", popup); |
| 723 | + }; |
| 724 | + |
| 725 | + Ok(Callback::EditorCompositor(Box::new(call))) |
| 726 | + }); |
| 727 | +} |
| 728 | + |
| 729 | +pub fn code_actions_for_range( |
| 730 | + doc: &Document, |
| 731 | + range: helix_core::Range, |
| 732 | + only: Option<Vec<CodeActionKind>>, |
| 733 | +) -> Vec<( |
| 734 | + impl Future<Output = Result<Value, helix_lsp::Error>>, |
| 735 | + LanguageServerId, |
| 736 | +)> { |
| 737 | + let mut seen_language_servers = HashSet::new(); |
| 738 | + |
| 739 | + doc.language_servers_with_feature(LanguageServerFeature::CodeAction) |
| 740 | + .filter(|ls| seen_language_servers.insert(ls.id())) |
| 741 | + // TODO this should probably already been filtered in something like "language_servers_with_feature" |
| 742 | + .filter_map(|language_server| { |
| 743 | + let offset_encoding = language_server.offset_encoding(); |
| 744 | + let language_server_id = language_server.id(); |
| 745 | + let lsp_range = range_to_lsp_range(doc.text(), range, offset_encoding); |
| 746 | + // Filter and convert overlapping diagnostics |
| 747 | + let code_action_context = lsp::CodeActionContext { |
| 748 | + diagnostics: doc |
| 749 | + .diagnostics() |
| 750 | + .iter() |
| 751 | + .filter(|&diag| { |
| 752 | + range.overlaps(&helix_core::Range::new(diag.range.start, diag.range.end)) |
| 753 | + }) |
| 754 | + .map(|diag| diagnostic_to_lsp_diagnostic(doc.text(), diag, offset_encoding)) |
| 755 | + .collect(), |
| 756 | + only: only.clone(), |
| 757 | + trigger_kind: Some(CodeActionTriggerKind::INVOKED), |
| 758 | + }; |
| 759 | + let code_action_request = |
| 760 | + language_server.code_actions(doc.identifier(), lsp_range, code_action_context)?; |
| 761 | + Some((code_action_request, language_server_id)) |
| 762 | + }) |
| 763 | + .collect::<Vec<_>>() |
| 764 | +} |
| 765 | + |
| 766 | +/// Will apply the code actions on save from the language config for each language server |
| 767 | +pub fn code_actions_on_save(cx: &mut compositor::Context, doc_id: &DocumentId) { |
| 768 | + let doc = doc!(cx.editor, doc_id); |
| 769 | + |
| 770 | + let code_actions_on_save_cfg = doc |
| 771 | + .language_config() |
| 772 | + .and_then(|c| c.code_actions_on_save.clone()); |
| 773 | + |
| 774 | + if let Some(code_actions_on_save_cfg) = code_actions_on_save_cfg { |
| 775 | + for code_action_kind in code_actions_on_save_cfg.into_iter().filter_map(|action| { |
| 776 | + action |
| 777 | + .enabled |
| 778 | + .then_some(CodeActionKind::from(action.code_action)) |
| 779 | + }) { |
| 780 | + log::debug!("Attempting code action on save {:?}", code_action_kind); |
| 781 | + let doc = doc!(cx.editor, doc_id); |
| 782 | + let full_range = Range::new(0, doc.text().len_chars()); |
| 783 | + let code_actions: Vec<CodeActionOrCommandItem> = |
| 784 | + code_actions_for_range(doc, full_range, Some(vec![code_action_kind.clone()])) |
| 785 | + .into_iter() |
| 786 | + .filter_map(|(future, language_server_id)| { |
| 787 | + if let Ok(json) = helix_lsp::block_on(future) { |
| 788 | + if let Ok(Some(mut actions)) = serde_json::from_value::< |
| 789 | + Option<helix_lsp::lsp::CodeActionResponse>, |
| 790 | + >(json) |
756 | 791 | { |
757 | | - if let Ok(response) = helix_lsp::block_on(future) { |
758 | | - if let Ok(code_action) = |
759 | | - serde_json::from_value::<CodeAction>(response) |
760 | | - { |
761 | | - resolved_code_action = Some(code_action); |
762 | | - } |
| 792 | + // Retain only enabled code actions that do not have commands. |
| 793 | + // |
| 794 | + // Commands are deprecated and are not supported because they apply |
| 795 | + // workspace edits asynchronously and there is currently no mechanism |
| 796 | + // to handle waiting for the workspace edits to be applied before moving |
| 797 | + // on to the next code action (or auto-format). |
| 798 | + actions.retain(|action| { |
| 799 | + matches!( |
| 800 | + action, |
| 801 | + CodeActionOrCommand::CodeAction(CodeAction { |
| 802 | + disabled: None, |
| 803 | + command: None, |
| 804 | + .. |
| 805 | + }) |
| 806 | + ) |
| 807 | + }); |
| 808 | + |
| 809 | + // Use the first matching code action |
| 810 | + if let Some(lsp_item) = actions.first() { |
| 811 | + return Some(CodeActionOrCommandItem { |
| 812 | + lsp_item: lsp_item.clone(), |
| 813 | + language_server_id, |
| 814 | + }); |
763 | 815 | } |
764 | 816 | } |
765 | 817 | } |
766 | | - let resolved_code_action = |
767 | | - resolved_code_action.as_ref().unwrap_or(code_action); |
| 818 | + None |
| 819 | + }) |
| 820 | + .collect(); |
768 | 821 |
|
769 | | - if let Some(ref workspace_edit) = resolved_code_action.edit { |
770 | | - let _ = editor.apply_workspace_edit(offset_encoding, workspace_edit); |
771 | | - } |
| 822 | + if code_actions.is_empty() { |
| 823 | + log::debug!("Code action on save not found {:?}", code_action_kind); |
| 824 | + cx.editor |
| 825 | + .set_error(format!("Code Action not found: {:?}", code_action_kind)); |
| 826 | + } |
772 | 827 |
|
773 | | - // if code action provides both edit and command first the edit |
774 | | - // should be applied and then the command |
775 | | - if let Some(command) = &code_action.command { |
776 | | - execute_lsp_command(editor, action.language_server_id, command.clone()); |
| 828 | + for code_action in code_actions { |
| 829 | + log::debug!( |
| 830 | + "Applying code action on save {:?} for language server {:?}", |
| 831 | + code_action.lsp_item, |
| 832 | + code_action.language_server_id |
| 833 | + ); |
| 834 | + apply_code_action(cx.editor, &code_action); |
| 835 | + |
| 836 | + // TODO: Find a better way to handle this |
| 837 | + // Sleep to avoid race condition between next code action/auto-format |
| 838 | + // and the textDocument/didChange notification |
| 839 | + std::thread::sleep(std::time::Duration::from_millis(10)); |
| 840 | + } |
| 841 | + } |
| 842 | + } |
| 843 | +} |
| 844 | + |
| 845 | +pub fn apply_code_action(editor: &mut Editor, action: &CodeActionOrCommandItem) { |
| 846 | + let Some(language_server) = editor.language_server_by_id(action.language_server_id) else { |
| 847 | + editor.set_error("Language Server disappeared"); |
| 848 | + return; |
| 849 | + }; |
| 850 | + let offset_encoding = language_server.offset_encoding(); |
| 851 | + |
| 852 | + match &action.lsp_item { |
| 853 | + lsp::CodeActionOrCommand::Command(command) => { |
| 854 | + log::debug!("code action command: {:?}", command); |
| 855 | + execute_lsp_command(editor, action.language_server_id, command.clone()); |
| 856 | + } |
| 857 | + lsp::CodeActionOrCommand::CodeAction(code_action) => { |
| 858 | + log::debug!("code action: {:?}", code_action); |
| 859 | + // we support lsp "codeAction/resolve" for `edit` and `command` fields |
| 860 | + let mut resolved_code_action = None; |
| 861 | + if code_action.edit.is_none() || code_action.command.is_none() { |
| 862 | + if let Some(future) = language_server.resolve_code_action(code_action.clone()) { |
| 863 | + if let Ok(response) = helix_lsp::block_on(future) { |
| 864 | + if let Ok(code_action) = serde_json::from_value::<CodeAction>(response) { |
| 865 | + resolved_code_action = Some(code_action); |
777 | 866 | } |
778 | 867 | } |
779 | 868 | } |
780 | | - }); |
781 | | - picker.move_down(); // pre-select the first item |
782 | | - |
783 | | - let popup = Popup::new("code-action", picker).with_scrollbar(false); |
| 869 | + } |
| 870 | + let resolved_code_action = resolved_code_action.as_ref().unwrap_or(code_action); |
784 | 871 |
|
785 | | - compositor.replace_or_push("code-action", popup); |
786 | | - }; |
| 872 | + if let Some(ref workspace_edit) = resolved_code_action.edit { |
| 873 | + let _ = editor.apply_workspace_edit(offset_encoding, workspace_edit); |
| 874 | + } |
787 | 875 |
|
788 | | - Ok(Callback::EditorCompositor(Box::new(call))) |
789 | | - }); |
| 876 | + // if code action provides both edit and command first the edit |
| 877 | + // should be applied and then the command |
| 878 | + if let Some(command) = &code_action.command { |
| 879 | + execute_lsp_command(editor, action.language_server_id, command.clone()); |
| 880 | + } |
| 881 | + } |
| 882 | + } |
790 | 883 | } |
791 | 884 |
|
792 | 885 | pub fn execute_lsp_command( |
|
0 commit comments