use crate::syntax::{AstNode, List, ListKind, SyntaxKind, SyntaxNode, SyntaxToken};
use tower_lsp_server::ls_types::{Range, TextEdit};
use super::super::conversions::offset_to_position;
pub fn find_list_at_position(tree: &SyntaxNode, offset: usize) -> Option<SyntaxNode> {
let text_size = rowan::TextSize::from(offset as u32);
let token = tree.token_at_offset(text_size).right_biased()?;
token
.parent_ancestors()
.find(|node| node.kind() == SyntaxKind::LIST)
}
pub fn detect_list_type(list_node: &SyntaxNode) -> Option<ListKind> {
let list = List::cast(list_node.clone())?;
list.kind()
}
pub fn convert_to_loose(list_node: &SyntaxNode, text: &str) -> Vec<TextEdit> {
let list = match List::cast(list_node.clone()) {
Some(l) => l,
None => return vec![],
};
if list.is_loose() {
return vec![];
}
let mut edits = Vec::new();
let items: Vec<_> = list.items().collect();
for (idx, item) in items.iter().enumerate() {
if idx == items.len() - 1 {
continue;
}
let item_end = item.syntax().text_range().end().into();
let position = offset_to_position(text, item_end);
edits.push(TextEdit {
range: Range {
start: position,
end: position,
},
new_text: "\n".to_string(),
});
}
edits
}
pub fn convert_to_compact(list_node: &SyntaxNode, text: &str) -> Vec<TextEdit> {
let list = match List::cast(list_node.clone()) {
Some(l) => l,
None => return vec![],
};
if list.is_compact() {
return vec![];
}
let mut edits = Vec::new();
let children: Vec<_> = list_node.children_with_tokens().collect();
let mut prev_was_item = false;
for (idx, child) in children.iter().enumerate() {
if let Some(node) = child.as_node() {
if node.kind() == SyntaxKind::LIST_ITEM {
prev_was_item = true;
} else if node.kind() == SyntaxKind::BLANK_LINE && prev_was_item {
let has_next_item = children[idx + 1..]
.iter()
.find(|c| {
c.as_node()
.map(|n| n.kind() != SyntaxKind::BLANK_LINE)
.unwrap_or(false)
})
.and_then(|c| c.as_node())
.map(|n| n.kind() == SyntaxKind::LIST_ITEM)
.unwrap_or(false);
if has_next_item {
let start = offset_to_position(text, node.text_range().start().into());
let end = offset_to_position(text, node.text_range().end().into());
edits.push(TextEdit {
range: Range { start, end },
new_text: String::new(),
});
}
}
} else if let Some(token) = child.as_token()
&& token.kind() == SyntaxKind::BLANK_LINE
&& prev_was_item
{
let has_next_item = children[idx + 1..]
.iter()
.find(|c| {
if let Some(n) = c.as_node() {
n.kind() != SyntaxKind::BLANK_LINE
} else if let Some(t) = c.as_token() {
t.kind() != SyntaxKind::BLANK_LINE
} else {
false
}
})
.and_then(|c| c.as_node())
.map(|n| n.kind() == SyntaxKind::LIST_ITEM)
.unwrap_or(false);
if has_next_item {
let start = offset_to_position(text, token.text_range().start().into());
let end = offset_to_position(text, token.text_range().end().into());
edits.push(TextEdit {
range: Range { start, end },
new_text: String::new(),
});
}
}
}
edits
}
pub fn convert_to_ordered(list_node: &SyntaxNode, text: &str) -> Vec<TextEdit> {
if detect_list_type(list_node) != Some(ListKind::Bullet) {
return vec![];
}
let Some(list) = List::cast(list_node.clone()) else {
return vec![];
};
list.items()
.enumerate()
.filter_map(|(idx, item)| {
let marker = first_item_marker_token(item.syntax())?;
let start = offset_to_position(text, marker.text_range().start().into());
let end = offset_to_position(text, marker.text_range().end().into());
Some(TextEdit {
range: Range { start, end },
new_text: format!("{}.", idx + 1),
})
})
.collect()
}
pub fn convert_to_bullet(list_node: &SyntaxNode, text: &str) -> Vec<TextEdit> {
if !matches!(
detect_list_type(list_node),
Some(ListKind::Ordered | ListKind::Task)
) {
return vec![];
}
let Some(list) = List::cast(list_node.clone()) else {
return vec![];
};
list.items()
.flat_map(|item| {
let mut edits = Vec::new();
let Some(marker) = first_item_marker_token(item.syntax()) else {
return edits;
};
let start = offset_to_position(text, marker.text_range().start().into());
let end = offset_to_position(text, marker.text_range().end().into());
edits.push(TextEdit {
range: Range { start, end },
new_text: "-".to_string(),
});
if let Some(checkbox) = task_checkbox_token(item.syntax()) {
let checkbox_start = offset_to_position(text, checkbox.text_range().start().into());
let checkbox_end = offset_to_position(text, checkbox.text_range().end().into());
edits.push(TextEdit {
range: Range {
start: checkbox_start,
end: checkbox_end,
},
new_text: String::new(),
});
}
edits
})
.collect()
}
pub fn convert_to_task(list_node: &SyntaxNode, text: &str) -> Vec<TextEdit> {
let list_type = detect_list_type(list_node);
if !matches!(list_type, Some(ListKind::Bullet | ListKind::Ordered)) {
return vec![];
}
let Some(list) = List::cast(list_node.clone()) else {
return vec![];
};
list.items()
.filter_map(|item| {
let marker = first_item_marker_token(item.syntax())?;
let insert_at = offset_to_position(text, marker.text_range().end().into());
Some(TextEdit {
range: Range {
start: insert_at,
end: insert_at,
},
new_text: " [ ]".to_string(),
})
})
.collect()
}
pub fn convert_task_to_ordered(list_node: &SyntaxNode, text: &str) -> Vec<TextEdit> {
if detect_list_type(list_node) != Some(ListKind::Task) {
return vec![];
}
let Some(list) = List::cast(list_node.clone()) else {
return vec![];
};
list.items()
.enumerate()
.flat_map(|(idx, item)| {
let mut edits = Vec::new();
let Some(marker) = first_item_marker_token(item.syntax()) else {
return edits;
};
let marker_start = offset_to_position(text, marker.text_range().start().into());
let marker_end = offset_to_position(text, marker.text_range().end().into());
edits.push(TextEdit {
range: Range {
start: marker_start,
end: marker_end,
},
new_text: format!("{}.", idx + 1),
});
if let Some(checkbox) = task_checkbox_token(item.syntax()) {
let checkbox_start = offset_to_position(text, checkbox.text_range().start().into());
let checkbox_end = offset_to_position(text, checkbox.text_range().end().into());
edits.push(TextEdit {
range: Range {
start: checkbox_start,
end: checkbox_end,
},
new_text: String::new(),
});
}
edits
})
.collect()
}
fn first_item_marker_token(item_node: &SyntaxNode) -> Option<SyntaxToken> {
item_node
.children_with_tokens()
.find_map(|elem| elem.as_token().cloned())
.filter(|token| token.kind() == SyntaxKind::LIST_MARKER)
}
fn task_checkbox_token(item_node: &SyntaxNode) -> Option<SyntaxToken> {
item_node.children_with_tokens().find_map(|elem| {
elem.as_token()
.filter(|token| token.kind() == SyntaxKind::TASK_CHECKBOX)
.cloned()
})
}
#[cfg(test)]
mod tests {
use super::*;
use crate::parse;
#[test]
fn find_list_at_position_finds_innermost() {
let input = "- Outer\n - Inner\n - Inner2\n- Outer2\n";
let tree = parse(input, None);
let offset = input.find("Inner").unwrap();
let list = find_list_at_position(&tree, offset).expect("Should find list");
let wrapped = List::cast(list).expect("Should cast to List");
assert_eq!(wrapped.items().count(), 2);
}
#[test]
fn convert_compact_to_loose() {
let input = "- First\n- Second\n- Third\n";
let tree = parse(input, None);
let list_node = tree
.descendants()
.find(|n| n.kind() == SyntaxKind::LIST)
.expect("Should find list");
let edits = convert_to_loose(&list_node, input);
assert_eq!(edits.len(), 2);
assert_eq!(edits[0].new_text, "\n");
assert_eq!(edits[0].range.start.line, 1);
assert_eq!(edits[1].new_text, "\n");
assert_eq!(edits[1].range.start.line, 2);
}
#[test]
fn convert_loose_to_compact() {
let input = "- First\n\n- Second\n\n- Third\n";
let tree = parse(input, None);
let list_node = tree
.descendants()
.find(|n| n.kind() == SyntaxKind::LIST)
.expect("Should find list");
let edits = convert_to_compact(&list_node, input);
assert_eq!(edits.len(), 2);
for edit in &edits {
assert_eq!(edit.new_text, "");
assert!(edit.range.start != edit.range.end);
}
}
#[test]
fn already_loose_returns_empty() {
let input = "- First\n\n- Second\n";
let tree = parse(input, None);
let list_node = tree
.descendants()
.find(|n| n.kind() == SyntaxKind::LIST)
.expect("Should find list");
let edits = convert_to_loose(&list_node, input);
assert_eq!(edits.len(), 0, "Should return no edits for already loose");
}
#[test]
fn already_compact_returns_empty() {
let input = "- First\n- Second\n";
let tree = parse(input, None);
let list_node = tree
.descendants()
.find(|n| n.kind() == SyntaxKind::LIST)
.expect("Should find list");
let edits = convert_to_compact(&list_node, input);
assert_eq!(edits.len(), 0, "Should return no edits for already compact");
}
#[test]
fn detect_list_type_for_bullet_and_ordered() {
let bullet_tree = parse("- First\n- Second\n", None);
let bullet_list = bullet_tree
.descendants()
.find(|n| n.kind() == SyntaxKind::LIST)
.expect("Should find bullet list");
assert_eq!(detect_list_type(&bullet_list), Some(ListKind::Bullet));
let ordered_tree = parse("1. First\n2. Second\n", None);
let ordered_list = ordered_tree
.descendants()
.find(|n| n.kind() == SyntaxKind::LIST)
.expect("Should find ordered list");
assert_eq!(detect_list_type(&ordered_list), Some(ListKind::Ordered));
}
#[test]
fn detect_list_type_for_task() {
let task_tree = parse("- [ ] First\n- [x] Second\n", None);
let task_list = task_tree
.descendants()
.find(|n| n.kind() == SyntaxKind::LIST)
.expect("Should find task list");
assert_eq!(detect_list_type(&task_list), Some(ListKind::Task));
}
#[test]
fn convert_bullet_to_ordered() {
let input = "- First\n- Second\n- Third\n";
let tree = parse(input, None);
let list_node = tree
.descendants()
.find(|n| n.kind() == SyntaxKind::LIST)
.expect("Should find list");
let edits = convert_to_ordered(&list_node, input);
assert_eq!(edits.len(), 3);
assert_eq!(edits[0].new_text, "1.");
assert_eq!(edits[1].new_text, "2.");
assert_eq!(edits[2].new_text, "3.");
}
#[test]
fn convert_ordered_to_bullet() {
let input = "1. First\n2. Second\n3. Third\n";
let tree = parse(input, None);
let list_node = tree
.descendants()
.find(|n| n.kind() == SyntaxKind::LIST)
.expect("Should find list");
let edits = convert_to_bullet(&list_node, input);
assert_eq!(edits.len(), 3);
assert!(edits.iter().all(|edit| edit.new_text == "-"));
}
#[test]
fn convert_bullet_to_task() {
let input = "- First\n- Second\n";
let tree = parse(input, None);
let list_node = tree
.descendants()
.find(|n| n.kind() == SyntaxKind::LIST)
.expect("Should find list");
let edits = convert_to_task(&list_node, input);
assert_eq!(edits.len(), 2);
assert!(edits.iter().all(|edit| edit.new_text == " [ ]"));
}
#[test]
fn convert_ordered_to_task() {
let input = "1. First\n2. Second\n";
let tree = parse(input, None);
let list_node = tree
.descendants()
.find(|n| n.kind() == SyntaxKind::LIST)
.expect("Should find list");
let edits = convert_to_task(&list_node, input);
assert_eq!(edits.len(), 2);
assert!(edits.iter().all(|edit| edit.new_text == " [ ]"));
}
#[test]
fn convert_task_to_bullet_removes_checkboxes() {
let input = "- [ ] First\n- [x] Second\n";
let tree = parse(input, None);
let list_node = tree
.descendants()
.find(|n| n.kind() == SyntaxKind::LIST)
.expect("Should find list");
let edits = convert_to_bullet(&list_node, input);
assert_eq!(edits.len(), 4, "marker + checkbox edit per item");
assert_eq!(edits[0].new_text, "-");
assert_eq!(edits[1].new_text, "");
assert_eq!(edits[2].new_text, "-");
assert_eq!(edits[3].new_text, "");
}
#[test]
fn convert_task_to_ordered_removes_checkboxes() {
let input = "- [ ] First\n- [x] Second\n";
let tree = parse(input, None);
let list_node = tree
.descendants()
.find(|n| n.kind() == SyntaxKind::LIST)
.expect("Should find list");
let edits = convert_task_to_ordered(&list_node, input);
assert_eq!(edits.len(), 4, "marker + checkbox edit per item");
assert_eq!(edits[0].new_text, "1.");
assert_eq!(edits[1].new_text, "");
assert_eq!(edits[2].new_text, "2.");
assert_eq!(edits[3].new_text, "");
}
}