427 lines
17 KiB
Rust
427 lines
17 KiB
Rust
//! Глобальный 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<Option<AppApi>> = const { RefCell::new(None) };
|
||
|
||
/// Команды окна, ожидающие ближайшего кадра.
|
||
static WINDOW_COMMANDS: RefCell<VecDeque<WindowCommand>> = const { RefCell::new(VecDeque::new()) };
|
||
|
||
/// Развёрнуто ли окно. Вид обновляет значение в каждом кадре, чтобы
|
||
/// `app:is_maximized()` из Lua видел актуальное состояние.
|
||
static WINDOW_MAXIMIZED: Cell<bool> = 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<WindowCommand> {
|
||
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<RefCell<Workspace>>,
|
||
#[lua(skip)]
|
||
keymap: Rc<RefCell<KeyMap>>,
|
||
#[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<RefCell<Workspace>>, keymap: Rc<RefCell<KeyMap>>) -> 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<TabInfo> {
|
||
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` — нажатие клавиши без модификаторов;
|
||
/// * `<S-c>`, `<C-z>`, `<A-r>` — нажатие с Shift, Ctrl или Alt;
|
||
/// * `!a` — отпускание клавиши;
|
||
/// * `<A-!r>` — отпускание клавиши при зажатом 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<String>) {
|
||
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<String> {
|
||
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)
|
||
}
|
||
}
|