diff options
-rw-r--r-- | crates/ra_ide/src/lib.rs | 39 | ||||
-rw-r--r-- | crates/rust-analyzer/src/config.rs | 7 | ||||
-rw-r--r-- | crates/rust-analyzer/src/diagnostics/snapshots/rust_analyzer__diagnostics__to_proto__tests__snap_multi_line_fix.snap | 1 | ||||
-rw-r--r-- | crates/rust-analyzer/src/diagnostics/snapshots/rust_analyzer__diagnostics__to_proto__tests__snap_rustc_unused_variable.snap | 1 | ||||
-rw-r--r-- | crates/rust-analyzer/src/diagnostics/to_proto.rs | 1 | ||||
-rw-r--r-- | crates/rust-analyzer/src/lsp_ext.rs | 18 | ||||
-rw-r--r-- | crates/rust-analyzer/src/main_loop.rs | 1 | ||||
-rw-r--r-- | crates/rust-analyzer/src/main_loop/handlers.rs | 77 | ||||
-rw-r--r-- | crates/rust-analyzer/src/to_proto.rs | 26 | ||||
-rw-r--r-- | docs/dev/lsp-extensions.md | 24 | ||||
-rw-r--r-- | editors/code/src/client.ts | 74 | ||||
-rw-r--r-- | editors/code/src/commands.ts | 19 | ||||
-rw-r--r-- | editors/code/src/lsp_ext.ts | 6 | ||||
-rw-r--r-- | editors/code/src/main.ts | 1 |
14 files changed, 210 insertions, 85 deletions
diff --git a/crates/ra_ide/src/lib.rs b/crates/ra_ide/src/lib.rs index 12d5716e8..34c2d75fe 100644 --- a/crates/ra_ide/src/lib.rs +++ b/crates/ra_ide/src/lib.rs | |||
@@ -77,7 +77,7 @@ pub use crate::{ | |||
77 | }; | 77 | }; |
78 | 78 | ||
79 | pub use hir::Documentation; | 79 | pub use hir::Documentation; |
80 | pub use ra_assists::{AssistConfig, AssistId}; | 80 | pub use ra_assists::{Assist, AssistConfig, AssistId, ResolvedAssist}; |
81 | pub use ra_db::{ | 81 | pub use ra_db::{ |
82 | Canceled, CrateGraph, CrateId, Edition, FileId, FilePosition, FileRange, SourceRootId, | 82 | Canceled, CrateGraph, CrateId, Edition, FileId, FilePosition, FileRange, SourceRootId, |
83 | }; | 83 | }; |
@@ -142,14 +142,6 @@ pub struct AnalysisHost { | |||
142 | db: RootDatabase, | 142 | db: RootDatabase, |
143 | } | 143 | } |
144 | 144 | ||
145 | #[derive(Debug)] | ||
146 | pub struct Assist { | ||
147 | pub id: AssistId, | ||
148 | pub label: String, | ||
149 | pub group_label: Option<String>, | ||
150 | pub source_change: SourceChange, | ||
151 | } | ||
152 | |||
153 | impl AnalysisHost { | 145 | impl AnalysisHost { |
154 | pub fn new(lru_capacity: Option<usize>) -> AnalysisHost { | 146 | pub fn new(lru_capacity: Option<usize>) -> AnalysisHost { |
155 | AnalysisHost { db: RootDatabase::new(lru_capacity) } | 147 | AnalysisHost { db: RootDatabase::new(lru_capacity) } |
@@ -470,20 +462,23 @@ impl Analysis { | |||
470 | self.with_db(|db| completion::completions(db, config, position).map(Into::into)) | 462 | self.with_db(|db| completion::completions(db, config, position).map(Into::into)) |
471 | } | 463 | } |
472 | 464 | ||
473 | /// Computes assists (aka code actions aka intentions) for the given | 465 | /// Computes resolved assists with source changes for the given position. |
466 | pub fn resolved_assists( | ||
467 | &self, | ||
468 | config: &AssistConfig, | ||
469 | frange: FileRange, | ||
470 | ) -> Cancelable<Vec<ResolvedAssist>> { | ||
471 | self.with_db(|db| ra_assists::Assist::resolved(db, config, frange)) | ||
472 | } | ||
473 | |||
474 | /// Computes unresolved assists (aka code actions aka intentions) for the given | ||
474 | /// position. | 475 | /// position. |
475 | pub fn assists(&self, config: &AssistConfig, frange: FileRange) -> Cancelable<Vec<Assist>> { | 476 | pub fn unresolved_assists( |
476 | self.with_db(|db| { | 477 | &self, |
477 | ra_assists::Assist::resolved(db, config, frange) | 478 | config: &AssistConfig, |
478 | .into_iter() | 479 | frange: FileRange, |
479 | .map(|assist| Assist { | 480 | ) -> Cancelable<Vec<Assist>> { |
480 | id: assist.assist.id, | 481 | self.with_db(|db| Assist::unresolved(db, config, frange)) |
481 | label: assist.assist.label, | ||
482 | group_label: assist.assist.group.map(|it| it.0), | ||
483 | source_change: assist.source_change, | ||
484 | }) | ||
485 | .collect() | ||
486 | }) | ||
487 | } | 482 | } |
488 | 483 | ||
489 | /// Computes the set of diagnostics for the given file. | 484 | /// Computes the set of diagnostics for the given file. |
diff --git a/crates/rust-analyzer/src/config.rs b/crates/rust-analyzer/src/config.rs index 0e5dc56fd..23168c3ae 100644 --- a/crates/rust-analyzer/src/config.rs +++ b/crates/rust-analyzer/src/config.rs | |||
@@ -123,6 +123,7 @@ pub struct ClientCapsConfig { | |||
123 | pub code_action_literals: bool, | 123 | pub code_action_literals: bool, |
124 | pub work_done_progress: bool, | 124 | pub work_done_progress: bool, |
125 | pub code_action_group: bool, | 125 | pub code_action_group: bool, |
126 | pub resolve_code_action: bool, | ||
126 | } | 127 | } |
127 | 128 | ||
128 | impl Default for Config { | 129 | impl Default for Config { |
@@ -336,7 +337,11 @@ impl Config { | |||
336 | 337 | ||
337 | let code_action_group = | 338 | let code_action_group = |
338 | experimental.get("codeActionGroup").and_then(|it| it.as_bool()) == Some(true); | 339 | experimental.get("codeActionGroup").and_then(|it| it.as_bool()) == Some(true); |
339 | self.client_caps.code_action_group = code_action_group | 340 | self.client_caps.code_action_group = code_action_group; |
341 | |||
342 | let resolve_code_action = | ||
343 | experimental.get("resolveCodeAction").and_then(|it| it.as_bool()) == Some(true); | ||
344 | self.client_caps.resolve_code_action = resolve_code_action; | ||
340 | } | 345 | } |
341 | } | 346 | } |
342 | } | 347 | } |
diff --git a/crates/rust-analyzer/src/diagnostics/snapshots/rust_analyzer__diagnostics__to_proto__tests__snap_multi_line_fix.snap b/crates/rust-analyzer/src/diagnostics/snapshots/rust_analyzer__diagnostics__to_proto__tests__snap_multi_line_fix.snap index c40cfdcdc..272057b47 100644 --- a/crates/rust-analyzer/src/diagnostics/snapshots/rust_analyzer__diagnostics__to_proto__tests__snap_multi_line_fix.snap +++ b/crates/rust-analyzer/src/diagnostics/snapshots/rust_analyzer__diagnostics__to_proto__tests__snap_multi_line_fix.snap | |||
@@ -65,6 +65,7 @@ expression: diag | |||
65 | fixes: [ | 65 | fixes: [ |
66 | CodeAction { | 66 | CodeAction { |
67 | title: "return the expression directly", | 67 | title: "return the expression directly", |
68 | id: None, | ||
68 | group: None, | 69 | group: None, |
69 | kind: Some( | 70 | kind: Some( |
70 | "quickfix", | 71 | "quickfix", |
diff --git a/crates/rust-analyzer/src/diagnostics/snapshots/rust_analyzer__diagnostics__to_proto__tests__snap_rustc_unused_variable.snap b/crates/rust-analyzer/src/diagnostics/snapshots/rust_analyzer__diagnostics__to_proto__tests__snap_rustc_unused_variable.snap index 33b516e26..f0273315e 100644 --- a/crates/rust-analyzer/src/diagnostics/snapshots/rust_analyzer__diagnostics__to_proto__tests__snap_rustc_unused_variable.snap +++ b/crates/rust-analyzer/src/diagnostics/snapshots/rust_analyzer__diagnostics__to_proto__tests__snap_rustc_unused_variable.snap | |||
@@ -50,6 +50,7 @@ expression: diag | |||
50 | fixes: [ | 50 | fixes: [ |
51 | CodeAction { | 51 | CodeAction { |
52 | title: "consider prefixing with an underscore", | 52 | title: "consider prefixing with an underscore", |
53 | id: None, | ||
53 | group: None, | 54 | group: None, |
54 | kind: Some( | 55 | kind: Some( |
55 | "quickfix", | 56 | "quickfix", |
diff --git a/crates/rust-analyzer/src/diagnostics/to_proto.rs b/crates/rust-analyzer/src/diagnostics/to_proto.rs index 6a6e7b457..04e286780 100644 --- a/crates/rust-analyzer/src/diagnostics/to_proto.rs +++ b/crates/rust-analyzer/src/diagnostics/to_proto.rs | |||
@@ -145,6 +145,7 @@ fn map_rust_child_diagnostic( | |||
145 | } else { | 145 | } else { |
146 | MappedRustChildDiagnostic::SuggestedFix(lsp_ext::CodeAction { | 146 | MappedRustChildDiagnostic::SuggestedFix(lsp_ext::CodeAction { |
147 | title: rd.message.clone(), | 147 | title: rd.message.clone(), |
148 | id: None, | ||
148 | group: None, | 149 | group: None, |
149 | kind: Some("quickfix".to_string()), | 150 | kind: Some("quickfix".to_string()), |
150 | edit: Some(lsp_ext::SnippetWorkspaceEdit { | 151 | edit: Some(lsp_ext::SnippetWorkspaceEdit { |
diff --git a/crates/rust-analyzer/src/lsp_ext.rs b/crates/rust-analyzer/src/lsp_ext.rs index ec24ce5e0..3b957534d 100644 --- a/crates/rust-analyzer/src/lsp_ext.rs +++ b/crates/rust-analyzer/src/lsp_ext.rs | |||
@@ -97,6 +97,22 @@ pub struct JoinLinesParams { | |||
97 | pub ranges: Vec<Range>, | 97 | pub ranges: Vec<Range>, |
98 | } | 98 | } |
99 | 99 | ||
100 | pub enum ResolveCodeActionRequest {} | ||
101 | |||
102 | impl Request for ResolveCodeActionRequest { | ||
103 | type Params = ResolveCodeActionParams; | ||
104 | type Result = Option<SnippetWorkspaceEdit>; | ||
105 | const METHOD: &'static str = "experimental/resolveCodeAction"; | ||
106 | } | ||
107 | |||
108 | /// Params for the ResolveCodeActionRequest | ||
109 | #[derive(Debug, Eq, PartialEq, Clone, Deserialize, Serialize)] | ||
110 | #[serde(rename_all = "camelCase")] | ||
111 | pub struct ResolveCodeActionParams { | ||
112 | pub code_action_params: lsp_types::CodeActionParams, | ||
113 | pub id: String, | ||
114 | } | ||
115 | |||
100 | pub enum OnEnter {} | 116 | pub enum OnEnter {} |
101 | 117 | ||
102 | impl Request for OnEnter { | 118 | impl Request for OnEnter { |
@@ -202,6 +218,8 @@ impl Request for CodeActionRequest { | |||
202 | pub struct CodeAction { | 218 | pub struct CodeAction { |
203 | pub title: String, | 219 | pub title: String, |
204 | #[serde(skip_serializing_if = "Option::is_none")] | 220 | #[serde(skip_serializing_if = "Option::is_none")] |
221 | pub id: Option<String>, | ||
222 | #[serde(skip_serializing_if = "Option::is_none")] | ||
205 | pub group: Option<String>, | 223 | pub group: Option<String>, |
206 | #[serde(skip_serializing_if = "Option::is_none")] | 224 | #[serde(skip_serializing_if = "Option::is_none")] |
207 | pub kind: Option<String>, | 225 | pub kind: Option<String>, |
diff --git a/crates/rust-analyzer/src/main_loop.rs b/crates/rust-analyzer/src/main_loop.rs index 1f8f6b978..e60337b8e 100644 --- a/crates/rust-analyzer/src/main_loop.rs +++ b/crates/rust-analyzer/src/main_loop.rs | |||
@@ -509,6 +509,7 @@ fn on_request( | |||
509 | .on::<lsp_ext::Runnables>(handlers::handle_runnables)? | 509 | .on::<lsp_ext::Runnables>(handlers::handle_runnables)? |
510 | .on::<lsp_ext::InlayHints>(handlers::handle_inlay_hints)? | 510 | .on::<lsp_ext::InlayHints>(handlers::handle_inlay_hints)? |
511 | .on::<lsp_ext::CodeActionRequest>(handlers::handle_code_action)? | 511 | .on::<lsp_ext::CodeActionRequest>(handlers::handle_code_action)? |
512 | .on::<lsp_ext::ResolveCodeActionRequest>(handlers::handle_resolve_code_action)? | ||
512 | .on::<lsp_types::request::OnTypeFormatting>(handlers::handle_on_type_formatting)? | 513 | .on::<lsp_types::request::OnTypeFormatting>(handlers::handle_on_type_formatting)? |
513 | .on::<lsp_types::request::DocumentSymbolRequest>(handlers::handle_document_symbol)? | 514 | .on::<lsp_types::request::DocumentSymbolRequest>(handlers::handle_document_symbol)? |
514 | .on::<lsp_types::request::WorkspaceSymbol>(handlers::handle_workspace_symbol)? | 515 | .on::<lsp_types::request::WorkspaceSymbol>(handlers::handle_workspace_symbol)? |
diff --git a/crates/rust-analyzer/src/main_loop/handlers.rs b/crates/rust-analyzer/src/main_loop/handlers.rs index c2a5bf4d6..6acf80c58 100644 --- a/crates/rust-analyzer/src/main_loop/handlers.rs +++ b/crates/rust-analyzer/src/main_loop/handlers.rs | |||
@@ -25,7 +25,7 @@ use ra_project_model::TargetKind; | |||
25 | use ra_syntax::{AstNode, SyntaxKind, TextRange, TextSize}; | 25 | use ra_syntax::{AstNode, SyntaxKind, TextRange, TextSize}; |
26 | use serde::{Deserialize, Serialize}; | 26 | use serde::{Deserialize, Serialize}; |
27 | use serde_json::to_value; | 27 | use serde_json::to_value; |
28 | use stdx::format_to; | 28 | use stdx::{format_to, split1}; |
29 | 29 | ||
30 | use crate::{ | 30 | use crate::{ |
31 | cargo_target_spec::CargoTargetSpec, | 31 | cargo_target_spec::CargoTargetSpec, |
@@ -701,37 +701,27 @@ pub fn handle_formatting( | |||
701 | }])) | 701 | }])) |
702 | } | 702 | } |
703 | 703 | ||
704 | pub fn handle_code_action( | 704 | fn handle_fixes( |
705 | snap: GlobalStateSnapshot, | 705 | snap: &GlobalStateSnapshot, |
706 | params: lsp_types::CodeActionParams, | 706 | params: &lsp_types::CodeActionParams, |
707 | ) -> Result<Option<Vec<lsp_ext::CodeAction>>> { | 707 | res: &mut Vec<lsp_ext::CodeAction>, |
708 | let _p = profile("handle_code_action"); | 708 | ) -> Result<()> { |
709 | // We intentionally don't support command-based actions, as those either | ||
710 | // requires custom client-code anyway, or requires server-initiated edits. | ||
711 | // Server initiated edits break causality, so we avoid those as well. | ||
712 | if !snap.config.client_caps.code_action_literals { | ||
713 | return Ok(None); | ||
714 | } | ||
715 | |||
716 | let file_id = from_proto::file_id(&snap, ¶ms.text_document.uri)?; | 709 | let file_id = from_proto::file_id(&snap, ¶ms.text_document.uri)?; |
717 | let line_index = snap.analysis().file_line_index(file_id)?; | 710 | let line_index = snap.analysis().file_line_index(file_id)?; |
718 | let range = from_proto::text_range(&line_index, params.range); | 711 | let range = from_proto::text_range(&line_index, params.range); |
719 | let frange = FileRange { file_id, range }; | ||
720 | |||
721 | let diagnostics = snap.analysis().diagnostics(file_id)?; | 712 | let diagnostics = snap.analysis().diagnostics(file_id)?; |
722 | let mut res: Vec<lsp_ext::CodeAction> = Vec::new(); | ||
723 | 713 | ||
724 | let fixes_from_diagnostics = diagnostics | 714 | let fixes_from_diagnostics = diagnostics |
725 | .into_iter() | 715 | .into_iter() |
726 | .filter_map(|d| Some((d.range, d.fix?))) | 716 | .filter_map(|d| Some((d.range, d.fix?))) |
727 | .filter(|(diag_range, _fix)| diag_range.intersect(range).is_some()) | 717 | .filter(|(diag_range, _fix)| diag_range.intersect(range).is_some()) |
728 | .map(|(_range, fix)| fix); | 718 | .map(|(_range, fix)| fix); |
729 | |||
730 | for fix in fixes_from_diagnostics { | 719 | for fix in fixes_from_diagnostics { |
731 | let title = fix.label; | 720 | let title = fix.label; |
732 | let edit = to_proto::snippet_workspace_edit(&snap, fix.source_change)?; | 721 | let edit = to_proto::snippet_workspace_edit(&snap, fix.source_change)?; |
733 | let action = lsp_ext::CodeAction { | 722 | let action = lsp_ext::CodeAction { |
734 | title, | 723 | title, |
724 | id: None, | ||
735 | group: None, | 725 | group: None, |
736 | kind: Some(lsp_types::code_action_kind::QUICKFIX.into()), | 726 | kind: Some(lsp_types::code_action_kind::QUICKFIX.into()), |
737 | edit: Some(edit), | 727 | edit: Some(edit), |
@@ -747,13 +737,62 @@ pub fn handle_code_action( | |||
747 | } | 737 | } |
748 | res.push(fix.action.clone()); | 738 | res.push(fix.action.clone()); |
749 | } | 739 | } |
740 | Ok(()) | ||
741 | } | ||
742 | |||
743 | pub fn handle_code_action( | ||
744 | snap: GlobalStateSnapshot, | ||
745 | params: lsp_types::CodeActionParams, | ||
746 | ) -> Result<Option<Vec<lsp_ext::CodeAction>>> { | ||
747 | let _p = profile("handle_code_action"); | ||
748 | // We intentionally don't support command-based actions, as those either | ||
749 | // requires custom client-code anyway, or requires server-initiated edits. | ||
750 | // Server initiated edits break causality, so we avoid those as well. | ||
751 | if !snap.config.client_caps.code_action_literals { | ||
752 | return Ok(None); | ||
753 | } | ||
754 | |||
755 | let file_id = from_proto::file_id(&snap, ¶ms.text_document.uri)?; | ||
756 | let line_index = snap.analysis().file_line_index(file_id)?; | ||
757 | let range = from_proto::text_range(&line_index, params.range); | ||
758 | let frange = FileRange { file_id, range }; | ||
759 | let mut res: Vec<lsp_ext::CodeAction> = Vec::new(); | ||
750 | 760 | ||
751 | for assist in snap.analysis().assists(&snap.config.assist, frange)?.into_iter() { | 761 | handle_fixes(&snap, ¶ms, &mut res)?; |
752 | res.push(to_proto::code_action(&snap, assist)?.into()); | 762 | |
763 | if snap.config.client_caps.resolve_code_action { | ||
764 | for (index, assist) in | ||
765 | snap.analysis().unresolved_assists(&snap.config.assist, frange)?.into_iter().enumerate() | ||
766 | { | ||
767 | res.push(to_proto::unresolved_code_action(&snap, assist, index)?); | ||
768 | } | ||
769 | } else { | ||
770 | for assist in snap.analysis().resolved_assists(&snap.config.assist, frange)?.into_iter() { | ||
771 | res.push(to_proto::resolved_code_action(&snap, assist)?); | ||
772 | } | ||
753 | } | 773 | } |
774 | |||
754 | Ok(Some(res)) | 775 | Ok(Some(res)) |
755 | } | 776 | } |
756 | 777 | ||
778 | pub fn handle_resolve_code_action( | ||
779 | snap: GlobalStateSnapshot, | ||
780 | params: lsp_ext::ResolveCodeActionParams, | ||
781 | ) -> Result<Option<lsp_ext::SnippetWorkspaceEdit>> { | ||
782 | let _p = profile("handle_resolve_code_action"); | ||
783 | let file_id = from_proto::file_id(&snap, ¶ms.code_action_params.text_document.uri)?; | ||
784 | let line_index = snap.analysis().file_line_index(file_id)?; | ||
785 | let range = from_proto::text_range(&line_index, params.code_action_params.range); | ||
786 | let frange = FileRange { file_id, range }; | ||
787 | |||
788 | let assists = snap.analysis().resolved_assists(&snap.config.assist, frange)?; | ||
789 | let (id_string, index) = split1(¶ms.id, ':').unwrap(); | ||
790 | let index = index.parse::<usize>().unwrap(); | ||
791 | let assist = &assists[index]; | ||
792 | assert!(assist.assist.id.0 == id_string); | ||
793 | Ok(to_proto::resolved_code_action(&snap, assist.clone())?.edit) | ||
794 | } | ||
795 | |||
757 | pub fn handle_code_lens( | 796 | pub fn handle_code_lens( |
758 | snap: GlobalStateSnapshot, | 797 | snap: GlobalStateSnapshot, |
759 | params: lsp_types::CodeLensParams, | 798 | params: lsp_types::CodeLensParams, |
diff --git a/crates/rust-analyzer/src/to_proto.rs b/crates/rust-analyzer/src/to_proto.rs index 0915a7fcb..1da4d80ec 100644 --- a/crates/rust-analyzer/src/to_proto.rs +++ b/crates/rust-analyzer/src/to_proto.rs | |||
@@ -3,8 +3,8 @@ use ra_db::{FileId, FileRange}; | |||
3 | use ra_ide::{ | 3 | use ra_ide::{ |
4 | Assist, CompletionItem, CompletionItemKind, Documentation, FileSystemEdit, Fold, FoldKind, | 4 | Assist, CompletionItem, CompletionItemKind, Documentation, FileSystemEdit, Fold, FoldKind, |
5 | FunctionSignature, Highlight, HighlightModifier, HighlightTag, HighlightedRange, Indel, | 5 | FunctionSignature, Highlight, HighlightModifier, HighlightTag, HighlightedRange, Indel, |
6 | InlayHint, InlayKind, InsertTextFormat, LineIndex, NavigationTarget, ReferenceAccess, Runnable, | 6 | InlayHint, InlayKind, InsertTextFormat, LineIndex, NavigationTarget, ReferenceAccess, |
7 | RunnableKind, Severity, SourceChange, SourceFileEdit, TextEdit, | 7 | ResolvedAssist, Runnable, RunnableKind, Severity, SourceChange, SourceFileEdit, TextEdit, |
8 | }; | 8 | }; |
9 | use ra_syntax::{SyntaxKind, TextRange, TextSize}; | 9 | use ra_syntax::{SyntaxKind, TextRange, TextSize}; |
10 | use ra_vfs::LineEndings; | 10 | use ra_vfs::LineEndings; |
@@ -623,20 +623,36 @@ fn main() <fold>{ | |||
623 | } | 623 | } |
624 | } | 624 | } |
625 | 625 | ||
626 | pub(crate) fn code_action( | 626 | pub(crate) fn unresolved_code_action( |
627 | snap: &GlobalStateSnapshot, | 627 | snap: &GlobalStateSnapshot, |
628 | assist: Assist, | 628 | assist: Assist, |
629 | index: usize, | ||
629 | ) -> Result<lsp_ext::CodeAction> { | 630 | ) -> Result<lsp_ext::CodeAction> { |
630 | let res = lsp_ext::CodeAction { | 631 | let res = lsp_ext::CodeAction { |
631 | title: assist.label, | 632 | title: assist.label, |
632 | group: if snap.config.client_caps.code_action_group { assist.group_label } else { None }, | 633 | id: Some(format!("{}:{}", assist.id.0.to_owned(), index.to_string())), |
634 | group: assist.group.filter(|_| snap.config.client_caps.code_action_group).map(|gr| gr.0), | ||
633 | kind: Some(String::new()), | 635 | kind: Some(String::new()), |
634 | edit: Some(snippet_workspace_edit(snap, assist.source_change)?), | 636 | edit: None, |
635 | command: None, | 637 | command: None, |
636 | }; | 638 | }; |
637 | Ok(res) | 639 | Ok(res) |
638 | } | 640 | } |
639 | 641 | ||
642 | pub(crate) fn resolved_code_action( | ||
643 | snap: &GlobalStateSnapshot, | ||
644 | assist: ResolvedAssist, | ||
645 | ) -> Result<lsp_ext::CodeAction> { | ||
646 | let change = assist.source_change; | ||
647 | unresolved_code_action(snap, assist.assist, 0).and_then(|it| { | ||
648 | Ok(lsp_ext::CodeAction { | ||
649 | id: None, | ||
650 | edit: Some(snippet_workspace_edit(snap, change)?), | ||
651 | ..it | ||
652 | }) | ||
653 | }) | ||
654 | } | ||
655 | |||
640 | pub(crate) fn runnable( | 656 | pub(crate) fn runnable( |
641 | snap: &GlobalStateSnapshot, | 657 | snap: &GlobalStateSnapshot, |
642 | file_id: FileId, | 658 | file_id: FileId, |
diff --git a/docs/dev/lsp-extensions.md b/docs/dev/lsp-extensions.md index 647cf6107..7f7940d0b 100644 --- a/docs/dev/lsp-extensions.md +++ b/docs/dev/lsp-extensions.md | |||
@@ -97,6 +97,30 @@ Invoking code action at this position will yield two code actions for importing | |||
97 | * Is a fixed two-level structure enough? | 97 | * Is a fixed two-level structure enough? |
98 | * Should we devise a general way to encode custom interaction protocols for GUI refactorings? | 98 | * Should we devise a general way to encode custom interaction protocols for GUI refactorings? |
99 | 99 | ||
100 | ## Lazy assists with `ResolveCodeAction` | ||
101 | |||
102 | **Issue:** https://github.com/microsoft/language-server-protocol/issues/787 | ||
103 | |||
104 | **Client Capability** `{ "resolveCodeAction": boolean }` | ||
105 | |||
106 | If this capability is set, the assists will be computed lazily. Thus `CodeAction` returned from the server will only contain `id` but not `edit` or `command` fields. The only exclusion from the rule is the diagnostic edits. | ||
107 | |||
108 | After the client got the id, it should then call `experimental/resolveCodeAction` command on the server and provide the following payload: | ||
109 | |||
110 | ```typescript | ||
111 | interface ResolveCodeActionParams { | ||
112 | id: string; | ||
113 | codeActionParams: lc.CodeActionParams; | ||
114 | } | ||
115 | ``` | ||
116 | |||
117 | As a result of the command call the client will get the respective workspace edit (`lc.WorkspaceEdit`). | ||
118 | |||
119 | ### Unresolved Questions | ||
120 | |||
121 | * Apply smarter filtering for ids? | ||
122 | * Upon `resolveCodeAction` command only call the assits which should be resolved and not all of them? | ||
123 | |||
100 | ## Parent Module | 124 | ## Parent Module |
101 | 125 | ||
102 | **Issue:** https://github.com/microsoft/language-server-protocol/issues/1002 | 126 | **Issue:** https://github.com/microsoft/language-server-protocol/issues/1002 |
diff --git a/editors/code/src/client.ts b/editors/code/src/client.ts index d64f9a3f9..40ad1e3cd 100644 --- a/editors/code/src/client.ts +++ b/editors/code/src/client.ts | |||
@@ -1,8 +1,11 @@ | |||
1 | import * as lc from 'vscode-languageclient'; | 1 | import * as lc from 'vscode-languageclient'; |
2 | import * as vscode from 'vscode'; | 2 | import * as vscode from 'vscode'; |
3 | import * as ra from '../src/lsp_ext'; | ||
4 | import * as Is from 'vscode-languageclient/lib/utils/is'; | ||
3 | 5 | ||
4 | import { CallHierarchyFeature } from 'vscode-languageclient/lib/callHierarchy.proposed'; | 6 | import { CallHierarchyFeature } from 'vscode-languageclient/lib/callHierarchy.proposed'; |
5 | import { SemanticTokensFeature, DocumentSemanticsTokensSignature } from 'vscode-languageclient/lib/semanticTokens.proposed'; | 7 | import { SemanticTokensFeature, DocumentSemanticsTokensSignature } from 'vscode-languageclient/lib/semanticTokens.proposed'; |
8 | import { assert } from './util'; | ||
6 | 9 | ||
7 | export function createClient(serverPath: string, cwd: string): lc.LanguageClient { | 10 | export function createClient(serverPath: string, cwd: string): lc.LanguageClient { |
8 | // '.' Is the fallback if no folder is open | 11 | // '.' Is the fallback if no folder is open |
@@ -32,6 +35,8 @@ export function createClient(serverPath: string, cwd: string): lc.LanguageClient | |||
32 | if (res === undefined) throw new Error('busy'); | 35 | if (res === undefined) throw new Error('busy'); |
33 | return res; | 36 | return res; |
34 | }, | 37 | }, |
38 | // Using custom handling of CodeActions where each code action is resloved lazily | ||
39 | // That's why we are not waiting for any command or edits | ||
35 | async provideCodeActions(document: vscode.TextDocument, range: vscode.Range, context: vscode.CodeActionContext, token: vscode.CancellationToken, _next: lc.ProvideCodeActionsSignature) { | 40 | async provideCodeActions(document: vscode.TextDocument, range: vscode.Range, context: vscode.CodeActionContext, token: vscode.CancellationToken, _next: lc.ProvideCodeActionsSignature) { |
36 | const params: lc.CodeActionParams = { | 41 | const params: lc.CodeActionParams = { |
37 | textDocument: client.code2ProtocolConverter.asTextDocumentIdentifier(document), | 42 | textDocument: client.code2ProtocolConverter.asTextDocumentIdentifier(document), |
@@ -43,32 +48,36 @@ export function createClient(serverPath: string, cwd: string): lc.LanguageClient | |||
43 | const result: (vscode.CodeAction | vscode.Command)[] = []; | 48 | const result: (vscode.CodeAction | vscode.Command)[] = []; |
44 | const groups = new Map<string, { index: number; items: vscode.CodeAction[] }>(); | 49 | const groups = new Map<string, { index: number; items: vscode.CodeAction[] }>(); |
45 | for (const item of values) { | 50 | for (const item of values) { |
51 | // In our case we expect to get code edits only from diagnostics | ||
46 | if (lc.CodeAction.is(item)) { | 52 | if (lc.CodeAction.is(item)) { |
53 | assert(!item.command, "We don't expect to receive commands in CodeActions"); | ||
47 | const action = client.protocol2CodeConverter.asCodeAction(item); | 54 | const action = client.protocol2CodeConverter.asCodeAction(item); |
48 | const group = actionGroup(item); | 55 | result.push(action); |
49 | if (isSnippetEdit(item) || group) { | 56 | continue; |
50 | action.command = { | 57 | } |
51 | command: "rust-analyzer.applySnippetWorkspaceEdit", | 58 | assert(isCodeActionWithoutEditsAndCommands(item), "We don't expect edits or commands here"); |
52 | title: "", | 59 | const action = new vscode.CodeAction(item.title); |
53 | arguments: [action.edit], | 60 | const group = (item as any).group; |
54 | }; | 61 | const id = (item as any).id; |
55 | action.edit = undefined; | 62 | const resolveParams: ra.ResolveCodeActionParams = { |
56 | } | 63 | id: id, |
57 | 64 | codeActionParams: params | |
58 | if (group) { | 65 | }; |
59 | let entry = groups.get(group); | 66 | action.command = { |
60 | if (!entry) { | 67 | command: "rust-analyzer.resolveCodeAction", |
61 | entry = { index: result.length, items: [] }; | 68 | title: item.title, |
62 | groups.set(group, entry); | 69 | arguments: [resolveParams], |
63 | result.push(action); | 70 | }; |
64 | } | 71 | if (group) { |
65 | entry.items.push(action); | 72 | let entry = groups.get(group); |
66 | } else { | 73 | if (!entry) { |
74 | entry = { index: result.length, items: [] }; | ||
75 | groups.set(group, entry); | ||
67 | result.push(action); | 76 | result.push(action); |
68 | } | 77 | } |
78 | entry.items.push(action); | ||
69 | } else { | 79 | } else { |
70 | const command = client.protocol2CodeConverter.asCommand(item); | 80 | result.push(action); |
71 | result.push(command); | ||
72 | } | 81 | } |
73 | } | 82 | } |
74 | for (const [group, { index, items }] of groups) { | 83 | for (const [group, { index, items }] of groups) { |
@@ -80,7 +89,7 @@ export function createClient(serverPath: string, cwd: string): lc.LanguageClient | |||
80 | command: "rust-analyzer.applyActionGroup", | 89 | command: "rust-analyzer.applyActionGroup", |
81 | title: "", | 90 | title: "", |
82 | arguments: [items.map((item) => { | 91 | arguments: [items.map((item) => { |
83 | return { label: item.title, edit: item.command!!.arguments!![0] }; | 92 | return { label: item.title, arguments: item.command!!.arguments!![0] }; |
84 | })], | 93 | })], |
85 | }; | 94 | }; |
86 | result[index] = action; | 95 | result[index] = action; |
@@ -119,24 +128,17 @@ class ExperimentalFeatures implements lc.StaticFeature { | |||
119 | const caps: any = capabilities.experimental ?? {}; | 128 | const caps: any = capabilities.experimental ?? {}; |
120 | caps.snippetTextEdit = true; | 129 | caps.snippetTextEdit = true; |
121 | caps.codeActionGroup = true; | 130 | caps.codeActionGroup = true; |
131 | caps.resolveCodeAction = true; | ||
122 | capabilities.experimental = caps; | 132 | capabilities.experimental = caps; |
123 | } | 133 | } |
124 | initialize(_capabilities: lc.ServerCapabilities<any>, _documentSelector: lc.DocumentSelector | undefined): void { | 134 | initialize(_capabilities: lc.ServerCapabilities<any>, _documentSelector: lc.DocumentSelector | undefined): void { |
125 | } | 135 | } |
126 | } | 136 | } |
127 | 137 | ||
128 | function isSnippetEdit(action: lc.CodeAction): boolean { | 138 | function isCodeActionWithoutEditsAndCommands(value: any): boolean { |
129 | const documentChanges = action.edit?.documentChanges ?? []; | 139 | const candidate: lc.CodeAction = value; |
130 | for (const edit of documentChanges) { | 140 | return candidate && Is.string(candidate.title) && |
131 | if (lc.TextDocumentEdit.is(edit)) { | 141 | (candidate.diagnostics === void 0 || Is.typedArray(candidate.diagnostics, lc.Diagnostic.is)) && |
132 | if (edit.edits.some((indel) => (indel as any).insertTextFormat === lc.InsertTextFormat.Snippet)) { | 142 | (candidate.kind === void 0 || Is.string(candidate.kind)) && |
133 | return true; | 143 | (candidate.edit === void 0 && candidate.command === void 0); |
134 | } | ||
135 | } | ||
136 | } | ||
137 | return false; | ||
138 | } | ||
139 | |||
140 | function actionGroup(action: lc.CodeAction): string | undefined { | ||
141 | return (action as any).group; | ||
142 | } | 144 | } |
diff --git a/editors/code/src/commands.ts b/editors/code/src/commands.ts index 534d2a984..3e9c3aa0e 100644 --- a/editors/code/src/commands.ts +++ b/editors/code/src/commands.ts | |||
@@ -343,10 +343,25 @@ export function showReferences(ctx: Ctx): Cmd { | |||
343 | } | 343 | } |
344 | 344 | ||
345 | export function applyActionGroup(_ctx: Ctx): Cmd { | 345 | export function applyActionGroup(_ctx: Ctx): Cmd { |
346 | return async (actions: { label: string; edit: vscode.WorkspaceEdit }[]) => { | 346 | return async (actions: { label: string; arguments: ra.ResolveCodeActionParams }[]) => { |
347 | const selectedAction = await vscode.window.showQuickPick(actions); | 347 | const selectedAction = await vscode.window.showQuickPick(actions); |
348 | if (!selectedAction) return; | 348 | if (!selectedAction) return; |
349 | await applySnippetWorkspaceEdit(selectedAction.edit); | 349 | vscode.commands.executeCommand( |
350 | 'rust-analyzer.resolveCodeAction', | ||
351 | selectedAction.arguments, | ||
352 | ); | ||
353 | }; | ||
354 | } | ||
355 | |||
356 | export function resolveCodeAction(ctx: Ctx): Cmd { | ||
357 | const client = ctx.client; | ||
358 | return async (params: ra.ResolveCodeActionParams) => { | ||
359 | const item: lc.WorkspaceEdit = await client.sendRequest(ra.resolveCodeAction, params); | ||
360 | if (!item) { | ||
361 | return; | ||
362 | } | ||
363 | const edit = client.protocol2CodeConverter.asWorkspaceEdit(item); | ||
364 | await applySnippetWorkspaceEdit(edit); | ||
350 | }; | 365 | }; |
351 | } | 366 | } |
352 | 367 | ||
diff --git a/editors/code/src/lsp_ext.ts b/editors/code/src/lsp_ext.ts index c51acfccb..9793b926c 100644 --- a/editors/code/src/lsp_ext.ts +++ b/editors/code/src/lsp_ext.ts | |||
@@ -33,6 +33,12 @@ export const matchingBrace = new lc.RequestType<MatchingBraceParams, lc.Position | |||
33 | 33 | ||
34 | export const parentModule = new lc.RequestType<lc.TextDocumentPositionParams, lc.LocationLink[], void>("experimental/parentModule"); | 34 | export const parentModule = new lc.RequestType<lc.TextDocumentPositionParams, lc.LocationLink[], void>("experimental/parentModule"); |
35 | 35 | ||
36 | export interface ResolveCodeActionParams { | ||
37 | id: string; | ||
38 | codeActionParams: lc.CodeActionParams; | ||
39 | } | ||
40 | export const resolveCodeAction = new lc.RequestType<ResolveCodeActionParams, lc.WorkspaceEdit, unknown>('experimental/resolveCodeAction'); | ||
41 | |||
36 | export interface JoinLinesParams { | 42 | export interface JoinLinesParams { |
37 | textDocument: lc.TextDocumentIdentifier; | 43 | textDocument: lc.TextDocumentIdentifier; |
38 | ranges: lc.Range[]; | 44 | ranges: lc.Range[]; |
diff --git a/editors/code/src/main.ts b/editors/code/src/main.ts index b7337621c..a92c676fa 100644 --- a/editors/code/src/main.ts +++ b/editors/code/src/main.ts | |||
@@ -98,6 +98,7 @@ export async function activate(context: vscode.ExtensionContext) { | |||
98 | ctx.registerCommand('debugSingle', commands.debugSingle); | 98 | ctx.registerCommand('debugSingle', commands.debugSingle); |
99 | ctx.registerCommand('showReferences', commands.showReferences); | 99 | ctx.registerCommand('showReferences', commands.showReferences); |
100 | ctx.registerCommand('applySnippetWorkspaceEdit', commands.applySnippetWorkspaceEditCommand); | 100 | ctx.registerCommand('applySnippetWorkspaceEdit', commands.applySnippetWorkspaceEditCommand); |
101 | ctx.registerCommand('resolveCodeAction', commands.resolveCodeAction); | ||
101 | ctx.registerCommand('applyActionGroup', commands.applyActionGroup); | 102 | ctx.registerCommand('applyActionGroup', commands.applyActionGroup); |
102 | 103 | ||
103 | ctx.pushCleanup(activateTaskProvider(workspaceFolder)); | 104 | ctx.pushCleanup(activateTaskProvider(workspaceFolder)); |