Files
qode/src/tree.rs
T
2026-10-31 01:06:21 +03:00

683 lines
25 KiB
Rust

//! Абстрактное дерево для блоков панелей.
//!
//! Редактор ничего не знает о файловой системе: он хранит узлы дерева с
//! идентификатором, подписью, иконкой и флагом «раскрываемый», рисует их и
//! сообщает Lua о раскрытии и кликах. Какой именно смысл у узлов — файлы,
//! задачи, настройки — решает сценарий, который наполняет дерево через
//! [`TreeApi`](crate::api::TreeApi).
//!
//! Дерево хранится плоско: у каждого узла есть родитель и список детей.
//! [`TreeState::rows`] разворачивает видимые узлы в отсортированный по
//! подписи список, который и рисует вид.
use std::cell::RefCell;
use std::collections::HashMap;
use std::rc::Rc;
use mlua::Function;
use gpui_kit::{App, FocusHandle, ScrollHandle};
/// Общая ссылка на состояние блока дерева.
pub type TreeHandle = Rc<RefCell<TreeState>>;
/// Создаёт пустое дерево и общую ссылку на него.
pub fn new_tree() -> TreeHandle {
Rc::new(RefCell::new(TreeState::new()))
}
/// Один узел, переданный из Lua.
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct TreeNode {
/// Условный идентификатор: путь файла или произвольный ключ.
pub id: String,
/// Текст, который видит пользователь.
pub label: String,
/// Имя иконки Lucide; `None` — иконка по умолчанию.
pub icon: Option<String>,
/// Раскрываемая ли нода (папка или иной контейнер).
pub expandable: bool,
/// Раскрыта ли нода сейчас.
pub expanded: bool,
}
/// Узел с сохранёнными связями между элементами дерева.
struct NodeRecord {
node: TreeNode,
parent: Option<String>,
children: Vec<String>,
/// В узел уже добавляли дочерние элементы хотя бы один раз.
loaded: bool,
}
/// Строка, которую вид рисует в текущем кадре.
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct TreeRow {
pub id: String,
pub label: String,
pub icon: Option<String>,
/// Уровень вложенности: 0 — корень.
pub depth: usize,
/// Нода раскрывается.
pub expandable: bool,
/// Нода раскрыта прямо сейчас.
pub expanded: bool,
}
/// Что должен сделать вид после клика по узлу.
pub enum TreeAction {
/// Раскрыть или свернуть контейнер.
Toggle {
id: String,
expanded: bool,
/// Были ли в узел уже загружены дочерние элементы до этого клика.
loaded: bool,
callback: Option<Function>,
},
/// Клик по конечной ноде.
Click {
id: String,
callback: Option<Function>,
},
}
/// Что вернула обработка клавиши дерева.
pub enum TreeKeyResult {
/// Клавиша не относится к навигации по дереву.
Ignored,
/// Клавиша обработана (выделение изменилось или осталось на краю).
Handled,
/// Нужно вызвать обработчик раскрытия или клика.
Action(TreeAction),
}
/// Состояние абстрактного дерева.
#[derive(Default)]
pub struct TreeState {
nodes: HashMap<String, NodeRecord>,
roots: Vec<String>,
toggle_callback: Option<Function>,
click_callback: Option<Function>,
/// Выбранная строка; сохраняется, пока панель закрыта.
selected: Option<String>,
/// Фокус создаётся лениво: состояние дерева может появиться до первого
/// кадра, когда `App` ещё недоступен.
focus_handle: Option<FocusHandle>,
/// Следующий кадр должен передать фокус этому дереву.
focus_requested: bool,
/// Прокрутка списка: выбранная строка всегда прокручивается в видимую часть.
scroll: ScrollHandle,
}
impl TreeState {
/// Создаёт пустое дерево.
pub fn new() -> Self {
Self::default()
}
/// Обработчик раскрытия/сворачивания: `(expanded, id, loaded)`.
pub fn set_toggle_callback(&mut self, callback: Option<Function>) {
self.toggle_callback = callback;
}
/// Обработчик клика по конечной ноде: `(id)`.
pub fn set_click_callback(&mut self, callback: Option<Function>) {
self.click_callback = callback;
}
/// Идентификатор выбранной строки.
pub fn selected(&self) -> Option<&str> {
self.selected.as_deref()
}
/// Раскрывает всех закрытых родителей `id`, выбирает узел и просит вид
/// передать ему фокус в следующем кадре.
///
/// Возвращает `false`, если узла с таким `id` нет.
pub fn focus(&mut self, id: &str) -> bool {
if !self.nodes.contains_key(id) {
return false;
}
self.expand_ancestors(id);
self.focus_requested = true;
self.select_and_scroll(id);
true
}
/// Обработчик прокрутки списка дерева.
pub fn scroll_handle(&self) -> ScrollHandle {
self.scroll.clone()
}
/// Фокус-обработчик дерева; создаётся при первом кадре.
pub fn focus_handle(&mut self, cx: &App) -> FocusHandle {
self.focus_handle
.get_or_insert_with(|| cx.focus_handle())
.clone()
}
/// Забирает отложенный запрос фокуса.
pub fn take_focus_request(&mut self) -> bool {
std::mem::take(&mut self.focus_requested)
}
/// Обрабатывает клавишу навигации по дереву.
pub fn handle_key(&mut self, key: &str) -> TreeKeyResult {
match key.trim().to_lowercase().as_str() {
"up" | "arrowup" => {
self.move_selection(-1);
TreeKeyResult::Handled
}
"down" | "arrowdown" => {
self.move_selection(1);
TreeKeyResult::Handled
}
"enter" | "return" | "kp_enter" => match self.activate_selected() {
Some(action) => TreeKeyResult::Action(action),
None => TreeKeyResult::Ignored,
},
"left" | "arrowleft" => self.collapse_or_select_parent(),
"right" | "arrowright" => self.expand_or_select_child(),
_ => TreeKeyResult::Ignored,
}
}
/// Добавляет узлы к `parent`; пустой `parent` — корень дерева.
///
/// Существующие узлы не удаляются и не дублируются: повторная передача
/// того же `id` обновляет описание узла. После вызова контейнер считается
/// загруженным, поэтому при следующем раскрытии в обработчик придёт
/// `loaded = true`.
///
/// # Errors
/// Возвращает текст ошибки для некорректного `id` (пустой идентификатор),
/// неизвестного родителя или попытки сделать узел потомком самого себя.
pub fn append(&mut self, parent: &str, nodes: Vec<TreeNode>) -> Result<(), String> {
if !parent.is_empty() && !self.nodes.contains_key(parent) {
return Err(format!("родительская нода {parent:?} не найдена"));
}
for node in &nodes {
if node.id.is_empty() {
return Err("идентификатор ноды не может быть пустым".to_owned());
}
if node.id == parent || self.would_cycle(&node.id, parent) {
return Err(format!(
"нода {:?} не может быть потомком самой себя",
node.id
));
}
}
for node in nodes {
let requested_parent = (!parent.is_empty()).then(|| parent.to_owned());
let id = node.id.clone();
if self.nodes.contains_key(&id) {
let old_parent = self.nodes.get(&id).and_then(|record| record.parent.clone());
// Повторный append обновляет описание узла, но не создаёт копию.
if let Some(record) = self.nodes.get_mut(&id) {
record.node = node;
}
if old_parent != requested_parent {
self.detach(&id);
self.attach(&requested_parent, &id);
}
} else {
self.nodes.insert(
id.clone(),
NodeRecord {
node,
parent: requested_parent.clone(),
children: Vec::new(),
loaded: false,
},
);
self.attach(&requested_parent, &id);
}
}
if let Some(record) = self.nodes.get_mut(parent) {
record.loaded = true;
}
Ok(())
}
/// Удаляет все узлы и сбрасывает выбор.
///
/// Обработчики раскрытия и клика сохраняются: сценарий может наполнять
/// дерево заново, например при обновлении списка диагностики.
pub fn clear(&mut self) {
self.nodes.clear();
self.roots.clear();
self.selected = None;
}
/// Удаляет узел и все его дочерние узлы рекурсивно.
///
/// Возвращает `false`, если узла с таким `id` нет.
pub fn remove(&mut self, id: &str) -> bool {
if !self.nodes.contains_key(id) {
return false;
}
self.detach(id);
let mut stack = vec![id.to_owned()];
while let Some(current) = stack.pop() {
if let Some(record) = self.nodes.remove(&current) {
stack.extend(record.children);
}
}
true
}
/// Видимые строки дерева в порядке отрисовки.
///
/// Дети каждого уровня сортируются по отображаемому тексту без учёта
/// регистра; дети свёрнутых узлов не показываются.
pub fn rows(&self) -> Vec<TreeRow> {
let mut rows = Vec::new();
for id in self.sorted(&self.roots) {
self.push_rows(&id, 0, &mut rows);
}
rows
}
/// Обрабатывает клик по узлу и возвращает действие для вида.
pub fn activate(&mut self, id: &str) -> Option<TreeAction> {
let toggle_callback = self.toggle_callback.clone();
let click_callback = self.click_callback.clone();
// Раскрытие или открытие файла может изменить документы и заставить
// редактор забрать фокус; дерево возвращает его себе в следующем кадре.
self.focus_requested = true;
let record = self.nodes.get_mut(id)?;
if record.node.expandable {
let loaded = record.loaded;
record.node.expanded = !record.node.expanded;
let expanded = record.node.expanded;
Some(TreeAction::Toggle {
id: id.to_owned(),
expanded,
loaded,
callback: toggle_callback,
})
} else {
Some(TreeAction::Click {
id: id.to_owned(),
callback: click_callback,
})
}
}
/// Дочерние `id`, отсортированные по подписи: сначала раскрываемые узлы
/// (папки), затем конечные; внутри группы — по алфавиту без учёта регистра.
fn sorted(&self, ids: &[String]) -> Vec<String> {
let mut entries = ids
.iter()
.filter_map(|id| {
self.nodes.get(id).map(|record| {
(
record.node.expandable,
record.node.label.to_lowercase(),
id.clone(),
)
})
})
.collect::<Vec<_>>();
entries.sort_by(|a, b| {
b.0.cmp(&a.0)
.then_with(|| a.1.cmp(&b.1))
.then_with(|| a.2.cmp(&b.2))
});
entries.into_iter().map(|(_, _, id)| id).collect()
}
/// Делает строку выбранной и прокручивает её в видимую часть.
fn select_and_scroll(&mut self, id: &str) {
self.selected = Some(id.to_owned());
if let Some(index) = self.rows().iter().position(|row| row.id == id) {
self.scroll.scroll_to_item(index);
}
}
/// Сдвигает выборку по видимым строкам.
fn move_selection(&mut self, delta: isize) {
let rows = self.rows();
if rows.is_empty() {
self.selected = None;
return;
}
let current = self
.selected
.as_deref()
.and_then(|id| rows.iter().position(|row| row.id == id));
let next = match current {
Some(ix) if delta < 0 => ix.saturating_sub(1),
Some(ix) => (ix + 1).min(rows.len() - 1),
None if delta < 0 => rows.len() - 1,
None => 0,
};
let id = rows[next].id.clone();
self.select_and_scroll(&id);
}
/// Enter: раскрывает папку или вызывает обработчик клика конечной ноды.
fn activate_selected(&mut self) -> Option<TreeAction> {
let id = self.selected.clone()?;
self.activate(&id)
}
/// Стрелка влево: сворачивает папку, иначе выбирает родителя.
fn collapse_or_select_parent(&mut self) -> TreeKeyResult {
let Some(id) = self.selected.clone() else {
return TreeKeyResult::Ignored;
};
let expanded = self
.nodes
.get(&id)
.is_some_and(|record| record.node.expandable && record.node.expanded);
if expanded {
return match self.activate(&id) {
Some(action) => TreeKeyResult::Action(action),
None => TreeKeyResult::Handled,
};
}
if let Some(parent) = self.nodes.get(&id).and_then(|record| record.parent.clone()) {
self.select_and_scroll(&parent);
}
TreeKeyResult::Handled
}
/// Стрелка вправо: раскрывает папку, иначе выбирает первого ребёнка.
fn expand_or_select_child(&mut self) -> TreeKeyResult {
let Some(id) = self.selected.clone() else {
return TreeKeyResult::Ignored;
};
let Some(record) = self.nodes.get(&id) else {
return TreeKeyResult::Ignored;
};
let expandable = record.node.expandable;
let expanded = record.node.expanded;
let first_child = expanded
.then(|| self.sorted(&record.children).first().cloned())
.flatten();
if expandable && !expanded {
return match self.activate(&id) {
Some(action) => TreeKeyResult::Action(action),
None => TreeKeyResult::Handled,
};
}
if let Some(child) = first_child {
self.select_and_scroll(&child);
}
TreeKeyResult::Handled
}
/// Раскрывает всех родителей узла, не трогая его собственное состояние.
fn expand_ancestors(&mut self, id: &str) {
let mut parents = Vec::new();
let mut current = self.nodes.get(id).and_then(|record| record.parent.clone());
while let Some(parent) = current {
current = self
.nodes
.get(&parent)
.and_then(|record| record.parent.clone());
parents.push(parent);
}
for parent in parents {
if let Some(record) = self.nodes.get_mut(&parent) {
record.node.expanded = true;
}
}
}
/// Добавляет `id` и его раскрытых потомков в плоский список.
fn push_rows(&self, id: &str, depth: usize, rows: &mut Vec<TreeRow>) {
let Some(record) = self.nodes.get(id) else {
return;
};
rows.push(TreeRow {
id: record.node.id.clone(),
label: record.node.label.clone(),
icon: record.node.icon.clone(),
depth,
expandable: record.node.expandable,
expanded: record.node.expanded,
});
if record.node.expanded {
for child in self.sorted(&record.children) {
self.push_rows(&child, depth + 1, rows);
}
}
}
/// Проверяет, окажется ли `parent` внутри поддерева `node_id`.
fn would_cycle(&self, node_id: &str, parent: &str) -> bool {
let mut current = parent;
while !current.is_empty() {
if current == node_id {
return true;
}
match self
.nodes
.get(current)
.and_then(|record| record.parent.as_deref())
{
Some(next) => current = next,
None => break,
}
}
false
}
/// Присоединяет узел к родителю или корню.
fn attach(&mut self, parent: &Option<String>, id: &str) {
match parent {
Some(parent) => {
if let Some(record) = self.nodes.get_mut(parent) {
record.children.push(id.to_owned());
}
}
None => self.roots.push(id.to_owned()),
}
}
/// Отсоединяет узел от родителя или корня.
fn detach(&mut self, id: &str) {
let Some(parent) = self.nodes.get(id).and_then(|record| record.parent.clone()) else {
self.roots.retain(|root| root != id);
return;
};
if let Some(record) = self.nodes.get_mut(&parent) {
record.children.retain(|child| child != id);
}
}
}
#[cfg(test)]
mod tests {
use super::*;
fn node(id: &str, label: &str, expandable: bool) -> TreeNode {
TreeNode {
id: id.to_owned(),
label: label.to_owned(),
icon: None,
expandable,
expanded: false,
}
}
#[test]
fn rows_are_sorted_by_label_and_follow_expansion() {
let mut tree = TreeState::new();
tree.append(
"",
vec![
node("b", "banana", false),
node("folder", "Alpha", true),
node("a", "apple", false),
],
)
.unwrap();
tree.append(
"folder",
vec![node("z", "zebra", false), node("c", "cat", false)],
)
.unwrap();
// Свёрнутая папка не показывает детей.
let rows = tree.rows();
assert_eq!(
rows.iter()
.map(|row| row.label.as_str())
.collect::<Vec<_>>(),
vec!["Alpha", "apple", "banana"]
);
assert!(matches!(
tree.activate("folder"),
Some(TreeAction::Toggle {
expanded: true,
loaded: true,
..
})
));
let rows = tree.rows();
assert_eq!(
rows.iter()
.map(|row| (row.label.as_str(), row.depth))
.collect::<Vec<_>>(),
vec![
("Alpha", 0),
("cat", 1),
("zebra", 1),
("apple", 0),
("banana", 0)
]
);
}
#[test]
fn append_does_not_erase_and_marks_the_parent_loaded() {
let mut tree = TreeState::new();
tree.append("", vec![node("src", "src", true)]).unwrap();
assert!(matches!(
tree.activate("src"),
Some(TreeAction::Toggle {
expanded: true,
loaded: false,
..
})
));
// Сворачиваем, чтобы проверить флаг загрузки после append.
tree.activate("src");
tree.append("src", vec![node("main.rs", "main.rs", false)])
.unwrap();
assert!(matches!(
tree.activate("src"),
Some(TreeAction::Toggle {
expanded: true,
loaded: true,
..
})
));
assert_eq!(tree.rows().len(), 2);
}
#[test]
fn remove_deletes_the_subtree() {
let mut tree = TreeState::new();
tree.append(
"",
vec![node("src", "src", true), node("readme", "README.md", false)],
)
.unwrap();
tree.append("src", vec![node("ui", "ui", true)]).unwrap();
tree.append("ui", vec![node("tree", "tree.rs", false)])
.unwrap();
assert!(tree.remove("src"));
let rows = tree.rows();
assert_eq!(
rows.iter()
.map(|row| row.label.as_str())
.collect::<Vec<_>>(),
vec!["README.md"]
);
assert!(!tree.remove("src"));
}
#[test]
fn focus_expands_ancestors_and_selects_the_node() {
let mut tree = TreeState::new();
tree.append("", vec![node("src", "src", true)]).unwrap();
tree.append("src", vec![node("ui", "ui", true)]).unwrap();
tree.append("ui", vec![node("tree", "tree.rs", false)])
.unwrap();
// Все родители свёрнуты — видна только корневая папка.
assert_eq!(tree.rows().len(), 1);
assert!(!tree.focus("missing"));
assert!(tree.focus("tree"));
assert_eq!(tree.selected(), Some("tree"));
assert!(tree.take_focus_request());
assert_eq!(
tree.rows()
.iter()
.map(|row| row.label.as_str())
.collect::<Vec<_>>(),
vec!["src", "ui", "tree.rs"]
);
}
#[test]
fn keyboard_moves_selection_and_activates_nodes() {
let mut tree = TreeState::new();
tree.append(
"",
vec![node("src", "src", true), node("readme", "README.md", false)],
)
.unwrap();
// Папки идут раньше файлов, поэтому первая строка — src.
assert!(matches!(tree.handle_key("down"), TreeKeyResult::Handled));
assert_eq!(tree.selected(), Some("src"));
assert!(matches!(tree.handle_key("down"), TreeKeyResult::Handled));
assert_eq!(tree.selected(), Some("readme"));
assert!(matches!(
tree.handle_key("enter"),
TreeKeyResult::Action(TreeAction::Click { .. })
));
assert!(matches!(tree.handle_key("up"), TreeKeyResult::Handled));
assert_eq!(tree.selected(), Some("src"));
assert!(matches!(
tree.handle_key("right"),
TreeKeyResult::Action(TreeAction::Toggle { expanded: true, .. })
));
assert!(matches!(
tree.handle_key("left"),
TreeKeyResult::Action(TreeAction::Toggle {
expanded: false,
..
})
));
}
#[test]
fn invalid_parents_and_cycles_are_rejected() {
let mut tree = TreeState::new();
assert!(tree.append("missing", vec![node("a", "a", false)]).is_err());
tree.append("", vec![node("a", "a", true)]).unwrap();
tree.append("a", vec![node("b", "b", true)]).unwrap();
assert!(tree.append("b", vec![node("a", "a", true)]).is_err());
}
}