ts_bridge/protocol/text_document/
code_action_resolve.rs

1//! =============================================================================
2//! codeAction/resolve
3//! =============================================================================
4//!
5//! Resolves lazily-evaluated code actions, currently focusing on "fix all".
6//! When a code action stores `CodeActionData::FixAll`, we reissue tsserver’s
7//! `getCombinedCodeFix` to materialize the edits.
8
9use anyhow::{Context, Result};
10use lsp_types::{CodeAction, CodeActionKind};
11use serde_json::{Value, json};
12
13use crate::protocol::text_document::code_action::{
14    CodeActionData, FixAllData, OrganizeImportsData, organize_imports_payload,
15    workspace_edit_from_tsserver_changes,
16};
17use crate::protocol::{AdapterResult, RequestSpec};
18use crate::rpc::{Priority, Route};
19
20pub fn handle(mut action: CodeAction) -> Option<RequestSpec> {
21    let data = action.data.take()?;
22    let data: CodeActionData = serde_json::from_value(data).ok()?;
23
24    match data {
25        CodeActionData::FixAll(fix_all) => build_fix_all_request(action, fix_all),
26        CodeActionData::OrganizeImports(data) => build_organize_imports_request(action, data),
27    }
28}
29
30fn build_fix_all_request(action: CodeAction, fix_all: FixAllData) -> Option<RequestSpec> {
31    let request = json!({
32        "command": "getCombinedCodeFix",
33        "arguments": {
34            "scope": {
35                "type": "file",
36                "args": {
37                    "file": fix_all.file,
38                }
39            },
40            "fixId": fix_all.fix_id,
41        }
42    });
43
44    let context = serde_json::to_value(action).ok()?;
45
46    Some(RequestSpec {
47        route: Route::Syntax,
48        payload: request,
49        priority: Priority::Low,
50        on_response: Some(adapt_fix_all_response),
51        response_context: Some(context),
52    })
53}
54
55fn build_organize_imports_request(
56    action: CodeAction,
57    data: OrganizeImportsData,
58) -> Option<RequestSpec> {
59    let request = organize_imports_payload(&data.file);
60    let context = serde_json::to_value(action).ok()?;
61
62    Some(RequestSpec {
63        route: Route::Syntax,
64        payload: request,
65        priority: Priority::Low,
66        on_response: Some(adapt_organize_imports_response),
67        response_context: Some(context),
68    })
69}
70
71fn adapt_fix_all_response(payload: &Value, context: Option<&Value>) -> Result<AdapterResult> {
72    let mut action: CodeAction =
73        serde_json::from_value(context.cloned().context("missing code action context")?)?;
74    let body = payload
75        .get("body")
76        .context("tsserver fix-all missing body")?;
77    let combined = body
78        .get("changes")
79        .or_else(|| body.get("FileChanges"))
80        .and_then(|value| value.as_array())
81        .cloned()
82        .unwrap_or_default();
83
84    if let Some(edit) = workspace_edit_from_tsserver_changes(&combined) {
85        action.edit = Some(edit);
86    }
87
88    if action.kind.is_none() {
89        action.kind = Some(CodeActionKind::SOURCE_FIX_ALL);
90    }
91
92    Ok(AdapterResult::ready(serde_json::to_value(action)?))
93}
94
95fn adapt_organize_imports_response(
96    payload: &Value,
97    context: Option<&Value>,
98) -> Result<AdapterResult> {
99    let mut action: CodeAction =
100        serde_json::from_value(context.cloned().context("missing code action context")?)?;
101    let changes = payload
102        .get("body")
103        .and_then(|value| value.as_array())
104        .cloned()
105        .unwrap_or_default();
106
107    if let Some(edit) = workspace_edit_from_tsserver_changes(&changes) {
108        action.edit = Some(edit);
109    }
110
111    if action.kind.is_none() {
112        action.kind = Some(CodeActionKind::SOURCE_ORGANIZE_IMPORTS);
113    }
114
115    Ok(AdapterResult::ready(serde_json::to_value(action)?))
116}