|
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