complition

This commit is contained in:
2026-10-30 20:50:04 +03:00
parent 56115302df
commit 55c3955897
15 changed files with 4103 additions and 32 deletions
+277 -3
View File
@@ -10,11 +10,16 @@
use std::cell::{Cell, RefCell};
use std::collections::VecDeque;
use std::path::{Path, PathBuf};
use std::rc::Rc;
use std::sync::Arc;
use serde_json::Number;
use crate::constants::{BOTTOM_PANEL_HEIGHT, LEFT_PANEL_WIDTH, RIGHT_PANEL_WIDTH};
use crate::editor::{Language, Workspace};
use crate::keymap::{KeyBinding, KeyMap};
use crate::lsp::{LspConfig, LspManager, WatcherConfig, document_uri};
use crate::panel::{PanelHandle, PanelState};
thread_local! {
@@ -86,6 +91,8 @@ pub struct AppApi {
right_panel: PanelHandle,
#[lua(skip)]
bottom_panel: PanelHandle,
#[lua(skip)]
lsp: Arc<LspManager>,
}
/// API одной панели окна.
@@ -145,6 +152,7 @@ impl AppApi {
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))),
lsp: Arc::new(LspManager::new()),
};
INSTANCE.with(|slot| *slot.borrow_mut() = Some(api.clone()));
api
@@ -181,6 +189,12 @@ impl AppApi {
self.bottom_panel.clone()
}
/// Менеджер LSP-серверов (для вида и тестов).
#[lua(skip)]
pub fn lsp_handle(&self) -> Arc<LspManager> {
self.lsp.clone()
}
/// Все открытые вкладки в порядке слева направо.
#[allow(dead_code)]
// Часть публичного API приложения: вызывается внешним кодом и тестами.
@@ -236,10 +250,16 @@ impl AppApi {
#[lua(infallible)]
pub fn close_tab(&self, index: usize) -> bool {
let mut workspace = self.workspace.borrow_mut();
if index >= workspace.documents().len() {
let Some(document) = workspace.document(index) else {
return false;
}
};
let uri = document_uri(&document.name);
workspace.close(index);
drop(workspace);
if let Some(uri) = uri {
self.lsp.close_document(&uri);
}
true
}
@@ -254,7 +274,10 @@ impl AppApi {
let doc = workspace.active();
(doc.name.clone(), doc.content.clone())
};
std::fs::write(&path, content)?;
std::fs::write(&path, &content)?;
if let Some(uri) = document_uri(&path) {
self.lsp.did_save(&uri, &content);
}
self.workspace.borrow_mut().mark_active_saved();
Ok(())
}
@@ -311,6 +334,34 @@ impl AppApi {
workspace.active_index()
}
/// Открывает файл с диска на редактирование (Rust-версия `app:open`).
///
/// Если файла нет, создаётся пустая вкладка с таким именем — как в vim.
/// Язык подсветки определяется по расширению, если для него есть грамматика.
#[lua(skip)]
pub fn open(&self, path: impl AsRef<Path>) -> std::io::Result<usize> {
let path = path.as_ref();
let content = match std::fs::read_to_string(path) {
Ok(content) => content,
Err(err) if err.kind() == std::io::ErrorKind::NotFound => String::new(),
Err(err) => return Err(err),
};
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))
}
/// Lua-версия [`AppApi::open`]: `app:open("main.go")`.
///
/// Возвращает номер вкладки (с нуля). Ошибка чтения становится ошибкой 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}")))
}
/// Делает вкладку активной по её номеру в списке [`AppApi::tabs`].
///
/// Возвращает `false`, если вкладки с таким номером нет.
@@ -423,4 +474,227 @@ impl AppApi {
pub fn is_maximized(&self) -> bool {
WINDOW_MAXIMIZED.with(Cell::get)
}
/// Запускает LSP-сервер по описанию (Rust-версия Lua-метода).
#[lua(skip)]
pub fn run_lsp(&self, config: LspConfig) -> Result<(), String> {
self.lsp.start(config)
}
/// Запускает LSP-сервер.
///
/// Пример из Lua:
///
/// ```lua
/// app:run_lsp({
/// name = "gopls",
/// cmd = "/usr/local/bin/gopls",
/// args = { "serve" },
/// env = { GOFLAGS = "-mod=mod" },
/// languages = { "go" },
/// watcher = "**/*.go",
/// initialization_options = { usePlaceholders = true },
/// })
/// ```
///
/// Возвращает `true`, если сервер запущен. Ошибка запуска или таймаут
/// `initialize` превращается в ошибку Lua.
#[lua(name = "run_lsp")]
fn lua_run_lsp(&self, config: mlua::Table) -> mlua::Result<bool> {
let config = parse_lsp_config(&config)?;
self.run_lsp(config)
.map(|()| true)
.map_err(mlua::Error::RuntimeError)
}
/// Имена всех запущенных LSP-серверов.
#[lua(infallible)]
pub fn lsp_servers(&self) -> Vec<String> {
self.lsp.server_names()
}
/// Останавливает LSP-сервер по имени.
///
/// Возвращает `false`, если сервера с таким именем не было.
#[lua(infallible)]
pub fn stop_lsp(&self, name: String) -> bool {
self.lsp.stop(&name)
}
/// Перезапускает LSP-сервер с прежними настройками.
///
/// Возвращает `false`, если сервера не было или новый процесс не поднялся.
#[lua(infallible)]
pub fn restart_lsp(&self, name: String) -> bool {
self.lsp.restart(&name)
}
}
/// Разбирает Lua-таблицу, переданную в `app:run_lsp`.
fn parse_lsp_config(table: &mlua::Table) -> mlua::Result<LspConfig> {
let name: String = table
.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 args = match table.get::<mlua::Table>("args") {
Ok(args) => args
.sequence_values::<String>()
.collect::<mlua::Result<Vec<_>>>()?,
Err(_) => Vec::new(),
};
let env = match table.get::<mlua::Table>("env") {
Ok(env) => env
.pairs::<String, String>()
.collect::<mlua::Result<Vec<_>>>()?,
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)
};
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()?;
Ok(LspConfig {
name,
command,
args,
env,
root,
languages,
watcher: parse_watcher(table)?,
initialization_options,
})
}
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);
};
match value {
mlua::Value::String(pattern) => Ok(Some(WatcherConfig {
patterns: vec![pattern.to_string_lossy()],
ignore: Vec::new(),
})),
mlua::Value::Boolean(true) => Ok(Some(WatcherConfig {
patterns: vec!["**/*".to_owned()],
ignore: Vec::new(),
})),
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();
if patterns.is_empty() {
Ok(None)
} else {
Ok(Some(WatcherConfig { patterns, ignore }))
}
}
_ => Err(mlua::Error::RuntimeError(
"watcher должен быть строкой, таблицей или boolean".to_owned(),
)),
}
}
fn lua_strings(value: mlua::Value) -> mlua::Result<Vec<String>> {
match value {
mlua::Value::String(value) => Ok(vec![value.to_string_lossy()]),
mlua::Value::Table(table) => table
.sequence_values::<String>()
.collect::<mlua::Result<Vec<_>>>(),
mlua::Value::Nil => Ok(Vec::new()),
_ => Err(mlua::Error::RuntimeError(
"ожидалась строка или массив строк".to_owned(),
)),
}
}
fn lua_to_json(value: mlua::Value) -> mlua::Result<serde_json::Value> {
match value {
mlua::Value::Nil => Ok(serde_json::Value::Null),
mlua::Value::Boolean(value) => Ok(serde_json::Value::Bool(value)),
mlua::Value::Integer(value) => Ok(serde_json::Value::Number(value.into())),
mlua::Value::Number(value) => Ok(Number::from_f64(value)
.map(serde_json::Value::Number)
.unwrap_or(serde_json::Value::Null)),
mlua::Value::String(value) => Ok(serde_json::Value::String(value.to_string_lossy())),
mlua::Value::Table(table) => {
let length = table.raw_len();
if length > 0 {
let mut items = Vec::with_capacity(length);
for value in table.sequence_values::<mlua::Value>() {
items.push(lua_to_json(value?)?);
}
Ok(serde_json::Value::Array(items))
} else {
let mut map = serde_json::Map::new();
for pair in table.pairs::<mlua::Value, mlua::Value>() {
let (key, value) = pair?;
let key = match key {
mlua::Value::String(key) => key.to_string_lossy(),
mlua::Value::Integer(key) => key.to_string(),
mlua::Value::Number(key) => key.to_string(),
_ => continue,
};
map.insert(key, lua_to_json(value)?);
}
Ok(serde_json::Value::Object(map))
}
}
_ => Ok(serde_json::Value::Null),
}
}