//! Глобальный API приложения. //! //! Единственная точка входа для команд управления окном и редактором: UI-код //! и Lua-сценарии не трогают `Window` и состояние вида напрямую, а вызывают //! методы [`AppApi`]. //! //! Команды окна не выполняются в момент вызова: Lua-обработчик выполняется вне //! контекста `App`/`Window`, поэтому [`AppApi`] складывает их в очередь, а вид //! вычитывает её в начале кадра. use std::cell::{Cell, RefCell}; use std::collections::VecDeque; use std::rc::Rc; use crate::constants::{BOTTOM_PANEL_HEIGHT, LEFT_PANEL_WIDTH, RIGHT_PANEL_WIDTH}; use crate::editor::{Language, Workspace}; use crate::keymap::{KeyBinding, KeyMap}; use crate::panel::{PanelHandle, PanelState}; thread_local! { /// Текущий экземпляр API. `thread_local`, потому что UI GPUI привязан к /// потоку с event loop; заодно это делает тесты независимыми друг от друга. static INSTANCE: RefCell> = const { RefCell::new(None) }; /// Команды окна, ожидающие ближайшего кадра. static WINDOW_COMMANDS: RefCell> = const { RefCell::new(VecDeque::new()) }; /// Развёрнуто ли окно. Вид обновляет значение в каждом кадре, чтобы /// `app:is_maximized()` из Lua видел актуальное состояние. static WINDOW_MAXIMIZED: Cell = const { Cell::new(false) }; } /// Действие над окном, отложенное до ближайшего кадра. #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub enum WindowCommand { Close, Minimize, ToggleMaximize, SetMaximized(bool), } /// Запоминает, развёрнуто ли окно сейчас. pub fn set_window_maximized(maximized: bool) { WINDOW_MAXIMIZED.with(|state| state.set(maximized)); } /// Забирает накопленные команды окна. /// /// Очередь очищается: выполнять команды нужно ровно один раз. pub fn drain_window_commands() -> Vec { WINDOW_COMMANDS.with(|queue| queue.borrow_mut().drain(..).collect()) } /// Кладёт команду в очередь. fn push_window_command(command: WindowCommand) { WINDOW_COMMANDS.with(|queue| queue.borrow_mut().push_back(command)); } /// Краткая информация об открытой вкладке. #[derive(Clone, Debug, PartialEq, Eq)] pub struct TabInfo { /// Номер вкладки (с нуля), слева направо. pub index: usize, /// Название — то же, что показано в заголовке вкладки. pub title: String, } /// Глобальный объект с функциями управления приложением. /// /// Использование: `AppApi::global().close()`, `AppApi::global().tabs()`. /// Экземпляр устанавливается один раз при создании приложения (см. /// [`AppApi::install`]) и указывает на общие с видом документы и карту клавиш. /// /// Методы этого `impl`-блока автоматически экспортируются в Lua через /// `#[mlua::userdata_impl]`: в сценариях они доступны как `app:method(...)`. /// Чтобы не экспортировать метод, пометьте его `#[lua(skip)]`. #[derive(Clone, mlua::UserData)] pub struct AppApi { #[lua(skip)] workspace: Rc>, #[lua(skip)] keymap: Rc>, #[lua(skip)] left_panel: PanelHandle, #[lua(skip)] right_panel: PanelHandle, #[lua(skip)] bottom_panel: PanelHandle, } /// API одной панели окна. /// /// Один и тот же тип используется для `app.left_panel`, `app.right_panel` и /// `app.bottom_panel`: отличается только состояние, на которое ссылается /// объект. Поэтому панели управляются одинаково: `app.left_panel:open()`, /// `app.bottom_panel:toggle()` и так далее. #[derive(mlua::UserData)] pub struct PanelApi { #[lua(skip)] state: PanelHandle, } impl PanelApi { fn new(state: PanelHandle) -> Self { Self { state } } } #[mlua::userdata_impl] impl PanelApi { /// Открывает панель. #[lua(infallible)] pub fn open(&self) { self.state.borrow_mut().open(); } /// Закрывает панель. #[lua(infallible)] pub fn close(&self) { self.state.borrow_mut().close(); } /// Переключает панель и возвращает новое состояние. #[lua(infallible)] pub fn toggle(&self) -> bool { self.state.borrow_mut().toggle() } /// Открыта ли панель прямо сейчас. #[lua(infallible)] pub fn is_open(&self) -> bool { self.state.borrow().is_open() } } #[mlua::userdata_impl] impl AppApi { /// Делает переданные документы и карту клавиш текущими для /// [`AppApi::global`]. #[lua(skip)] pub fn install(workspace: Rc>, keymap: Rc>) -> Self { let api = Self { workspace, keymap, left_panel: Rc::new(RefCell::new(PanelState::new(LEFT_PANEL_WIDTH))), right_panel: Rc::new(RefCell::new(PanelState::new(RIGHT_PANEL_WIDTH))), bottom_panel: Rc::new(RefCell::new(PanelState::new(BOTTOM_PANEL_HEIGHT))), }; INSTANCE.with(|slot| *slot.borrow_mut() = Some(api.clone())); api } /// Глобальный экземпляр API. /// /// # Panics /// Если [`AppApi::install`] ещё не вызывался. #[lua(skip)] pub fn global() -> Self { INSTANCE.with(|slot| { slot.borrow() .clone() .expect("AppApi не инициализирован: сначала вызовите AppApi::install") }) } /// Состояние левой панели (для вида). #[lua(skip)] pub fn left_panel_handle(&self) -> PanelHandle { self.left_panel.clone() } /// Состояние правой панели (для вида). #[lua(skip)] pub fn right_panel_handle(&self) -> PanelHandle { self.right_panel.clone() } /// Состояние нижней панели (для вида). #[lua(skip)] pub fn bottom_panel_handle(&self) -> PanelHandle { self.bottom_panel.clone() } /// Все открытые вкладки в порядке слева направо. #[allow(dead_code)] // Часть публичного API приложения: вызывается внешним кодом и тестами. #[lua(infallible)] pub fn tabs(&self) -> Vec { self.workspace .borrow() .documents() .iter() .enumerate() .map(|(index, doc)| TabInfo { index, title: doc.name.clone(), }) .collect() } /// Индекс активной вкладки в списке [`AppApi::tabs`]. #[allow(dead_code)] // Часть публичного API приложения: вызывается внешним кодом и тестами. #[lua(infallible, name = "active_tab")] pub fn active_tab_index(&self) -> usize { self.workspace.borrow().active_index() } /// Переключает на следующую вкладку (по кругу) и возвращает её индекс. #[allow(dead_code)] // Часть публичного 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 } /// Переключает на предыдущую вкладку (по кругу) и возвращает её индекс. #[allow(dead_code)] // Часть публичного 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 } /// Закрывает вкладку по её номеру в списке [`AppApi::tabs`]. /// /// Возвращает `false`, если вкладки с таким номером нет. Если закрывается /// последняя вкладка, вместо неё открывается пустой документ. #[lua(infallible)] pub fn close_tab(&self, index: usize) -> bool { let mut workspace = self.workspace.borrow_mut(); if index >= workspace.documents().len() { return false; } workspace.close(index); true } /// Сохраняет содержимое активной вкладки в файл, названный как сама вкладка. /// /// Путь считается относительно текущего рабочего каталога процесса. /// При успешной записи с вкладки снимается маркер несохранённых изменений. #[lua(skip)] pub fn save(&self) -> std::io::Result<()> { let (path, content) = { let workspace = self.workspace.borrow(); let doc = workspace.active(); (doc.name.clone(), doc.content.clone()) }; std::fs::write(&path, content)?; self.workspace.borrow_mut().mark_active_saved(); Ok(()) } /// Lua-версия [`AppApi::save`]: ошибка ввода-вывода становится ошибкой Lua. #[lua(name = "save")] fn lua_save(&self) -> mlua::Result<()> { self.save() .map_err(|err| mlua::Error::RuntimeError(err.to_string())) } /// Назначает обработчик на нажатие или отпускание клавиши. /// /// `keys` записывается в духе nvim: /// /// * `a`, `1` — нажатие клавиши без модификаторов; /// * ``, ``, `` — нажатие с Shift, Ctrl или Alt; /// * `!a` — отпускание клавиши; /// * `` — отпускание клавиши при зажатом Alt. /// /// Повторное назначение той же клавиши заменяет обработчик. /// /// # 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); Ok(()) } /// Содержимое активной вкладки. #[lua(infallible)] pub fn active_text(&self) -> String { self.workspace.borrow().active().content.clone() } /// Заменяет содержимое активной вкладки. #[lua(skip)] pub fn set_active_text(&self, text: impl Into) { self.workspace.borrow_mut().set_active_text(text); } /// Lua-версия [`AppApi::set_active_text`] (generic-методы не экспортируются). #[lua(infallible, name = "set_active_text")] fn lua_set_active_text(&self, text: String) { self.set_active_text(text); } /// Открывает новую пустую вкладку, делает её активной и возвращает её индекс. #[lua(infallible)] pub fn open_tab(&self) -> usize { let mut workspace = self.workspace.borrow_mut(); workspace.open_new(); workspace.active_index() } /// Делает вкладку активной по её номеру в списке [`AppApi::tabs`]. /// /// Возвращает `false`, если вкладки с таким номером нет. #[lua(infallible)] pub fn set_active_tab(&self, index: usize) -> bool { let mut workspace = self.workspace.borrow_mut(); if index >= workspace.documents().len() { return false; } workspace.set_active(index); true } /// Включает подсветку синтаксиса для вкладки `index`. /// /// `language` — название языка: `rust`, `go`, `js`, `json` или `python` /// (принимаются также `rs`, `golang`, `javascript`, `py`; регистр не важен). /// Подсветка по умолчанию выключена и включается только этим методом. /// /// Возвращает `false`, если вкладки нет или язык не поддерживается. #[lua(skip)] pub fn set_language(&self, index: usize, language: &str) -> bool { let Some(language) = Language::from_name(language) else { return false; }; self.workspace.borrow_mut().set_language(index, language) } /// Lua-версия [`AppApi::set_language`]. #[lua(infallible, name = "set_language")] fn lua_set_language(&self, index: usize, language: String) -> bool { self.set_language(index, &language) } /// Язык подсветки вкладки: `"rust"`, `"go"`, `"js"`, `"json"`, `"python"` /// или `nil`, если подсветка выключена / вкладки нет. #[lua(infallible)] pub fn language(&self, index: usize) -> Option { self.workspace .borrow() .document(index) .and_then(|doc| doc.language) .map(|language| language.name().to_owned()) } /// Выключает подсветку синтаксиса для вкладки. /// /// Возвращает `false`, если вкладки с таким номером нет. #[lua(infallible)] pub fn clear_language(&self, index: usize) -> bool { self.workspace.borrow_mut().clear_language(index) } /// Левая панель окна. /// /// В Lua доступна как `app.left_panel` с методами `open`, `close`, /// `toggle` и `is_open`. #[allow(dead_code)] #[lua(getter, infallible)] pub fn left_panel(&self) -> PanelApi { PanelApi::new(self.left_panel.clone()) } /// Правая панель окна. /// /// В Lua доступна как `app.right_panel` с методами `open`, `close`, /// `toggle` и `is_open`. #[allow(dead_code)] #[lua(getter, infallible)] pub fn right_panel(&self) -> PanelApi { PanelApi::new(self.right_panel.clone()) } /// Нижняя панель окна. /// /// В Lua доступна как `app.bottom_panel` с теми же методами, что и /// `app.right_panel`. #[allow(dead_code)] #[lua(getter, infallible)] pub fn bottom_panel(&self) -> PanelApi { PanelApi::new(self.bottom_panel.clone()) } /// Закрывает окно приложения. #[lua(infallible)] pub fn close(&self) { push_window_command(WindowCommand::Close); } /// Разворачивает окно на весь экран или возвращает его прежний размер. #[lua(infallible)] pub fn toggle_maximize(&self) { push_window_command(WindowCommand::ToggleMaximize); } /// Разворачивает (`true`) или восстанавливает (`false`) окно. #[lua(infallible)] pub fn set_maximized(&self, maximized: bool) { push_window_command(WindowCommand::SetMaximized(maximized)); } /// Сворачивает окно. #[lua(infallible)] pub fn minimize(&self) { push_window_command(WindowCommand::Minimize); } /// Развёрнуто ли окно прямо сейчас. #[lua(infallible)] pub fn is_maximized(&self) -> bool { WINDOW_MAXIMIZED.with(Cell::get) } }