merman_editor_core/
code_actions.rs1use crate::types::{Position, Range};
2use merman_analysis::{DiagnosticFix, DiagnosticFixEdit};
3
4#[derive(Debug, Clone, PartialEq, Eq)]
5pub struct EditorCodeAction {
6 pub title: String,
7 pub edits: Vec<EditorCodeActionEdit>,
8 pub is_preferred: bool,
9}
10
11#[derive(Debug, Clone, PartialEq, Eq)]
12pub struct EditorCodeActionEdit {
13 pub range: Range,
14 pub new_text: String,
15}
16
17pub fn code_actions_from_fixes<'a>(
18 fixes: impl IntoIterator<Item = &'a DiagnosticFix>,
19) -> Vec<EditorCodeAction> {
20 fixes.into_iter().filter_map(code_action_from_fix).collect()
21}
22
23pub fn code_action_from_fix(fix: &DiagnosticFix) -> Option<EditorCodeAction> {
24 let mut edits = fix
25 .edits
26 .iter()
27 .map(code_action_edit_from_fix_edit)
28 .collect::<Vec<_>>();
29 if edits.is_empty() {
30 return None;
31 }
32
33 edits.sort_by_key(|edit| range_sort_key(edit.range));
34 if has_overlapping_edits(&edits) {
35 return None;
36 }
37
38 Some(EditorCodeAction {
39 title: fix.title.clone(),
40 edits,
41 is_preferred: fix.is_preferred,
42 })
43}
44
45fn code_action_edit_from_fix_edit(edit: &DiagnosticFixEdit) -> EditorCodeActionEdit {
46 EditorCodeActionEdit {
47 range: Range::new(
48 Position::new(
49 edit.span.lsp_range.start.line,
50 edit.span.lsp_range.start.character,
51 ),
52 Position::new(
53 edit.span.lsp_range.end.line,
54 edit.span.lsp_range.end.character,
55 ),
56 ),
57 new_text: edit.replacement.clone(),
58 }
59}
60
61fn has_overlapping_edits(edits: &[EditorCodeActionEdit]) -> bool {
62 edits.windows(2).any(|window| {
63 let [left, right] = window else {
64 return false;
65 };
66 position_key(left.range.end) > position_key(right.range.start)
67 })
68}
69
70fn range_sort_key(range: Range) -> (usize, usize, usize, usize) {
71 (
72 range.start.line,
73 range.start.character,
74 range.end.line,
75 range.end.character,
76 )
77}
78
79fn position_key(position: Position) -> (usize, usize) {
80 (position.line, position.character)
81}