use async_lsp::lsp_types::{Position, Range, TextEdit};
use crate::completion::yaml_lines::{indent, is_blank_or_comment, is_list_item, line_map_key};
pub use self::list_conversion::ListConversion;
mod list_conversion;
pub struct CodeActionEngine;
impl CodeActionEngine {
pub fn list_conversions(text: &str, line: u32) -> Vec<ListConversion> {
let lines = text.split('\n').collect::<Vec<&str>>();
let line_idx = (line as usize).min(lines.len().saturating_sub(1));
[
Self::flow_to_block(&lines, line_idx),
Self::block_to_flow(&lines, line_idx),
]
.into_iter()
.flatten()
.collect()
}
fn flow_to_block(lines: &[&str], line_idx: usize) -> Option<ListConversion> {
let line = *lines.get(line_idx)?;
let key = line_map_key(line)?;
let colon_idx = line.find(':')?;
let value = line[colon_idx + 1..].trim();
let inner = value.strip_prefix('[')?.strip_suffix(']')?;
let items = split_flow_items(inner);
if items.is_empty() {
return None;
}
let child_indent = " ".repeat(indent(line) + 2);
let mut new_text = line[..=colon_idx].to_string();
for item in &items {
new_text.push('\n');
new_text.push_str(&child_indent);
new_text.push_str("- ");
new_text.push_str(item);
}
Some(ListConversion {
title: format!("Convert `{key}` to a block list"),
edit: TextEdit {
range: whole_line_range(line_idx, line),
new_text,
},
})
}
fn block_to_flow(lines: &[&str], line_idx: usize) -> Option<ListConversion> {
let key_line_idx = Self::sequence_key_line(lines, line_idx)?;
let key_line = lines[key_line_idx];
let key = line_map_key(key_line)?;
let colon_idx = key_line.find(':')?;
if !key_line[colon_idx + 1..].trim().is_empty() {
return None;
}
let key_indent = indent(key_line);
let mut items = Vec::new();
let mut last_idx = key_line_idx;
for (idx, line) in lines.iter().enumerate().skip(key_line_idx + 1) {
if is_blank_or_comment(line) {
break;
}
if indent(line) < key_indent || !is_list_item(line) {
break;
}
items.push(line.trim_start().trim_start_matches('-').trim().to_string());
last_idx = idx;
}
if items.is_empty() {
return None;
}
let new_text = format!(
"{indent}{key}: [{items}]",
indent = " ".repeat(key_indent),
items = items.join(", "),
);
Some(ListConversion {
title: format!("Convert `{key}` to an inline list"),
edit: TextEdit {
range: Range {
start: Position {
line: key_line_idx as u32,
character: 0,
},
end: Position {
line: last_idx as u32,
character: lines[last_idx].chars().count() as u32,
},
},
new_text,
},
})
}
fn sequence_key_line(lines: &[&str], line_idx: usize) -> Option<usize> {
let line = *lines.get(line_idx)?;
if line_map_key(line).is_some() {
return Some(line_idx);
}
if !is_list_item(line) {
return None;
}
let item_indent = indent(line);
lines[..line_idx]
.iter()
.enumerate()
.rev()
.find_map(|(idx, candidate)| {
if is_blank_or_comment(candidate) {
return None;
}
let candidate_indent = indent(candidate);
if candidate_indent > item_indent
|| (candidate_indent == item_indent && is_list_item(candidate))
{
return None;
}
line_map_key(candidate).map(|_key| idx)
})
}
}
fn split_flow_items(inner: &str) -> Vec<&str> {
inner
.split(',')
.map(str::trim)
.filter(|item| !item.is_empty())
.collect()
}
fn whole_line_range(line_idx: usize, line: &str) -> Range {
Range {
start: Position {
line: line_idx as u32,
character: 0,
},
end: Position {
line: line_idx as u32,
character: line.chars().count() as u32,
},
}
}