tinymist_query/code_action.rs
1use lsp_types::CodeActionContext;
2
3use crate::{SemanticRequest, analysis::CodeActionWorker, prelude::*};
4
5pub(crate) mod proto;
6pub use proto::*;
7
8/// The [`textDocument/codeAction`] request is sent from the client to the
9/// server to compute commands for a given text document and range. These
10/// commands are typically code fixes to either fix problems or to
11/// beautify/refactor code.
12///
13/// The result of a [`textDocument/codeAction`] request is an array of `Command`
14/// literals which are typically presented in the user interface.
15///
16/// [`textDocument/codeAction`]: https://microsoft.github.io/language-server-protocol/specification#textDocument_codeAction
17///
18/// To ensure that a server is useful in many clients, the commands specified in
19/// a code actions should be handled by the server and not by the client (see
20/// [`workspace/executeCommand`] and
21/// `ServerCapabilities::execute_command_provider`). If the client supports
22/// providing edits with a code action, then the mode should be used.
23///
24/// When the command is selected the server should be contacted again (via the
25/// [`workspace/executeCommand`] request) to execute the command.
26///
27/// [`workspace/executeCommand`]: https://microsoft.github.io/language-server-protocol/specification#workspace_executeCommand
28///
29/// # Compatibility
30///
31/// ## Since version 3.16.0
32///
33/// A client can offer a server to delay the computation of code action
34/// properties during a `textDocument/codeAction` request. This is useful for
35/// cases where it is expensive to compute the value of a property (for example,
36/// the `edit` property).
37///
38/// Clients signal this through the `code_action.resolve_support` client
39/// capability which lists all properties a client can resolve lazily. The
40/// server capability `code_action_provider.resolve_provider` signals that a
41/// server will offer a `codeAction/resolve` route.
42///
43/// To help servers uniquely identify a code action in the resolve request, a
44/// code action literal may optionally carry a `data` property. This is also
45/// guarded by an additional client capability `code_action.data_support`. In
46/// general, a client should offer data support if it offers resolve support.
47///
48/// It should also be noted that servers shouldn’t alter existing attributes of
49/// a code action in a `codeAction/resolve` request.
50///
51/// ## Since version 3.8.0
52///
53/// Support for [`CodeAction`] literals to enable the following scenarios:
54///
55/// * The ability to directly return a workspace edit from the code action
56/// request. This avoids having another server roundtrip to execute an actual
57/// code action. However server providers should be aware that if the code
58/// action is expensive to compute or the edits are huge it might still be
59/// beneficial if the result is simply a command and the actual edit is only
60/// computed when needed.
61///
62/// * The ability to group code actions using a kind. Clients are allowed to
63/// ignore that information. However it allows them to better group code
64/// action, for example, into corresponding menus (e.g. all refactor code
65/// actions into a refactor menu).
66#[derive(Debug, Clone)]
67pub struct CodeActionRequest {
68 /// The path of the document to request for.
69 pub path: PathBuf,
70 /// The range of the document to get code actions for.
71 pub range: LspRange,
72 /// The context of the code action request.
73 pub context: CodeActionContext,
74}
75
76impl SemanticRequest for CodeActionRequest {
77 type Response = Vec<CodeAction>;
78
79 fn request(self, ctx: &mut LocalContext) -> Option<Self::Response> {
80 log::trace!("requested code action: {self:?}");
81
82 let source = ctx.source_by_path(&self.path).ok()?;
83 let range = ctx.to_typst_range(self.range, &source)?;
84
85 let root = LinkedNode::new(source.root());
86 let mut worker = CodeActionWorker::new(ctx, source.clone());
87 worker.autofix(&root, &range, &self.context);
88 worker.scoped(&root, &range);
89
90 (!worker.actions.is_empty()).then_some(worker.actions)
91 }
92}
93
94#[cfg(test)]
95mod tests {
96 use typst::{diag::Warned, layout::PagedDocument};
97
98 use super::*;
99 use crate::{DiagWorker, tests::*};
100
101 #[test]
102 fn test() {
103 snapshot_testing("code_action", &|ctx, path| {
104 let source = ctx.source_by_path(&path).unwrap();
105
106 let request_range = find_test_range(&source);
107 let code_action_ctx = compute_code_action_context(ctx, &source, &request_range);
108 let request = CodeActionRequest {
109 path: path.clone(),
110 range: request_range,
111 context: code_action_ctx,
112 };
113
114 let result = request.request(ctx);
115
116 with_settings!({
117 description => format!("Code Action on {}", make_range_annotation(&source)),
118 }, {
119 assert_snapshot!(JsonRepr::new_redacted(result, &REDACT_LOC));
120 })
121 });
122 }
123
124 fn compute_code_action_context(
125 ctx: &mut LocalContext,
126 source: &Source,
127 request_range: &LspRange,
128 ) -> CodeActionContext {
129 let Warned {
130 output,
131 warnings: compiler_warnings,
132 } = typst::compile::<PagedDocument>(&ctx.world);
133 let compiler_errors = output.err().unwrap_or_default();
134
135 let lint_warnings = ctx.lint(source);
136 let diagnostics = DiagWorker::new(ctx)
137 .convert_all(
138 compiler_errors
139 .iter()
140 .chain(compiler_warnings.iter())
141 .chain(lint_warnings.iter()),
142 )
143 .into_values()
144 .flatten();
145
146 CodeActionContext {
147 // The filtering here matches the LSP specification and VS Code behavior;
148 // see https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#codeActionContext:
149 // `diagnostics`: An array of diagnostics known on the client side overlapping the range
150 // provided to the textDocument/codeAction request [...]
151 diagnostics: diagnostics
152 .filter(|diag| ranges_overlap(&diag.range, request_range))
153 .collect(),
154 only: None,
155 trigger_kind: None,
156 }
157 }
158
159 fn ranges_overlap(r1: &LspRange, r2: &LspRange) -> bool {
160 !(r1.end <= r2.start || r2.end <= r1.start)
161 }
162}