193 lines
8.4 KiB
Rust
193 lines
8.4 KiB
Rust
//! Выполнение Lua-сценариев приложения.
|
|
//!
|
|
//! При запуске приложение выполняет [`STARTUP_SCRIPT`] из каталога [`script_dir`]
|
|
//! (см. [`ScriptEngine::run_startup`]). Скриптам доступны:
|
|
//!
|
|
//! * объект `api` — управление приложением ([`AppApi`]). Он разбит на группы:
|
|
//! `api.editor` (редактор), `api.panels` (панели), `api.path` (файловая
|
|
//! система), `api.window` (окно и поле ввода) и `api.keymap` (глобальные
|
|
//! привязки); вызов — через двоеточие: `api.editor:save()`;
|
|
//! * функция `include(path)` — выполняет другой Lua-файл относительно [`script_dir`]
|
|
//! (или по абсолютному пути) в общем глобальном окружении;
|
|
//! * стандартный `require`, который также ищет модули в [`script_dir`].
|
|
//!
|
|
//! Каталог по умолчанию — текущий рабочий каталог. Его можно переопределить
|
|
//! аргументом `--config <каталог>` при запуске приложения (см.
|
|
//! [`parse_command_line`](crate::constants::parse_command_line)).
|
|
|
|
use std::path::{Path, PathBuf};
|
|
|
|
use mlua::{Lua, Table};
|
|
|
|
use crate::api::{AppApi, DirEntryInfo, TabInfo};
|
|
use crate::constants::{STARTUP_SCRIPT, script_dir};
|
|
use crate::lsp::{DiagnosticInfo, DocumentDiagnostics};
|
|
|
|
/// Вкладка превращается в Lua-таблицу `{ index = 0, title = "main.rs", icon = "file" }`.
|
|
impl mlua::IntoLua for TabInfo {
|
|
fn into_lua(self, lua: &Lua) -> mlua::Result<mlua::Value> {
|
|
let tab = lua.create_table()?;
|
|
tab.set("index", self.index)?;
|
|
tab.set("title", self.title)?;
|
|
tab.set("icon", self.icon)?;
|
|
Ok(mlua::Value::Table(tab))
|
|
}
|
|
}
|
|
|
|
/// Элемент каталога превращается в Lua-таблицу
|
|
/// `{ id = "src/main.rs", name = "main.rs", is_dir = false }`.
|
|
impl mlua::IntoLua for DirEntryInfo {
|
|
fn into_lua(self, lua: &Lua) -> mlua::Result<mlua::Value> {
|
|
let entry = lua.create_table()?;
|
|
entry.set("id", self.id)?;
|
|
entry.set("name", self.name)?;
|
|
entry.set("is_dir", self.is_dir)?;
|
|
Ok(mlua::Value::Table(entry))
|
|
}
|
|
}
|
|
|
|
/// Диагностика сервера для документа превращается в Lua-таблицу
|
|
/// `{ server = "gopls", uri = "file:///...", path = "/...", diagnostics = {...} }`.
|
|
impl mlua::IntoLua for DocumentDiagnostics {
|
|
fn into_lua(self, lua: &Lua) -> mlua::Result<mlua::Value> {
|
|
let entry = lua.create_table()?;
|
|
entry.set("server", self.server)?;
|
|
entry.set("uri", self.uri)?;
|
|
entry.set("path", self.path)?;
|
|
entry.set("diagnostics", self.diagnostics)?;
|
|
Ok(mlua::Value::Table(entry))
|
|
}
|
|
}
|
|
|
|
/// Сообщение диагностики превращается в Lua-таблицу с позицией (с нуля),
|
|
/// уровнем, текстом, источником, кодом и тегами.
|
|
impl mlua::IntoLua for DiagnosticInfo {
|
|
fn into_lua(self, lua: &Lua) -> mlua::Result<mlua::Value> {
|
|
let entry = lua.create_table()?;
|
|
entry.set("line", self.line)?;
|
|
entry.set("character", self.character)?;
|
|
entry.set("end_line", self.end_line)?;
|
|
entry.set("end_character", self.end_character)?;
|
|
entry.set("severity", self.severity)?;
|
|
entry.set("severity_name", self.severity_name)?;
|
|
entry.set("message", self.message)?;
|
|
entry.set("source", self.source)?;
|
|
entry.set("code", self.code)?;
|
|
entry.set("tags", self.tags)?;
|
|
Ok(mlua::Value::Table(entry))
|
|
}
|
|
}
|
|
|
|
/// Движок Lua и состояние его глобального окружения.
|
|
pub struct ScriptEngine {
|
|
lua: Lua,
|
|
/// Каталог, относительно которого ищутся `init.lua`, `include` и `require`.
|
|
script_dir: PathBuf,
|
|
}
|
|
|
|
impl ScriptEngine {
|
|
/// Создаёт движок и публикует в нём API приложения.
|
|
///
|
|
/// Каталог сценариев берётся из [`script_dir`] (то есть из `--config`
|
|
/// или текущего рабочего каталога).
|
|
pub fn new(api: AppApi) -> mlua::Result<Self> {
|
|
Self::new_in(api, script_dir())
|
|
}
|
|
|
|
/// Создаёт движок с явно заданным каталогом сценариев.
|
|
///
|
|
/// Полезно тестам и будущим режимам, где каталог конфигурации приходит
|
|
/// не из глобального `--config`.
|
|
pub fn new_in(api: AppApi, directory: impl AsRef<Path>) -> mlua::Result<Self> {
|
|
let engine = Self {
|
|
lua: Lua::new(),
|
|
script_dir: directory.as_ref().to_path_buf(),
|
|
};
|
|
engine.expose_api(api)?;
|
|
engine.configure_require()?;
|
|
Ok(engine)
|
|
}
|
|
|
|
/// Выполняет стартовый сценарий из каталога движка.
|
|
///
|
|
/// # Errors
|
|
/// Возвращает ошибку, если файла нет, его не удалось прочитать или в нём
|
|
/// есть синтаксическая/исполняемая ошибка.
|
|
pub fn run_startup(&self) -> mlua::Result<()> {
|
|
self.run_file(&script_path(&self.script_dir, STARTUP_SCRIPT))
|
|
}
|
|
|
|
/// Выполняет Lua-файл в глобальном окружении движка.
|
|
pub fn run_file(&self, path: &Path) -> mlua::Result<()> {
|
|
run_file(&self.lua, path).map(|_| ())
|
|
}
|
|
|
|
/// Публикует объект `api` с функциями управления приложением.
|
|
///
|
|
/// Все методы объектов [`AppApi`] из `#[mlua::userdata_impl]`-блоков
|
|
/// доступны как методы userdata: `api.editor:save()`,
|
|
/// `api.path:list("src")`, `api.window:close()`. Номера вкладок всюду
|
|
/// нумеруются с нуля.
|
|
fn expose_api(&self, api: AppApi) -> mlua::Result<()> {
|
|
self.lua
|
|
.globals()
|
|
.set("api", self.lua.create_userdata(api)?)?;
|
|
Ok(())
|
|
}
|
|
|
|
/// Добавляет каталог сценариев в `package.path`, чтобы `require` находил
|
|
/// соседние Lua-модули, и объявляет `include`.
|
|
fn configure_require(&self) -> mlua::Result<()> {
|
|
let globals = self.lua.globals();
|
|
|
|
if let Ok(package) = globals.get::<Table>("package") {
|
|
let current: String = package.get("path").unwrap_or_default();
|
|
let dir = self.script_dir.display().to_string().replace('\\', "/");
|
|
let extra = format!("{dir}/?.lua;{dir}/?/init.lua");
|
|
let path = if current.is_empty() {
|
|
extra
|
|
} else {
|
|
format!("{extra};{current}")
|
|
};
|
|
package.set("path", path)?;
|
|
}
|
|
|
|
// include выполняет файл сразу, как dofile, но путь по умолчанию
|
|
// отсчитывается от каталога сценариев, а не от cwd.
|
|
let directory = self.script_dir.clone();
|
|
globals.set(
|
|
"include",
|
|
self.lua.create_function(move |lua, path: String| {
|
|
run_file(lua, &script_path(&directory, &path))
|
|
})?,
|
|
)?;
|
|
|
|
Ok(())
|
|
}
|
|
}
|
|
|
|
/// Путь к файлу сценария: абсолютные пути используются как есть, остальные
|
|
/// отсчитываются от каталога сценариев.
|
|
fn script_path(directory: &Path, path: &str) -> PathBuf {
|
|
let path = Path::new(path);
|
|
if path.is_absolute() {
|
|
path.to_path_buf()
|
|
} else {
|
|
directory.join(path)
|
|
}
|
|
}
|
|
|
|
/// Читает и выполняет Lua-файл, возвращая значения, которые вернул чанк.
|
|
fn run_file(lua: &Lua, path: &Path) -> mlua::Result<mlua::MultiValue> {
|
|
let source = std::fs::read_to_string(path).map_err(|err| {
|
|
mlua::Error::RuntimeError(format!(
|
|
"не удалось прочитать сценарий {}: {err}",
|
|
path.display()
|
|
))
|
|
})?;
|
|
|
|
lua.load(&source)
|
|
.set_name(path.to_string_lossy().into_owned())
|
|
.eval()
|
|
}
|