files
This commit is contained in:
+241
@@ -18,9 +18,11 @@ use serde_json::Number;
|
||||
|
||||
use crate::constants::{BOTTOM_PANEL_HEIGHT, LEFT_PANEL_WIDTH, RIGHT_PANEL_WIDTH};
|
||||
use crate::editor::{Language, Workspace};
|
||||
use crate::icons;
|
||||
use crate::keymap::{KeyBinding, KeyMap};
|
||||
use crate::lsp::{LspConfig, LspManager, WatcherConfig, document_uri};
|
||||
use crate::panel::{PanelHandle, PanelState};
|
||||
use crate::tree::{TreeHandle, TreeNode};
|
||||
|
||||
thread_local! {
|
||||
/// Текущий экземпляр API. `thread_local`, потому что UI GPUI привязан к
|
||||
@@ -68,6 +70,22 @@ pub struct TabInfo {
|
||||
pub index: usize,
|
||||
/// Название — то же, что показано в заголовке вкладки.
|
||||
pub title: String,
|
||||
/// Имя иконки вкладки; по умолчанию — `file`.
|
||||
pub icon: String,
|
||||
}
|
||||
|
||||
/// Элемент каталога, возвращаемый `app:list_dir(path)`.
|
||||
///
|
||||
/// `id` — путь относительно текущего каталога процесса: его можно сразу
|
||||
/// положить в `id` узла дерева и передать в `app:open(id)`.
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub struct DirEntryInfo {
|
||||
/// Путь элемента относительно текущего каталога.
|
||||
pub id: String,
|
||||
/// Имя файла или каталога без пути.
|
||||
pub name: String,
|
||||
/// Является ли элемент каталогом.
|
||||
pub is_dir: bool,
|
||||
}
|
||||
|
||||
/// Глобальный объект с функциями управления приложением.
|
||||
@@ -113,6 +131,93 @@ impl PanelApi {
|
||||
}
|
||||
}
|
||||
|
||||
/// API одного блока дерева.
|
||||
///
|
||||
/// Объект возвращает `app.left_panel:add_tree()`. Редактор не знает, что
|
||||
/// означают узлы: сценарий сам решает, будет это файловое дерево проекта или
|
||||
/// что-то ещё, и передаёт готовый список узлов в [`TreeApi::append`].
|
||||
#[derive(mlua::UserData)]
|
||||
pub struct TreeApi {
|
||||
#[lua(skip)]
|
||||
state: TreeHandle,
|
||||
}
|
||||
|
||||
impl TreeApi {
|
||||
fn new(state: TreeHandle) -> Self {
|
||||
Self { state }
|
||||
}
|
||||
}
|
||||
|
||||
#[mlua::userdata_impl]
|
||||
impl TreeApi {
|
||||
/// Добавляет узлы к родительскому узлу `parent`.
|
||||
///
|
||||
/// `nodes` — массив таблиц:
|
||||
///
|
||||
/// ```lua
|
||||
/// {
|
||||
/// id = "src/main.rs", -- строковый идентификатор (путь файла)
|
||||
/// label = "main.rs", -- отображаемый текст
|
||||
/// icon = "file-code", -- имя иконки Lucide (необязательно)
|
||||
/// expandable = false, -- раскрываемая ли нода (папка)
|
||||
/// expanded = false, -- раскрыта ли она сейчас
|
||||
/// }
|
||||
/// ```
|
||||
///
|
||||
/// Пустая строка в `parent` означает корень дерева. Существующие узлы не
|
||||
/// удаляются: повторная передача того же `id` обновляет узел.
|
||||
pub fn append(&self, nodes: mlua::Table, parent: Option<String>) -> mlua::Result<()> {
|
||||
let nodes = parse_tree_nodes(&nodes)?;
|
||||
let parent = parent.unwrap_or_default();
|
||||
self.state
|
||||
.borrow_mut()
|
||||
.append(&parent, nodes)
|
||||
.map_err(mlua::Error::RuntimeError)
|
||||
}
|
||||
|
||||
/// Удаляет узел по `id` вместе со всеми дочерними узлами рекурсивно.
|
||||
///
|
||||
/// Возвращает `false`, если узла с таким идентификатором нет.
|
||||
#[lua(infallible)]
|
||||
pub fn remove(&self, id: String) -> bool {
|
||||
self.state.borrow_mut().remove(&id)
|
||||
}
|
||||
|
||||
/// Назначает обработчик раскрытия/сворачивания узла.
|
||||
///
|
||||
/// Обработчик получает `(expanded, id, loaded)`, где `expanded` — новая
|
||||
/// нода раскрыта, `id` — идентификатор узла, `loaded` — были ли в узел уже
|
||||
/// загружены дочерние элементы. После первого добавления детей всегда
|
||||
/// приходит `true`.
|
||||
#[lua(infallible)]
|
||||
pub fn on_toggle(&self, callback: mlua::Function) {
|
||||
self.state.borrow_mut().set_toggle_callback(Some(callback));
|
||||
}
|
||||
|
||||
/// Назначает обработчик клика по конечной ноде.
|
||||
///
|
||||
/// Обработчик получает `(id)` — идентификатор узла.
|
||||
#[lua(infallible)]
|
||||
pub fn on_click(&self, callback: mlua::Function) {
|
||||
self.state.borrow_mut().set_click_callback(Some(callback));
|
||||
}
|
||||
|
||||
/// Идентификатор выбранной строки или `nil`.
|
||||
#[lua(infallible)]
|
||||
pub fn selected(&self) -> Option<String> {
|
||||
self.state.borrow().selected().map(str::to_owned)
|
||||
}
|
||||
|
||||
/// Раскрывает всех закрытых родителей узла `id`, выбирает его и передаёт
|
||||
/// фокус дереву.
|
||||
///
|
||||
/// Возвращает `false`, если узла с таким идентификатором нет.
|
||||
#[lua(infallible)]
|
||||
pub fn focus(&self, id: String) -> bool {
|
||||
self.state.borrow_mut().focus(&id)
|
||||
}
|
||||
}
|
||||
|
||||
#[mlua::userdata_impl]
|
||||
impl PanelApi {
|
||||
/// Открывает панель.
|
||||
@@ -138,6 +243,17 @@ impl PanelApi {
|
||||
pub fn is_open(&self) -> bool {
|
||||
self.state.borrow().is_open()
|
||||
}
|
||||
|
||||
/// Добавляет в панель блок дерева и возвращает его API.
|
||||
///
|
||||
/// ```lua
|
||||
/// local tree = app.left_panel:add_tree()
|
||||
/// tree:append({ { id = "a", label = "a.txt" } }, "")
|
||||
/// ```
|
||||
#[lua(infallible, name = "add_tree")]
|
||||
pub fn add_tree(&self) -> TreeApi {
|
||||
TreeApi::new(self.state.borrow_mut().add_tree())
|
||||
}
|
||||
}
|
||||
|
||||
#[mlua::userdata_impl]
|
||||
@@ -208,6 +324,10 @@ impl AppApi {
|
||||
.map(|(index, doc)| TabInfo {
|
||||
index,
|
||||
title: doc.name.clone(),
|
||||
icon: doc
|
||||
.icon
|
||||
.clone()
|
||||
.unwrap_or_else(|| icons::DEFAULT_TAB_ICON.to_owned()),
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
@@ -362,6 +482,60 @@ impl AppApi {
|
||||
.map_err(|err| mlua::Error::RuntimeError(format!("не удалось открыть {path}: {err}")))
|
||||
}
|
||||
|
||||
/// Читает содержимое каталога (Rust-версия `app:list_dir`).
|
||||
///
|
||||
/// Пустой путь или `"."` означают текущий рабочий каталог процесса.
|
||||
/// Элементы сортируются по имени без учёта регистра. В `id` попадает путь
|
||||
/// относительно текущего каталога, если `path` тоже относительный.
|
||||
#[lua(skip)]
|
||||
pub fn list_dir(&self, path: impl AsRef<Path>) -> std::io::Result<Vec<DirEntryInfo>> {
|
||||
let raw = path.as_ref().as_os_str().to_string_lossy().into_owned();
|
||||
let root = raw.is_empty() || raw.as_str() == ".";
|
||||
let directory = if raw.is_empty() {
|
||||
Path::new(".")
|
||||
} else {
|
||||
path.as_ref()
|
||||
};
|
||||
|
||||
let mut entries = Vec::new();
|
||||
for entry in std::fs::read_dir(directory)? {
|
||||
let entry = entry?;
|
||||
let name = entry.file_name().to_string_lossy().into_owned();
|
||||
let is_dir = entry.file_type().map(|kind| kind.is_dir()).unwrap_or(false);
|
||||
let id = if root {
|
||||
name.clone()
|
||||
} else {
|
||||
Path::new(&raw)
|
||||
.join(&name)
|
||||
.to_string_lossy()
|
||||
.replace('\\', "/")
|
||||
};
|
||||
entries.push(DirEntryInfo { id, name, is_dir });
|
||||
}
|
||||
|
||||
entries.sort_by(|a, b| {
|
||||
a.name
|
||||
.to_lowercase()
|
||||
.cmp(&b.name.to_lowercase())
|
||||
.then_with(|| a.name.cmp(&b.name))
|
||||
});
|
||||
Ok(entries)
|
||||
}
|
||||
|
||||
/// Список содержимого каталога `path` относительно текущего каталога.
|
||||
///
|
||||
/// Каждый элемент — таблица `{ id = "src/main.rs", name = "main.rs",
|
||||
/// is_dir = false }`. Пустая строка или `"."` — текущий каталог. Метод
|
||||
/// предназначен для построения деревьев и списков из Lua.
|
||||
#[lua(name = "list_dir")]
|
||||
fn lua_list_dir(&self, path: Option<String>) -> mlua::Result<Vec<DirEntryInfo>> {
|
||||
let path = path.unwrap_or_default();
|
||||
self.list_dir(&path).map_err(|err| {
|
||||
let shown = if path.is_empty() { "." } else { path.as_str() };
|
||||
mlua::Error::RuntimeError(format!("не удалось прочитать каталог {shown}: {err}"))
|
||||
})
|
||||
}
|
||||
|
||||
/// Делает вкладку активной по её номеру в списке [`AppApi::tabs`].
|
||||
///
|
||||
/// Возвращает `false`, если вкладки с таким номером нет.
|
||||
@@ -375,6 +549,50 @@ impl AppApi {
|
||||
true
|
||||
}
|
||||
|
||||
/// Назначает иконку вкладке `index`.
|
||||
///
|
||||
/// `icon` — имя иконки Lucide: `file`, `file-code`, `book-open`, `settings`
|
||||
/// и т. п. Регистр не важен; `_` и пробелы равны дефису, префикс `icons/`
|
||||
/// и расширение `.svg` можно не писать.
|
||||
///
|
||||
/// Возвращает `false`, если вкладки нет или иконка с таким именем не входит
|
||||
/// в каталог.
|
||||
#[lua(skip)]
|
||||
pub fn set_tab_icon(&self, index: usize, icon: &str) -> bool {
|
||||
let Some(name) = icons::canonical_name(icon) else {
|
||||
return false;
|
||||
};
|
||||
self.workspace.borrow_mut().set_tab_icon(index, name)
|
||||
}
|
||||
|
||||
/// Lua-версия [`AppApi::set_tab_icon`].
|
||||
#[lua(infallible, name = "set_tab_icon")]
|
||||
fn lua_set_tab_icon(&self, index: usize, icon: String) -> bool {
|
||||
self.set_tab_icon(index, &icon)
|
||||
}
|
||||
|
||||
/// Возвращает вкладке иконку по умолчанию (`file`).
|
||||
///
|
||||
/// Возвращает `false`, если вкладки с таким номером нет.
|
||||
#[lua(infallible)]
|
||||
pub fn clear_tab_icon(&self, index: usize) -> bool {
|
||||
self.workspace.borrow_mut().clear_tab_icon(index)
|
||||
}
|
||||
|
||||
/// Имя иконки вкладки: назначенное через [`AppApi::set_tab_icon`] или
|
||||
/// `file` по умолчанию. `nil`, если вкладки нет.
|
||||
#[lua(infallible)]
|
||||
pub fn tab_icon(&self, index: usize) -> Option<String> {
|
||||
let workspace = self.workspace.borrow();
|
||||
workspace.document(index)?;
|
||||
Some(
|
||||
workspace
|
||||
.tab_icon(index)
|
||||
.unwrap_or(icons::DEFAULT_TAB_ICON)
|
||||
.to_owned(),
|
||||
)
|
||||
}
|
||||
|
||||
/// Включает подсветку синтаксиса для вкладки `index`.
|
||||
///
|
||||
/// `language` — название языка: `rust`, `go`, `js`, `json` или `python`
|
||||
@@ -650,6 +868,29 @@ fn parse_watcher(table: &mlua::Table) -> mlua::Result<Option<WatcherConfig>> {
|
||||
}
|
||||
}
|
||||
|
||||
/// Разбирает массив узлов дерева, переданный в `TreeApi::append`.
|
||||
fn parse_tree_nodes(nodes: &mlua::Table) -> mlua::Result<Vec<TreeNode>> {
|
||||
let mut parsed = Vec::new();
|
||||
for pair in nodes.sequence_values::<mlua::Table>() {
|
||||
let table = pair?;
|
||||
let id: String = table.get("id")?;
|
||||
let label = table.get::<String>("label").unwrap_or_else(|_| id.clone());
|
||||
let icon = table
|
||||
.get::<Option<String>>("icon")?
|
||||
.and_then(|name| icons::canonical_name(&name).map(str::to_owned));
|
||||
let expandable = table.get::<Option<bool>>("expandable")?.unwrap_or(false);
|
||||
let expanded = table.get::<Option<bool>>("expanded")?.unwrap_or(false);
|
||||
parsed.push(TreeNode {
|
||||
id,
|
||||
label,
|
||||
icon,
|
||||
expandable,
|
||||
expanded,
|
||||
});
|
||||
}
|
||||
Ok(parsed)
|
||||
}
|
||||
|
||||
fn lua_strings(value: mlua::Value) -> mlua::Result<Vec<String>> {
|
||||
match value {
|
||||
mlua::Value::String(value) => Ok(vec![value.to_string_lossy()]),
|
||||
|
||||
Reference in New Issue
Block a user