Files
qode/src/api.rs
T
2026-10-30 01:02:48 +03:00

427 lines
17 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
//! Глобальный 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)
}
}