refactoring
This commit is contained in:
+292
-90
@@ -13,14 +13,18 @@ use std::collections::VecDeque;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::rc::Rc;
|
||||
use std::sync::Arc;
|
||||
use std::time::Duration;
|
||||
|
||||
use mlua::IntoLua;
|
||||
use serde_json::Number;
|
||||
|
||||
use crate::constants::{BOTTOM_PANEL_HEIGHT, LEFT_PANEL_WIDTH, RIGHT_PANEL_WIDTH};
|
||||
use crate::editor::{Language, Workspace};
|
||||
use crate::editor::{FocusAfterSync, Language, Workspace};
|
||||
use crate::icons;
|
||||
use crate::keymap::{KeyBinding, KeyMap};
|
||||
use crate::lsp::{LspConfig, LspManager, WatcherConfig, document_uri};
|
||||
use crate::keymap::{AppCommand, CompletionCommand, KeyBinding, KeyContext, KeyHandler, KeyMap};
|
||||
use crate::lsp::{
|
||||
DocumentDiagnostics, LspConfig, LspManager, WatcherConfig, document_uri, position_to_offset,
|
||||
};
|
||||
use crate::panel::{PanelHandle, PanelState};
|
||||
use crate::tree::{TreeHandle, TreeNode};
|
||||
|
||||
@@ -88,6 +92,16 @@ pub struct DirEntryInfo {
|
||||
pub is_dir: bool,
|
||||
}
|
||||
|
||||
/// Зарегистрированный Lua-обработчик диагностики.
|
||||
///
|
||||
/// `delay` — пауза тишины, после которой обработчик вызывается. Если сервер
|
||||
/// присылает сообщения чаще, вызов откладывается до последнего изменения.
|
||||
#[derive(Clone)]
|
||||
struct DiagnosticsHandler {
|
||||
delay: Duration,
|
||||
callback: mlua::Function,
|
||||
}
|
||||
|
||||
/// Глобальный объект с функциями управления приложением.
|
||||
///
|
||||
/// Использование: `AppApi::global().close()`, `AppApi::global().tabs()`.
|
||||
@@ -111,6 +125,8 @@ pub struct AppApi {
|
||||
bottom_panel: PanelHandle,
|
||||
#[lua(skip)]
|
||||
lsp: Arc<LspManager>,
|
||||
#[lua(skip)]
|
||||
diagnostics_handler: Rc<RefCell<Option<DiagnosticsHandler>>>,
|
||||
}
|
||||
|
||||
/// API одной панели окна.
|
||||
@@ -183,6 +199,16 @@ impl TreeApi {
|
||||
self.state.borrow_mut().remove(&id)
|
||||
}
|
||||
|
||||
/// Удаляет все узлы дерева и сбрасывает выбор.
|
||||
///
|
||||
/// Обработчики `on_toggle` и `on_click` продолжают действовать: дерево
|
||||
/// можно наполнить заново. Удобно для списков, которые перерисовываются
|
||||
/// целиком, например для диагностики.
|
||||
#[lua(infallible)]
|
||||
pub fn clear(&self) {
|
||||
self.state.borrow_mut().clear();
|
||||
}
|
||||
|
||||
/// Назначает обработчик раскрытия/сворачивания узла.
|
||||
///
|
||||
/// Обработчик получает `(expanded, id, loaded)`, где `expanded` — новая
|
||||
@@ -246,13 +272,18 @@ impl PanelApi {
|
||||
|
||||
/// Добавляет в панель блок дерева и возвращает его API.
|
||||
///
|
||||
/// `title` — заголовок вкладки нижней панели. Боковые панели заголовок
|
||||
/// сохраняют, но не рисуют.
|
||||
///
|
||||
/// ```lua
|
||||
/// local tree = app.left_panel:add_tree()
|
||||
/// tree:append({ { id = "a", label = "a.txt" } }, "")
|
||||
///
|
||||
/// local diagnostics = app.bottom_panel:add_tree("Диагностика")
|
||||
/// ```
|
||||
#[lua(infallible, name = "add_tree")]
|
||||
pub fn add_tree(&self) -> TreeApi {
|
||||
TreeApi::new(self.state.borrow_mut().add_tree())
|
||||
pub fn add_tree(&self, title: Option<String>) -> TreeApi {
|
||||
TreeApi::new(self.state.borrow_mut().add_tree(title))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -269,6 +300,7 @@ impl AppApi {
|
||||
right_panel: Rc::new(RefCell::new(PanelState::new(RIGHT_PANEL_WIDTH))),
|
||||
bottom_panel: Rc::new(RefCell::new(PanelState::new(BOTTOM_PANEL_HEIGHT))),
|
||||
lsp: Arc::new(LspManager::new()),
|
||||
diagnostics_handler: Rc::new(RefCell::new(None)),
|
||||
};
|
||||
INSTANCE.with(|slot| *slot.borrow_mut() = Some(api.clone()));
|
||||
api
|
||||
@@ -324,10 +356,7 @@ impl AppApi {
|
||||
.map(|(index, doc)| TabInfo {
|
||||
index,
|
||||
title: doc.name.clone(),
|
||||
icon: doc
|
||||
.icon
|
||||
.clone()
|
||||
.unwrap_or_else(|| icons::DEFAULT_TAB_ICON.to_owned()),
|
||||
icon: doc.icon_name().to_owned(),
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
@@ -345,10 +374,7 @@ impl AppApi {
|
||||
// Часть публичного API приложения: вызывается внешним кодом и тестами.
|
||||
#[lua(infallible)]
|
||||
pub fn next_tab(&self) -> usize {
|
||||
let mut workspace = self.workspace.borrow_mut();
|
||||
let next = (workspace.active_index() + 1) % workspace.documents().len();
|
||||
workspace.set_active(next);
|
||||
next
|
||||
self.workspace.borrow_mut().cycle_active(1)
|
||||
}
|
||||
|
||||
/// Переключает на предыдущую вкладку (по кругу) и возвращает её индекс.
|
||||
@@ -356,11 +382,7 @@ impl AppApi {
|
||||
// Часть публичного API приложения: вызывается внешним кодом и тестами.
|
||||
#[lua(infallible)]
|
||||
pub fn previous_tab(&self) -> usize {
|
||||
let mut workspace = self.workspace.borrow_mut();
|
||||
let count = workspace.documents().len();
|
||||
let previous = (workspace.active_index() + count - 1) % count;
|
||||
workspace.set_active(previous);
|
||||
previous
|
||||
self.workspace.borrow_mut().cycle_active(-1)
|
||||
}
|
||||
|
||||
/// Закрывает вкладку по её номеру в списке [`AppApi::tabs`].
|
||||
@@ -414,17 +436,41 @@ impl AppApi {
|
||||
/// `keys` записывается в духе nvim:
|
||||
///
|
||||
/// * `a`, `1` — нажатие клавиши без модификаторов;
|
||||
/// * `<S-c>`, `<C-z>`, `<A-r>` — нажатие с Shift, Ctrl или Alt;
|
||||
/// * `<S-c>`, `<C-z>`, `<A-r>`, `<D-s>` — нажатие с Shift, Ctrl, Alt или
|
||||
/// платформенным модификатором (Cmd/Super);
|
||||
/// * `!a` — отпускание клавиши;
|
||||
/// * `<A-!r>` — отпускание клавиши при зажатом Alt.
|
||||
///
|
||||
/// Повторное назначение той же клавиши заменяет обработчик.
|
||||
/// Второй аргумент — Lua-функция, имя встроенной команды (`save`,
|
||||
/// `next_tab`, `toggle_left_panel` и т. п.) или `nil`, чтобы удалить
|
||||
/// привязку. Повторное назначение той же клавиши заменяет обработчик.
|
||||
///
|
||||
/// # Errors
|
||||
/// Возвращает ошибку Lua, если запись привязки некорректна.
|
||||
pub fn map_key(&self, keys: String, callback: mlua::Function) -> mlua::Result<()> {
|
||||
let binding = KeyBinding::parse(&keys).map_err(mlua::Error::RuntimeError)?;
|
||||
self.keymap.borrow_mut().bind(binding, callback);
|
||||
/// Возвращает ошибку Lua, если запись привязки или имя команды некорректны.
|
||||
pub fn map_key(&self, keys: String, handler: mlua::Value) -> mlua::Result<()> {
|
||||
self.bind_key(KeyContext::Global, &keys, handler)
|
||||
}
|
||||
|
||||
/// Назначает обработчик клавиши, пока открыт список автодополнения.
|
||||
///
|
||||
/// Синтаксис совпадает с [`AppApi::map_key`]. Встроенные команды этого
|
||||
/// слоя: `completion_trigger`, `completion_next`, `completion_previous`,
|
||||
/// `completion_page_up`, `completion_page_down`, `completion_accept`,
|
||||
/// `completion_cancel`, `completion_dismiss`.
|
||||
#[lua(name = "map_completion_key")]
|
||||
pub fn map_completion_key(&self, keys: String, handler: mlua::Value) -> mlua::Result<()> {
|
||||
self.bind_key(KeyContext::Completion, &keys, handler)
|
||||
}
|
||||
|
||||
#[lua(skip)]
|
||||
fn bind_key(&self, context: KeyContext, keys: &str, handler: mlua::Value) -> mlua::Result<()> {
|
||||
let binding = KeyBinding::parse(keys).map_err(mlua::Error::RuntimeError)?;
|
||||
match parse_key_handler(context, handler)? {
|
||||
Some(handler) => self.keymap.borrow_mut().set(context, binding, handler),
|
||||
None => {
|
||||
self.keymap.borrow_mut().unbind(context, &binding);
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -458,8 +504,34 @@ impl AppApi {
|
||||
///
|
||||
/// Если файла нет, создаётся пустая вкладка с таким именем — как в vim.
|
||||
/// Язык подсветки определяется по расширению, если для него есть грамматика.
|
||||
/// Фокус редактора не меняется: для этого есть [`AppApi::open_focused`]
|
||||
/// и Lua-форма `app:open(path, {...})`.
|
||||
#[lua(skip)]
|
||||
pub fn open(&self, path: impl AsRef<Path>) -> std::io::Result<usize> {
|
||||
self.open_impl(path, None, false)
|
||||
}
|
||||
|
||||
/// Открывает файл и переводит фокус в редактор (Rust-версия
|
||||
/// `app:open(path, position)`).
|
||||
///
|
||||
/// `position` — строка и столбец с нуля, как в диагностике LSP; столбец
|
||||
/// считается в UTF-16. `None` оставляет курсор там, где он был.
|
||||
#[lua(skip)]
|
||||
pub fn open_focused(
|
||||
&self,
|
||||
path: impl AsRef<Path>,
|
||||
position: Option<(u32, u32)>,
|
||||
) -> std::io::Result<usize> {
|
||||
self.open_impl(path, position, true)
|
||||
}
|
||||
|
||||
#[lua(skip)]
|
||||
fn open_impl(
|
||||
&self,
|
||||
path: impl AsRef<Path>,
|
||||
position: Option<(u32, u32)>,
|
||||
focus_editor: bool,
|
||||
) -> std::io::Result<usize> {
|
||||
let path = path.as_ref();
|
||||
let content = match std::fs::read_to_string(path) {
|
||||
Ok(content) => content,
|
||||
@@ -467,19 +539,39 @@ impl AppApi {
|
||||
Err(err) => return Err(err),
|
||||
};
|
||||
|
||||
let cursor = position.map(|(line, column)| {
|
||||
position_to_offset(&content, lsp_types::Position::new(line, column))
|
||||
});
|
||||
let focus = if focus_editor {
|
||||
FocusAfterSync::Editor(cursor)
|
||||
} else {
|
||||
FocusAfterSync::Keep
|
||||
};
|
||||
|
||||
let name = path.to_string_lossy().into_owned();
|
||||
let language = Language::from_path(&name);
|
||||
let mut workspace = self.workspace.borrow_mut();
|
||||
Ok(workspace.open_file(name, content, language))
|
||||
let index = workspace.open_file(name, content, language);
|
||||
workspace.set_focus_after_sync(focus);
|
||||
Ok(index)
|
||||
}
|
||||
|
||||
/// Lua-версия [`AppApi::open`]: `app:open("main.go")`.
|
||||
///
|
||||
/// Второй аргумент необязателен:
|
||||
///
|
||||
/// * `nil`/нет аргумента — открыть вкладку, не трогая фокус;
|
||||
/// * `{}` — перевести фокус в редактор, курсор оставить на месте;
|
||||
/// * `{line, column}` — перевести фокус и поставить курсор (с нуля).
|
||||
///
|
||||
/// Возвращает номер вкладки (с нуля). Ошибка чтения становится ошибкой Lua.
|
||||
#[lua(name = "open")]
|
||||
fn lua_open(&self, path: String) -> mlua::Result<usize> {
|
||||
self.open(&path)
|
||||
.map_err(|err| mlua::Error::RuntimeError(format!("не удалось открыть {path}: {err}")))
|
||||
fn lua_open(&self, path: String, position: mlua::Value) -> mlua::Result<usize> {
|
||||
match parse_open_focus(position)? {
|
||||
OpenFocus::Keep => self.open(&path),
|
||||
OpenFocus::Editor(position) => self.open_focused(&path, position),
|
||||
}
|
||||
.map_err(|err| mlua::Error::RuntimeError(format!("не удалось открыть {path}: {err}")))
|
||||
}
|
||||
|
||||
/// Читает содержимое каталога (Rust-версия `app:list_dir`).
|
||||
@@ -490,7 +582,7 @@ impl AppApi {
|
||||
#[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 relative_to_cwd = raw.is_empty() || raw == ".";
|
||||
let directory = if raw.is_empty() {
|
||||
Path::new(".")
|
||||
} else {
|
||||
@@ -502,7 +594,7 @@ impl AppApi {
|
||||
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 {
|
||||
let id = if relative_to_cwd {
|
||||
name.clone()
|
||||
} else {
|
||||
Path::new(&raw)
|
||||
@@ -583,14 +675,10 @@ impl AppApi {
|
||||
/// `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(),
|
||||
)
|
||||
self.workspace
|
||||
.borrow()
|
||||
.document(index)
|
||||
.map(|doc| doc.icon_name().to_owned())
|
||||
}
|
||||
|
||||
/// Включает подсветку синтаксиса для вкладки `index`.
|
||||
@@ -746,6 +834,108 @@ impl AppApi {
|
||||
pub fn restart_lsp(&self, name: String) -> bool {
|
||||
self.lsp.restart(&name)
|
||||
}
|
||||
|
||||
/// Регистрирует обработчик диагностики всех LSP-серверов.
|
||||
///
|
||||
/// `delay_ms` — пауза тишины в миллисекундах: обработчик вызывается после
|
||||
/// последнего обновления диагностики. Если серверы присылают сообщения
|
||||
/// чаще задержки, промежуточные вызовы пропускаются, а последний вызов
|
||||
/// выполняется после того, как обновления прекратились.
|
||||
///
|
||||
/// Обработчик получает один аргумент — массив записей
|
||||
/// `{ server, uri, path, diagnostics = { ... } }`. У сообщения есть
|
||||
/// `line`, `character`, `end_line`, `end_character` (с нуля, как в LSP),
|
||||
/// `severity` (1–4), `severity_name`, `message`, `source`, `code`, `tags`.
|
||||
/// Передача `nil` вместо функции удаляет обработчик.
|
||||
///
|
||||
/// ```lua
|
||||
/// app:on_lsp_diagnostics(1000, function(servers)
|
||||
/// for _, entry in ipairs(servers) do
|
||||
/// print(entry.server, entry.path, #entry.diagnostics)
|
||||
/// end
|
||||
/// end)
|
||||
/// ```
|
||||
#[lua(infallible, name = "on_lsp_diagnostics")]
|
||||
pub fn on_lsp_diagnostics(&self, delay_ms: u64, callback: Option<mlua::Function>) {
|
||||
let delay = Duration::from_millis(delay_ms);
|
||||
*self.diagnostics_handler.borrow_mut() =
|
||||
callback.map(|callback| DiagnosticsHandler { delay, callback });
|
||||
// Обработчик должен получить уже накопленную диагностику, а не ждать
|
||||
// следующего ответа LSP-сервера.
|
||||
self.lsp.refresh_diagnostics();
|
||||
}
|
||||
|
||||
/// Задержка зарегистрированного обработчика диагностики, если он есть.
|
||||
#[lua(skip)]
|
||||
pub fn diagnostics_delay(&self) -> Option<Duration> {
|
||||
self.diagnostics_handler
|
||||
.borrow()
|
||||
.as_ref()
|
||||
.map(|handler| handler.delay)
|
||||
}
|
||||
|
||||
/// Вызывает Lua-обработчик диагностики со свежим снимком.
|
||||
///
|
||||
/// Возвращает `false`, если обработчик не зарегистрирован.
|
||||
#[lua(skip)]
|
||||
pub fn emit_diagnostics(&self, report: Vec<DocumentDiagnostics>) -> bool {
|
||||
let Some(callback) = self
|
||||
.diagnostics_handler
|
||||
.borrow()
|
||||
.as_ref()
|
||||
.map(|handler| handler.callback.clone())
|
||||
else {
|
||||
return false;
|
||||
};
|
||||
|
||||
let Some(lua) = callback.weak_lua().try_upgrade() else {
|
||||
return false;
|
||||
};
|
||||
let value = match report.into_lua(&lua) {
|
||||
Ok(value) => value,
|
||||
Err(err) => {
|
||||
eprintln!("Lua: не удалось передать диагностику: {err}");
|
||||
return false;
|
||||
}
|
||||
};
|
||||
if let Err(err) = callback.call::<()>(value) {
|
||||
eprintln!("Lua: ошибка в обработчике диагностики: {err}");
|
||||
}
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
/// Второй аргумент `app:open`.
|
||||
enum OpenFocus {
|
||||
/// Фокус не меняется.
|
||||
Keep,
|
||||
/// Фокус переходит в редактор; `None` сохраняет текущий курсор.
|
||||
Editor(Option<(u32, u32)>),
|
||||
}
|
||||
|
||||
/// Разбирает необязательный второй аргумент `app:open`.
|
||||
fn parse_open_focus(value: mlua::Value) -> mlua::Result<OpenFocus> {
|
||||
let table = match value {
|
||||
mlua::Value::Nil => return Ok(OpenFocus::Keep),
|
||||
mlua::Value::Table(table) => table,
|
||||
_ => {
|
||||
return Err(mlua::Error::RuntimeError(
|
||||
"во втором аргументе app:open нужна таблица {строка, столбец} или {}".to_owned(),
|
||||
));
|
||||
}
|
||||
};
|
||||
|
||||
let line = table.get::<Option<i64>>(1)?;
|
||||
let column = table.get::<Option<i64>>(2)?;
|
||||
match (line, column) {
|
||||
(None, None) => Ok(OpenFocus::Editor(None)),
|
||||
(Some(line), Some(column)) if line >= 0 && column >= 0 => {
|
||||
Ok(OpenFocus::Editor(Some((line as u32, column as u32))))
|
||||
}
|
||||
_ => Err(mlua::Error::RuntimeError(
|
||||
"позиция в app:open — два неотрицательных числа: {строка, столбец}".to_owned(),
|
||||
)),
|
||||
}
|
||||
}
|
||||
|
||||
/// Разбирает Lua-таблицу, переданную в `app:run_lsp`.
|
||||
@@ -754,14 +944,9 @@ fn parse_lsp_config(table: &mlua::Table) -> mlua::Result<LspConfig> {
|
||||
.get("name")
|
||||
.map_err(|_| mlua::Error::RuntimeError("в run_lsp нужен строковый name".to_owned()))?;
|
||||
|
||||
let command = ["cmd", "command", "path", "binary"]
|
||||
.iter()
|
||||
.find_map(|key| table.get::<String>(*key).ok())
|
||||
.ok_or_else(|| {
|
||||
mlua::Error::RuntimeError(
|
||||
"в run_lsp нужен путь к серверу: cmd, command, path или binary".to_owned(),
|
||||
)
|
||||
})?;
|
||||
let command: String = table
|
||||
.get("cmd")
|
||||
.map_err(|_| mlua::Error::RuntimeError("в run_lsp нужен строковый cmd".to_owned()))?;
|
||||
|
||||
let args = match table.get::<mlua::Table>("args") {
|
||||
Ok(args) => args
|
||||
@@ -777,32 +962,22 @@ fn parse_lsp_config(table: &mlua::Table) -> mlua::Result<LspConfig> {
|
||||
Err(_) => Vec::new(),
|
||||
};
|
||||
|
||||
let root = table
|
||||
.get::<String>("root")
|
||||
.ok()
|
||||
.map(PathBuf::from)
|
||||
.unwrap_or_else(|| std::env::current_dir().unwrap_or_else(|_| PathBuf::from(".")));
|
||||
let root = if root.is_absolute() {
|
||||
root
|
||||
} else {
|
||||
std::env::current_dir()
|
||||
.unwrap_or_else(|_| PathBuf::from("."))
|
||||
.join(root)
|
||||
// Корень нормализуется в `LspManager::start`: относительный путь там
|
||||
// отсчитывается от текущего каталога процесса.
|
||||
let root = match table.get::<String>("root") {
|
||||
Ok(root) => PathBuf::from(root),
|
||||
Err(_) => std::env::current_dir().unwrap_or_else(|_| PathBuf::from(".")),
|
||||
};
|
||||
let root = std::fs::canonicalize(&root).unwrap_or(root);
|
||||
|
||||
let languages = match table.get::<mlua::Value>("languages") {
|
||||
Ok(value) => lua_strings(value)?,
|
||||
Err(_) => Vec::new(),
|
||||
};
|
||||
|
||||
let initialization_options = ["initialization_options", "init_options"]
|
||||
.iter()
|
||||
.find_map(|key| match table.get::<mlua::Value>(*key) {
|
||||
Ok(mlua::Value::Nil) | Err(_) => None,
|
||||
Ok(value) => Some(lua_to_json(value)),
|
||||
})
|
||||
.transpose()?;
|
||||
let initialization_options = match table.get::<mlua::Value>("initialization_options") {
|
||||
Ok(mlua::Value::Nil) | Err(_) => None,
|
||||
Ok(value) => Some(lua_to_json(value)?),
|
||||
};
|
||||
|
||||
Ok(LspConfig {
|
||||
name,
|
||||
@@ -817,15 +992,9 @@ fn parse_lsp_config(table: &mlua::Table) -> mlua::Result<LspConfig> {
|
||||
}
|
||||
|
||||
fn parse_watcher(table: &mlua::Table) -> mlua::Result<Option<WatcherConfig>> {
|
||||
let value = ["watcher", "file_watcher", "watch"].iter().find_map(|key| {
|
||||
match table.get::<mlua::Value>(*key) {
|
||||
Ok(mlua::Value::Nil) | Err(_) => None,
|
||||
Ok(value) => Some(value),
|
||||
}
|
||||
});
|
||||
|
||||
let Some(value) = value else {
|
||||
return Ok(None);
|
||||
let value = match table.get::<mlua::Value>("watcher") {
|
||||
Ok(mlua::Value::Nil) | Err(_) => return Ok(None),
|
||||
Ok(value) => value,
|
||||
};
|
||||
|
||||
match value {
|
||||
@@ -839,22 +1008,14 @@ fn parse_watcher(table: &mlua::Table) -> mlua::Result<Option<WatcherConfig>> {
|
||||
})),
|
||||
mlua::Value::Boolean(false) | mlua::Value::Nil => Ok(None),
|
||||
mlua::Value::Table(config) => {
|
||||
let patterns = ["pattern", "patterns"]
|
||||
.iter()
|
||||
.find_map(|key| match config.get::<mlua::Value>(*key) {
|
||||
Ok(mlua::Value::Nil) | Err(_) => None,
|
||||
Ok(value) => Some(lua_strings(value)),
|
||||
})
|
||||
.transpose()?
|
||||
.unwrap_or_default();
|
||||
let ignore = ["ignore", "ignored"]
|
||||
.iter()
|
||||
.find_map(|key| match config.get::<mlua::Value>(*key) {
|
||||
Ok(mlua::Value::Nil) | Err(_) => None,
|
||||
Ok(value) => Some(lua_strings(value)),
|
||||
})
|
||||
.transpose()?
|
||||
.unwrap_or_default();
|
||||
let patterns = match config.get::<mlua::Value>("pattern") {
|
||||
Ok(mlua::Value::Nil) | Err(_) => Vec::new(),
|
||||
Ok(value) => lua_strings(value)?,
|
||||
};
|
||||
let ignore = match config.get::<mlua::Value>("ignore") {
|
||||
Ok(mlua::Value::Nil) | Err(_) => Vec::new(),
|
||||
Ok(value) => lua_strings(value)?,
|
||||
};
|
||||
|
||||
if patterns.is_empty() {
|
||||
Ok(None)
|
||||
@@ -868,6 +1029,47 @@ fn parse_watcher(table: &mlua::Table) -> mlua::Result<Option<WatcherConfig>> {
|
||||
}
|
||||
}
|
||||
|
||||
/// Разбирает обработчик, переданный в `app:map_key` / `app:map_completion_key`.
|
||||
///
|
||||
/// `nil` означает «удалить привязку».
|
||||
fn parse_key_handler(
|
||||
context: KeyContext,
|
||||
handler: mlua::Value,
|
||||
) -> mlua::Result<Option<KeyHandler>> {
|
||||
match handler {
|
||||
mlua::Value::Nil => Ok(None),
|
||||
mlua::Value::Function(callback) => Ok(Some(KeyHandler::Lua(callback))),
|
||||
mlua::Value::String(name) => {
|
||||
let name = name.to_string_lossy();
|
||||
let parsed = match context {
|
||||
KeyContext::Global => AppCommand::parse(&name).map(KeyHandler::App),
|
||||
KeyContext::Completion => {
|
||||
CompletionCommand::parse(&name).map(KeyHandler::Completion)
|
||||
}
|
||||
};
|
||||
parsed.map(Some).ok_or_else(|| {
|
||||
let available = match context {
|
||||
KeyContext::Global => AppCommand::ALL
|
||||
.iter()
|
||||
.map(|command| command.name())
|
||||
.collect::<Vec<_>>(),
|
||||
KeyContext::Completion => CompletionCommand::ALL
|
||||
.iter()
|
||||
.map(|command| command.name())
|
||||
.collect::<Vec<_>>(),
|
||||
};
|
||||
mlua::Error::RuntimeError(format!(
|
||||
"неизвестная команда клавиши: {name}; доступны: {}",
|
||||
available.join(", ")
|
||||
))
|
||||
})
|
||||
}
|
||||
_ => Err(mlua::Error::RuntimeError(
|
||||
"обработчик должен быть функцией, именем команды или nil".to_owned(),
|
||||
)),
|
||||
}
|
||||
}
|
||||
|
||||
/// Разбирает массив узлов дерева, переданный в `TreeApi::append`.
|
||||
fn parse_tree_nodes(nodes: &mlua::Table) -> mlua::Result<Vec<TreeNode>> {
|
||||
let mut parsed = Vec::new();
|
||||
|
||||
Reference in New Issue
Block a user