use serde_json::Value;
use tower_lsp::jsonrpc::Result;
use tower_lsp::lsp_types::*;
use super::formatting::build_code_actions;
use crate::HarnLsp;
pub(crate) const APPLY_REPAIR_COMMAND: &str = "harn.applyRepair";
impl HarnLsp {
pub(super) async fn handle_execute_command(
&self,
params: ExecuteCommandParams,
) -> Result<Option<Value>> {
if params.command != APPLY_REPAIR_COMMAND {
return Ok(None);
}
let Some((uri, repair_id)) = parse_apply_repair_args(¶ms.arguments) else {
return Ok(None);
};
let (source, diagnostics, lint_diags, type_diags) = {
let docs = self.documents.lock().unwrap();
let Some(state) = docs.get(&uri) else {
return Ok(None);
};
(
state.source.clone(),
state.diagnostics.clone(),
state.lint_diagnostics.clone(),
state.type_diagnostics.clone(),
)
};
let edit = resolve_repair_edit(
&uri,
&source,
&diagnostics,
&lint_diags,
&type_diags,
&repair_id,
);
Ok(Some(match edit {
Some(edit) => serde_json::to_value(edit).unwrap_or(Value::Null),
None => Value::Null,
}))
}
}
fn parse_apply_repair_args(arguments: &[Value]) -> Option<(Url, String)> {
let arg = arguments.first()?;
let uri = Url::parse(arg.get("uri")?.as_str()?).ok()?;
let repair_id = arg.get("repair_id")?.as_str()?.to_string();
Some((uri, repair_id))
}
fn resolve_repair_edit(
uri: &Url,
source: &str,
diagnostics: &[Diagnostic],
lint_diags: &[harn_lint::LintDiagnostic],
type_diags: &[harn_parser::TypeDiagnostic],
repair_id: &str,
) -> Option<WorkspaceEdit> {
let context = CodeActionContext {
diagnostics: diagnostics.to_vec(),
only: None,
trigger_kind: None,
};
let actions = build_code_actions(uri, source, lint_diags, type_diags, &context);
actions.into_iter().find_map(|action| match action {
CodeActionOrCommand::CodeAction(action) => {
let matches = action
.data
.as_ref()
.and_then(|data| data.get("repair_id"))
.and_then(|value| value.as_str())
== Some(repair_id);
matches.then_some(action.edit).flatten()
}
CodeActionOrCommand::Command(_) => None,
})
}
#[cfg(test)]
mod tests {
use super::*;
use crate::document::DocumentState;
use serde_json::json;
fn doc_url() -> Url {
Url::parse("file:///repair.harn").unwrap()
}
fn make_immutable_repair_id(state: &DocumentState) -> String {
state
.lint_diagnostics
.iter()
.find(|ld| ld.rule == "mutable-never-reassigned")
.and_then(|ld| ld.repair())
.map(|repair| repair.id.as_str().to_string())
.expect("var-never-reassigned source should publish a make-mutable repair")
}
#[test]
fn resolve_repair_edit_returns_fix_for_known_repair_id() {
let source = "fn main() {\n var x = 1\n print(x)\n}\n";
let state = DocumentState::new(source.to_string());
let repair_id = make_immutable_repair_id(&state);
let edit = resolve_repair_edit(
&doc_url(),
&state.source,
&state.diagnostics,
&state.lint_diagnostics,
&state.type_diagnostics,
&repair_id,
)
.expect("known repair_id should resolve to a workspace edit");
let changes = edit.changes.expect("edit should carry per-file changes");
let text_edits = changes.get(&doc_url()).expect("edit targets the document");
assert_eq!(text_edits.len(), 1);
assert_eq!(text_edits[0].new_text, "let");
}
#[test]
fn resolve_repair_edit_is_none_for_unknown_repair_id() {
let source = "fn main() {\n var x = 1\n print(x)\n}\n";
let state = DocumentState::new(source.to_string());
let edit = resolve_repair_edit(
&doc_url(),
&state.source,
&state.diagnostics,
&state.lint_diagnostics,
&state.type_diagnostics,
"no/such-repair",
);
assert!(edit.is_none());
}
#[test]
fn parse_apply_repair_args_reads_uri_and_repair_id() {
let args =
vec![json!({ "uri": "file:///repair.harn", "repair_id": "bindings/make-immutable" })];
let (uri, repair_id) = parse_apply_repair_args(&args).expect("well-formed args parse");
assert_eq!(uri, doc_url());
assert_eq!(repair_id, "bindings/make-immutable");
}
#[test]
fn parse_apply_repair_args_rejects_missing_fields() {
assert!(parse_apply_repair_args(&[]).is_none());
assert!(parse_apply_repair_args(&[json!({ "uri": "file:///x.harn" })]).is_none());
assert!(parse_apply_repair_args(&[json!({ "repair_id": "x" })]).is_none());
}
}