use std::collections::HashMap;
use async_lsp::lsp_types::{Position, Range, TextEdit};
use comrak::{
Arena, ComrakOptions, ExtensionOptions,
nodes::{AstNode, NodeValue},
parse_document,
};
use ropey::RopeSlice;
pub enum ListType {
Ordered, Unordered, TaskList, }
pub fn convert_to_list(
rope: RopeSlice,
range: Range,
conversion_type: ListType,
) -> Option<Vec<TextEdit>> {
let arena = Arena::new();
let options = ComrakOptions {
extension: ExtensionOptions {
tasklist: true,
strikethrough: false,
tagfilter: false,
table: false,
autolink: false,
superscript: false,
header_ids: None,
footnotes: false,
description_lists: false,
front_matter_delimiter: None,
multiline_block_quotes: false,
alerts: false,
math_dollars: false,
math_code: false,
wikilinks_title_after_pipe: false,
wikilinks_title_before_pipe: false,
underline: false,
subscript: false,
spoiler: false,
greentext: false,
image_url_rewriter: None,
link_url_rewriter: None,
},
..Default::default()
};
let root = parse_document(&arena, &rope.to_string(), &options);
if contains_list(root) {
return None;
}
let mut counters: HashMap<usize, u32> = HashMap::new(); let mut current_levels = Vec::new();
let edits: Vec<TextEdit> = rope
.lines()
.enumerate()
.filter_map(|(index, line)| {
let indent = line.chars().take_while(|c| c.is_whitespace()).count();
if line.to_string().trim().is_empty() {
current_levels.clear();
return None;
}
while !current_levels.is_empty() && *current_levels.last().unwrap() > indent {
current_levels.pop();
}
if current_levels.last() != Some(&indent) {
current_levels.push(indent);
}
let prefix = match conversion_type {
ListType::Ordered => {
let level = current_levels.len().saturating_sub(1);
let counter = counters.entry(level).or_insert(0);
*counter += 1;
for l in (level + 1).. {
if counters.remove(&l).is_none() {
break;
}
}
let prefix = (0..=level)
.map(|l| counters.get(&l).unwrap_or(&1).to_string())
.collect::<Vec<_>>()
.join(".");
format!("{prefix}. ")
}
ListType::Unordered => {
"- ".to_string()
}
ListType::TaskList => {
"- [ ] ".to_string()
}
};
let insert_pos = Position {
line: range.start.line + index as u32,
character: indent as u32,
};
Some(TextEdit {
range: Range::new(insert_pos, insert_pos),
new_text: prefix,
})
})
.collect();
(!edits.is_empty()).then_some(edits)
}
fn contains_list<'a>(root: &'a AstNode<'a>) -> bool {
let mut stack = vec![root];
while let Some(node) = stack.pop() {
match &node.data.borrow().value {
NodeValue::List(_) | NodeValue::Item(_) | NodeValue::TaskItem(_) => {
return true;
}
_ => {}
}
stack.extend(node.children());
}
false
}