merman-editor-core 0.8.0-alpha.3

Protocol-neutral editor intelligence core for Merman.
Documentation
use crate::types::{Position, Range};
use merman_analysis::{DiagnosticFix, DiagnosticFixEdit};

#[derive(Debug, Clone, PartialEq, Eq)]
pub struct EditorCodeAction {
    pub title: String,
    pub edits: Vec<EditorCodeActionEdit>,
    pub is_preferred: bool,
}

#[derive(Debug, Clone, PartialEq, Eq)]
pub struct EditorCodeActionEdit {
    pub range: Range,
    pub new_text: String,
}

pub fn code_actions_from_fixes<'a>(
    fixes: impl IntoIterator<Item = &'a DiagnosticFix>,
) -> Vec<EditorCodeAction> {
    fixes.into_iter().filter_map(code_action_from_fix).collect()
}

pub fn code_action_from_fix(fix: &DiagnosticFix) -> Option<EditorCodeAction> {
    let mut edits = fix
        .edits
        .iter()
        .map(code_action_edit_from_fix_edit)
        .collect::<Vec<_>>();
    if edits.is_empty() {
        return None;
    }

    edits.sort_by_key(|edit| range_sort_key(edit.range));
    if has_overlapping_edits(&edits) {
        return None;
    }

    Some(EditorCodeAction {
        title: fix.title.clone(),
        edits,
        is_preferred: fix.is_preferred,
    })
}

fn code_action_edit_from_fix_edit(edit: &DiagnosticFixEdit) -> EditorCodeActionEdit {
    EditorCodeActionEdit {
        range: Range::new(
            Position::new(
                edit.span.lsp_range.start.line,
                edit.span.lsp_range.start.character,
            ),
            Position::new(
                edit.span.lsp_range.end.line,
                edit.span.lsp_range.end.character,
            ),
        ),
        new_text: edit.replacement.clone(),
    }
}

fn has_overlapping_edits(edits: &[EditorCodeActionEdit]) -> bool {
    edits.windows(2).any(|window| {
        let [left, right] = window else {
            return false;
        };
        position_key(left.range.end) > position_key(right.range.start)
    })
}

fn range_sort_key(range: Range) -> (usize, usize, usize, usize) {
    (
        range.start.line,
        range.start.character,
        range.end.line,
        range.end.character,
    )
}

fn position_key(position: Position) -> (usize, usize) {
    (position.line, position.character)
}