squawk-server 2.50.0

LSP server for Squawk
Documentation
use anyhow::{Context, Result};
use lsp_types::{
    CodeAction, CodeActionKind, CodeActionOrCommand, CodeActionParams, CodeActionResponse, Command,
    WorkspaceEdit,
};
use rustc_hash::FxHashMap;
use squawk_ide::code_actions::code_actions;
use squawk_ide::db::line_index;

use crate::diagnostic::{AssociatedDiagnosticData, DIAGNOSTIC_NAME};
use crate::global_state::Snapshot;
use crate::lsp_utils;

pub(crate) fn handle_code_action(
    snapshot: &Snapshot,
    params: CodeActionParams,
) -> Result<Option<CodeActionResponse>> {
    let uri = params.text_document.uri;

    let mut actions: CodeActionResponse = vec![];

    let db = snapshot.db();
    let file = snapshot.file(&uri).unwrap();
    let line_index = line_index(db, file);
    let offset = lsp_utils::offset(&line_index, params.range.start).unwrap();

    let ide_actions = code_actions(db, file, offset).unwrap_or_default();

    for action in ide_actions {
        let lsp_action = lsp_utils::code_action(&line_index, uri.clone(), action);
        actions.push(CodeActionOrCommand::CodeAction(lsp_action));
    }

    for mut diagnostic in params
        .context
        .diagnostics
        .into_iter()
        .filter(|diagnostic| diagnostic.source.as_deref() == Some(DIAGNOSTIC_NAME))
    {
        let Some(rule_name) = diagnostic.code.as_ref().map(|x| match x {
            lsp_types::NumberOrString::String(s) => s.clone(),
            lsp_types::NumberOrString::Number(n) => n.to_string(),
        }) else {
            continue;
        };
        let Some(data) = diagnostic.data.take() else {
            continue;
        };

        let associated_data: AssociatedDiagnosticData =
            serde_json::from_value(data).context("deserializing diagnostic data")?;

        if let Some(ignore_line_edit) = associated_data.ignore_line_edit {
            let disable_line_action = CodeAction {
                title: format!("Disable {rule_name} for this line"),
                kind: Some(CodeActionKind::QUICKFIX),
                diagnostics: Some(vec![diagnostic.clone()]),
                edit: Some(WorkspaceEdit::new({
                    let mut changes = FxHashMap::default();
                    changes.insert(uri.clone(), vec![ignore_line_edit]);
                    changes.into_iter().collect()
                })),
                ..Default::default()
            };
            actions.push(CodeActionOrCommand::CodeAction(disable_line_action));
        }
        if let Some(ignore_file_edit) = associated_data.ignore_file_edit {
            let disable_file_action = CodeAction {
                title: format!("Disable {rule_name} for the entire file"),
                kind: Some(CodeActionKind::QUICKFIX),
                diagnostics: Some(vec![diagnostic.clone()]),
                edit: Some(WorkspaceEdit::new({
                    let mut changes = FxHashMap::default();
                    changes.insert(uri.clone(), vec![ignore_file_edit]);
                    changes.into_iter().collect()
                })),
                ..Default::default()
            };
            actions.push(CodeActionOrCommand::CodeAction(disable_file_action));
        }

        let title = format!("Show documentation for {rule_name}");
        let documentation_action = CodeAction {
            title: title.clone(),
            kind: Some(CodeActionKind::QUICKFIX),
            diagnostics: Some(vec![diagnostic.clone()]),
            command: Some(Command {
                title,
                command: "vscode.open".to_string(),
                arguments: Some(vec![serde_json::to_value(format!(
                    "https://squawkhq.com/docs/{rule_name}"
                ))?]),
            }),
            ..Default::default()
        };
        actions.push(CodeActionOrCommand::CodeAction(documentation_action));

        if !associated_data.title.is_empty() && !associated_data.edits.is_empty() {
            let fix_action = CodeAction {
                title: associated_data.title,
                kind: Some(CodeActionKind::QUICKFIX),
                diagnostics: Some(vec![diagnostic.clone()]),
                edit: Some(WorkspaceEdit::new({
                    let mut changes = FxHashMap::default();
                    changes.insert(uri.clone(), associated_data.edits);
                    changes.into_iter().collect()
                })),
                is_preferred: Some(true),
                ..Default::default()
            };
            actions.push(CodeActionOrCommand::CodeAction(fix_action));
        }
    }

    Ok(Some(actions))
}