complition
This commit is contained in:
+277
-3
@@ -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),
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user