use std::{path::Path, sync::Arc, time::Duration};
use cosh_sdk::lsp::{
LanguageServer, PositionEncoding,
lsp_types::{
CodeActionOrCommand, Position, WorkspaceEdit,
request::{CodeActionRequest as CodeActionReq, Request as _},
},
};
use serde_json::json;
use super::{
support,
types::{CodeActionEntry, CodeActionsInput, CodeActionsOutput},
};
pub async fn run_code_actions(
deps: &super::support::Deps<'_>,
input: &CodeActionsInput,
) -> Result<CodeActionsOutput, String> {
let path = deps.manager.root().join(&input.file_path);
let clients = support::prepare(deps, &path).await?;
if clients.is_empty() {
return Err(format!("no language server handles `{}`", path.display()));
}
let encoding = clients[0].position_encoding();
let line_idx = input.line.saturating_sub(1);
let text = std::fs::read_to_string(&path)
.map_err(|err| format!("cannot read `{}`: {err}", path.display()))?;
let line_len = text
.lines()
.nth(line_idx as usize)
.map(|l| l.len() as u32)
.unwrap_or(0);
let range = cosh_sdk::lsp::lsp_types::Range {
start: Position::new(line_idx, 0),
end: Position::new(line_idx, line_len),
};
let stored = deps.diagnostics.snapshot_for(&path);
let matching: Vec<_> = stored
.iter()
.filter(|d| d.range.start.line <= line_idx && d.range.end.line >= line_idx)
.cloned()
.collect();
let params = json!({
"textDocument": { "uri": support::uri(&path)? },
"range": range,
"context": {
"diagnostics": matching,
"reason": "implicit",
},
});
let raw = fetch_raw_actions(&clients, deps.request_timeout, ¶ms).await?;
let actions = flatten(&raw);
match input.apply_index {
None => Ok(CodeActionsOutput {
applied: false,
formatted: render_list(&actions),
actions,
}),
Some(index) => {
let raw_action = raw
.get(index)
.ok_or_else(|| format!("action index {index} out of range"))?;
let (title, workspace_edit) = match raw_action {
CodeActionOrCommand::CodeAction(action) => {
let edit = action.edit.as_ref().ok_or_else(|| {
format!(
"action `{}` carries a command, not an edit — cannot auto-apply",
action.title
)
})?;
(&action.title, edit)
}
CodeActionOrCommand::Command(_) => {
return Err("commands cannot be auto-applied".into());
}
};
apply_workspace_edit(workspace_edit, encoding)?;
for client in &clients {
let _ = client.touch_file(&path).await;
}
Ok(CodeActionsOutput {
applied: true,
actions: vec![],
formatted: format!("Applied: {title}"),
})
}
}
}
async fn fetch_raw_actions(
clients: &[Arc<LanguageServer>],
timeout: Duration,
params: &serde_json::Value,
) -> Result<Vec<CodeActionOrCommand>, String> {
let response: Option<Vec<CodeActionOrCommand>> =
support::first_answer(clients, timeout, capability, |_client| {
(CodeActionReq::METHOD.to_owned(), params.clone())
})
.await?;
Ok(response.unwrap_or_default())
}
fn capability(caps: &cosh_sdk::lsp::lsp_types::ServerCapabilities) -> bool {
caps.code_action_provider.is_some()
}
fn flatten(raw: &[CodeActionOrCommand]) -> Vec<CodeActionEntry> {
raw.iter()
.enumerate()
.filter_map(|(index, item)| match item {
CodeActionOrCommand::CodeAction(action) => {
let files = extract_edit_files(action.edit.as_ref());
Some(CodeActionEntry {
index,
title: action.title.clone(),
kind: action.kind.as_ref().map(|k| k.as_str().to_owned()),
has_edit: action.edit.is_some(),
files,
})
}
CodeActionOrCommand::Command(_) => None,
})
.collect()
}
fn extract_edit_files(edit: Option<&WorkspaceEdit>) -> Vec<String> {
let mut files = Vec::new();
if let Some(edit) = edit
&& let Some(changes) = &edit.changes
{
for uri in changes.keys() {
if let Some(path) = cosh_sdk::lsp::uri_to_path(uri) {
files.push(path.display().to_string());
}
}
}
files
}
fn apply_workspace_edit(edit: &WorkspaceEdit, encoding: PositionEncoding) -> Result<(), String> {
let per_file = support::flatten_workspace_edit(edit, encoding)?;
for (path, planned) in &per_file {
support::apply_edits(Path::new(path), planned)?;
}
Ok(())
}
fn render_list(actions: &[CodeActionEntry]) -> String {
if actions.is_empty() {
return "No code actions available.".to_owned();
}
actions
.iter()
.map(|action| {
let kind = action.kind.as_deref().unwrap_or("action");
let files_note = if action.files.is_empty() {
String::new()
} else {
format!(" [{}]", action.files.join(", "))
};
format!("[{}] ({kind}) {}{}", action.index, action.title, files_note)
})
.collect::<Vec<_>>()
.join("\n")
}