354 lines
16 KiB
Markdown
354 lines
16 KiB
Markdown
# Lua API
|
||
|
||
Приложение выполняет `init.lua` при запуске и предоставляет сценариям объект `app`
|
||
для управления окном, вкладками, панелями, файловой системой, подсветкой и
|
||
LSP-серверами.
|
||
|
||
## Запуск и каталог сценариев
|
||
|
||
Каталог поиска `init.lua` задаётся константой `SCRIPT_DIR` в `src/constants.rs`
|
||
(по умолчанию — текущий каталог). Его можно переопределить аргументом командной строки:
|
||
|
||
```sh
|
||
gpui-editor-test --config /path/to/project main.go
|
||
cargo run -- --config ./config
|
||
cargo run -- main.go
|
||
```
|
||
|
||
- В каталоге `--config` должны лежать `init.lua` и подключаемые им модули.
|
||
- Первый позиционный аргумент открывается после выполнения `init.lua`
|
||
через `app:open(path)` — как `vim main.go` или `nano main.go`.
|
||
|
||
## Объект `app`
|
||
|
||
Номера вкладок во всех методах нумеруются с нуля.
|
||
|
||
### Вкладки
|
||
|
||
| Метод | Описание |
|
||
| --- | --- |
|
||
| `app:tabs()` | Список вкладок: `{{ index = 0, title = "main.rs", icon = "file" }, ...}` |
|
||
| `app:active_tab()` | Индекс активной вкладки |
|
||
| `app:set_active_tab(index)` | Сделать вкладку активной; `true`, если вкладка существует |
|
||
| `app:next_tab()` | Переключить на следующую вкладку, вернуть индекс |
|
||
| `app:previous_tab()` | Переключить на предыдущую вкладку, вернуть индекс |
|
||
| `app:open_tab()` | Открыть пустую вкладку, вернуть индекс |
|
||
| `app:open(path)` | Открыть файл с диска, вернуть индекс вкладки. Если файла нет — открывается пустой буфер с таким именем |
|
||
| `app:close_tab(index)` | Закрыть вкладку; `true`, если вкладка закрыта |
|
||
| `app:set_tab_icon(index, name)` | Назначить иконку вкладке; `false`, если вкладки нет или иконка неизвестна |
|
||
| `app:clear_tab_icon(index)` | Вернуть иконку по умолчанию (`file`) |
|
||
| `app:tab_icon(index)` | Имя иконки вкладки или `nil`, если вкладки нет |
|
||
|
||
### Иконки вкладок
|
||
|
||
Слева от названия вкладки показывается иконка; по умолчанию это `file`.
|
||
Имя — название иконки [Lucide](https://lucide.dev/icons/), регистр не важен.
|
||
Например:
|
||
|
||
```lua
|
||
app:set_tab_icon(0, "file-code") -- main.rs
|
||
app:set_tab_icon(1, "book-open") -- notes.md
|
||
app:clear_tab_icon(1) -- снова "file"
|
||
```
|
||
|
||
Подчёркивания и пробелы считаются дефисом, а `icons/` и `.svg` можно
|
||
не писать: `file_code`, `file-code` и `icons/file-code.svg` равнозначны.
|
||
Неизвестное имя не назначается — метод вернёт `false`.
|
||
|
||
### Текст
|
||
|
||
| Метод | Описание |
|
||
| --- | --- |
|
||
| `app:active_text()` | Текст активной вкладки |
|
||
| `app:set_active_text(text)` | Заменить текст активной вкладки |
|
||
|
||
### Подсветка синтаксиса
|
||
|
||
Подсветка по умолчанию выключена. Поддерживаемые языки: `rust`, `go`, `js`, `json`,
|
||
`python`.
|
||
|
||
| Метод | Описание |
|
||
| --- | --- |
|
||
| `app:set_language(index, "rust")` | Включить подсветку для вкладки; `false`, если вкладки нет или язык не поддерживается |
|
||
| `app:language(index)` | Язык подсветки вкладки или `nil` |
|
||
| `app:clear_language(index)` | Выключить подсветку для вкладки |
|
||
|
||
```lua
|
||
app:set_language(0, "rust")
|
||
```
|
||
|
||
### Сохранение
|
||
|
||
| Метод | Описание |
|
||
| --- | --- |
|
||
| `app:save()` | Сохранить активную вкладку в её файл |
|
||
|
||
### Файловая система
|
||
|
||
| Метод | Описание |
|
||
| --- | --- |
|
||
| `app:list_dir(path)` | Содержимое каталога: `{{ id = "src/main.rs", name = "main.rs", is_dir = false }, ...}` |
|
||
|
||
Путь отсчитывается от текущего рабочего каталога процесса. Пустая строка или
|
||
`"."` означают сам текущий каталог. Поле `id` — путь элемента относительно
|
||
текущего каталога: его можно передать в `app:open(id)` и использовать как
|
||
идентификатор узла дерева. Элементы уже отсортированы по имени без учёта
|
||
регистра.
|
||
|
||
```lua
|
||
for _, entry in ipairs(app:list_dir("src")) do
|
||
print(entry.name, entry.is_dir)
|
||
end
|
||
```
|
||
|
||
### Привязка клавиш
|
||
|
||
`app:map_key(keys, fn)` назначает обработчик на нажатие или отпускание клавиши
|
||
(синтаксис в духе nvim):
|
||
|
||
| Запись | Значение |
|
||
| --- | --- |
|
||
| `"a"` | нажатие A |
|
||
| `"1"` | нажатие 1 |
|
||
| `"!a"` | отпускание A |
|
||
| `"<S-c>"` | C при зажатом Shift |
|
||
| `"<C-z>"` | Z при зажатом Ctrl |
|
||
| `"<A-!r>"` | отпускание R при зажатом Alt |
|
||
|
||
Клавиша сравнивается с именем клавиши от GPUI. Caps Lock не важен. На Linux
|
||
и Windows буквы берутся по аппаратному keycode, поэтому `<C-q>` работает
|
||
и на русской раскладке.
|
||
|
||
```lua
|
||
app:map_key("<C-s>", function() app:save() end)
|
||
app:map_key("<!Esc>", function() app:close() end)
|
||
app:map_key("<C-q>", function() app:close() end)
|
||
app:map_key("<C-e>", function() app.right_panel:toggle() end)
|
||
app:map_key("<C-t>", function() app.bottom_panel:toggle() end)
|
||
```
|
||
|
||
### Окно
|
||
|
||
| Метод | Описание |
|
||
| --- | --- |
|
||
| `app:close()` | Закрыть окно |
|
||
| `app:toggle_maximize()` | Развернуть окно / вернуть прежний размер |
|
||
| `app:set_maximized(true/false)` | Развернуть (`true`) или восстановить (`false`) окно |
|
||
| `app:minimize()` | Свернуть окно |
|
||
| `app:is_maximized()` | Развёрнуто ли окно прямо сейчас |
|
||
|
||
## LSP-серверы
|
||
|
||
Запуск серверов целиком настраивается Lua-сценарием.
|
||
|
||
```lua
|
||
app:run_lsp({
|
||
name = "gopls", -- условное имя для stop/restart
|
||
cmd = "gopls", -- путь к бинарнику или имя в PATH
|
||
args = { "serve" }, -- аргументы процесса
|
||
env = { GOFLAGS = "-mod=mod" }, -- переменные окружения
|
||
languages = { "go" }, -- какие languageId обслуживает
|
||
watcher = "**/*.go", -- шаблон файлового вотчера
|
||
root = ".", -- корень workspace (по умолчанию cwd)
|
||
initialization_options = {}, -- initializationOptions сервера
|
||
})
|
||
```
|
||
|
||
| Метод | Описание |
|
||
| --- | --- |
|
||
| `app:run_lsp(config)` | Запустить сервер по описанию |
|
||
| `app:lsp_servers()` | Имена запущенных серверов: `{ "gopls", ... }` |
|
||
| `app:stop_lsp("gopls")` | Остановить сервер; `true`, если сервер был запущен |
|
||
| `app:restart_lsp("gopls")` | Перезапустить сервер с прежними настройками |
|
||
|
||
Поле `watcher` принимает строку или таблицу:
|
||
|
||
```lua
|
||
watcher = "**/*.go"
|
||
watcher = { pattern = "**/*.go", ignore = { "vendor/**" } }
|
||
```
|
||
|
||
Автодополнение включается само при вводе идентификатора; `Ctrl+Space` открывает
|
||
список вручную. Пока список открыт, `↑`/`↓` и `PageUp`/`PageDown` перемещают
|
||
выделение (курсор при этом стоит на месте), `Enter` или `Tab` вставляют выбранный
|
||
элемент, `Esc` закрывает список. Горизонтальные стрелки, `Home` и `End` закрывают
|
||
список и работают с курсором как обычно. Документация выбранного элемента
|
||
показывается второй всплывающей панелью справа от списка.
|
||
|
||
Примеры запуска:
|
||
|
||
```lua
|
||
app:run_lsp({ name = "gopls", cmd = "gopls", args = { "serve" },
|
||
languages = { "go" }, watcher = "**/*.go" })
|
||
|
||
app:run_lsp({ name = "rust-analyzer", cmd = "rust-analyzer",
|
||
languages = { "rust" }, watcher = "**/*.rs" })
|
||
```
|
||
|
||
Пример из `init.lua` с защитой от ошибки запуска:
|
||
|
||
```lua
|
||
local gopls_ok, gopls_err = pcall(function()
|
||
app:run_lsp({
|
||
name = "gopls",
|
||
cmd = "gopls",
|
||
args = { "serve" },
|
||
root = ".",
|
||
languages = { "go" },
|
||
watcher = { pattern = "**/*.go", ignore = { "vendor/**" } },
|
||
initialization_options = {
|
||
usePlaceholders = true,
|
||
completeUnimported = true,
|
||
},
|
||
})
|
||
end)
|
||
if not gopls_ok then
|
||
print("[lsp] gopls не запущен: " .. tostring(gopls_err))
|
||
end
|
||
```
|
||
|
||
> `--config` меняет только каталог `init.lua`, поэтому `root = "."` указывает
|
||
> на текущий каталог запуска — проект должен быть текущим рабочим каталогом.
|
||
|
||
## Панели окна
|
||
|
||
По умолчанию панели закрыты, размер меняется перетаскиванием.
|
||
Для `app.left_panel`, `app.right_panel` и `app.bottom_panel` доступны одинаковые методы:
|
||
|
||
| Метод | Описание |
|
||
| --- | --- |
|
||
| `app.left_panel:open()` | Открыть панель |
|
||
| `app.left_panel:close()` | Закрыть панель |
|
||
| `app.left_panel:toggle()` | Переключить панель; вернуть `true`, если панель открыта |
|
||
| `app.left_panel:is_open()` | Открыта ли панель |
|
||
|
||
```lua
|
||
app.left_panel:open()
|
||
app.right_panel:toggle()
|
||
app.bottom_panel:close()
|
||
```
|
||
|
||
### Блоки панелей
|
||
|
||
Панель может содержать блоки. Блок создаётся через API панели и живёт вместе
|
||
с ней; сейчас поддерживается один вид блока — абстрактное дерево.
|
||
|
||
```lua
|
||
local tree = app.left_panel:add_tree()
|
||
```
|
||
|
||
Редактор отвечает только за отрисовку дерева. Что означает каждый узел,
|
||
решает сценарий: это могут быть файлы проекта, задачи, настройки — что угодно.
|
||
Файловое дерево проекта в `init.lua` собирается из `app:list_dir`, но сам
|
||
редактор о файловой системе в блоке ничего не знает.
|
||
|
||
| Метод | Описание |
|
||
| --- | --- |
|
||
| `tree:append(nodes, parent)` | Добавить узлы к `parent`; `parent = ""` — корень |
|
||
| `tree:remove(id)` | Удалить узел и всех потомков рекурсивно; `false`, если узла нет |
|
||
| `tree:on_toggle(fn)` | Обработчик раскрытия: `fn(expanded, id, loaded)` |
|
||
| `tree:on_click(fn)` | Обработчик клика по конечной ноде: `fn(id)` |
|
||
| `tree:selected()` | Идентификатор выбранной строки или `nil` |
|
||
| `tree:focus(id)` | Раскрыть всех родителей, выбрать узел и передать фокус дереву |
|
||
|
||
Узел — таблица:
|
||
|
||
```lua
|
||
{
|
||
id = "src/main.rs", -- строковый идентификатор (путь файла)
|
||
label = "main.rs", -- отображаемый текст
|
||
icon = "file-code", -- имя иконки Lucide (необязательно)
|
||
expandable = false, -- раскрываемая ли нода (папка)
|
||
expanded = false, -- раскрыта ли она сейчас
|
||
}
|
||
```
|
||
|
||
`append` добавляет узлы, а не заменяет дерево. Повторная передача того же `id`
|
||
обновляет описание узла, но не создаёт копию.
|
||
|
||
Дети каждого уровня сортируются по `label` без учёта регистра. Сначала идут
|
||
раскрываемые узлы (`expandable = true`), затем конечные; внутри каждой группы —
|
||
по алфавиту.
|
||
|
||
В обработчик `on_toggle` приходит `loaded = true`, если дочерние узлы уже
|
||
добавляли в эту ноду; при первом раскрытии — `false`.
|
||
|
||
Клик по строке выбирает её. Выбранную строку можно перемещать стрелками
|
||
`↑`/`↓`; `↑`/`↓` двигают выбор, `→` раскрывает папку или переходит к первому
|
||
ребёнку, `←` сворачивает папку или выбирает родителя, `Enter` раскрывает папку
|
||
или вызывает `on_click` для конечной ноды. Выбор сохраняется, пока панель
|
||
закрыта; при повторном открытии панели фокус возвращается на выбранную строку.
|
||
|
||
Пример отложенной загрузки дерева:
|
||
|
||
```lua
|
||
local tree = app.left_panel:add_tree()
|
||
|
||
local function load_dir(path)
|
||
local nodes = {}
|
||
for _, entry in ipairs(app:list_dir(path)) do
|
||
nodes[#nodes + 1] = {
|
||
id = entry.id,
|
||
label = entry.name,
|
||
icon = entry.is_dir and "folder" or "file",
|
||
expandable = entry.is_dir,
|
||
expanded = false,
|
||
}
|
||
end
|
||
tree:append(nodes, path)
|
||
end
|
||
|
||
tree:on_toggle(function(expanded, id, loaded)
|
||
if expanded and not loaded then
|
||
load_dir(id)
|
||
end
|
||
end)
|
||
|
||
tree:on_click(function(id)
|
||
app:open(id)
|
||
end)
|
||
|
||
app.left_panel:open()
|
||
load_dir("")
|
||
|
||
-- При открытии панели вернуть фокус на последнюю выбранную строку.
|
||
app:map_key("<C-b>", function()
|
||
if app.left_panel:toggle() then
|
||
local selected = tree:selected()
|
||
if selected then
|
||
tree:focus(selected)
|
||
end
|
||
end
|
||
end)
|
||
```
|
||
|
||
## Подключение других Lua-файлов
|
||
|
||
Файлы подключаются из каталога сценариев:
|
||
|
||
| Способ | Описание |
|
||
| --- | --- |
|
||
| `include("tools.lua")` | Выполнить файл сразу |
|
||
| `require("tools")` | Выполнить один раз (как обычный Lua-модуль) |
|
||
|
||
## Добавление новых методов
|
||
|
||
Методы добавляются в `src/api.rs`: всё, что есть в `impl AppApi`, помеченном
|
||
`#[mlua::userdata_impl]`, автоматически доступно из Lua.
|
||
Чтобы не экспортировать метод, его помечают `#[lua(skip)]`.
|
||
|
||
## Полный пример `init.lua`
|
||
|
||
```lua
|
||
print("[lua] init.lua выполнен")
|
||
|
||
app:set_language(0, "rust")
|
||
app:next_tab()
|
||
|
||
app:map_key("<C-q>", function() app:close() end)
|
||
app:map_key("<C-e>", function() app.right_panel:toggle() end)
|
||
app:map_key("<C-t>", function() app.bottom_panel:toggle() end)
|
||
|
||
app:open_tab()
|
||
```
|