689 lines
34 KiB
Markdown
689 lines
34 KiB
Markdown
# Lua API
|
||
|
||
Приложение выполняет `init.lua` при запуске и предоставляет сценариям объект
|
||
`app` для управления окном, панелями и глобальными привязками клавиш.
|
||
Вкладки, текст, gutter, подсветка, сохранение и LSP-серверы живут в `app.editor`,
|
||
а блоки панелей (деревья) — в своих объектах.
|
||
|
||
Сам редактор не содержит демонстрационных документов: при запуске открыта
|
||
одна пустая вкладка `untitled-1.txt`. Всё остальное — файлы, деревья, серверы —
|
||
настраивается сценарием.
|
||
|
||
## Запуск и каталог сценариев
|
||
|
||
Каталог поиска `init.lua` задаётся константой `SCRIPT_DIR` в `src/constants.rs`
|
||
(по умолчанию — текущий каталог). Его можно переопределить аргументом командной строки:
|
||
|
||
```sh
|
||
qode --config /path/to/project main.go
|
||
cargo run -- --config ./config
|
||
cargo run -- main.go
|
||
```
|
||
|
||
- В каталоге `--config` должны лежать `init.lua` и подключаемые им модули.
|
||
- Первый позиционный аргумент открывается после выполнения `init.lua`
|
||
через `app.editor:open(path)` — как `vim main.go` или `nano main.go`.
|
||
- `--config` меняет только каталог сценариев. Рабочий каталог процесса, а
|
||
вместе с ним пути `app.editor:open`, `app.editor:save` и `root` у LSP не меняются.
|
||
|
||
## Объект `app`
|
||
|
||
Объект `app` управляет окном и панелями. Всё, что относится к самому
|
||
редактору — вкладки, текст, gutter, сохранение, LSP и привязки клавиш
|
||
редактора, — живёт в `app.editor`:
|
||
|
||
```lua
|
||
app.editor:open("src/main.rs")
|
||
app.editor:save()
|
||
app.editor:map_key("n", "<C-s>", "save")
|
||
```
|
||
|
||
Номера вкладок во всех методах нумеруются с нуля. Массив, который возвращает
|
||
`app.editor:tabs()`, остаётся обычной Lua-таблицей с индексами от 1, поэтому поле
|
||
`index` — единственный надёжный способ сослаться на вкладку.
|
||
|
||
### Вкладки
|
||
|
||
| Метод | Описание |
|
||
| --- | --- |
|
||
| `app.editor:tabs()` | Список вкладок: `{{ index = 0, title = "main.rs", icon = "file" }, ...}` |
|
||
| `app.editor:active_tab()` | Индекс активной вкладки |
|
||
| `app.editor:set_active_tab(index)` | Сделать вкладку активной; `true`, если вкладка существует |
|
||
| `app.editor:next_tab()` | Переключить на следующую вкладку, вернуть индекс |
|
||
| `app.editor:previous_tab()` | Переключить на предыдущую вкладку, вернуть индекс |
|
||
| `app.editor:open_tab()` | Открыть пустую вкладку, вернуть индекс |
|
||
| `app.editor:open(path, position)` | Открыть файл с диска, вернуть индекс вкладки. Если файла нет — открывается пустой буфер с таким именем. Без второго аргумента фокус не меняется; `{}` — перевести фокус в редактор, оставив курсор; `{line, column}` — ещё и поставить курсор (с нуля) |
|
||
| `app.editor:close_tab(index)` | Закрыть вкладку; `true`, если вкладка закрыта |
|
||
| `app.editor:set_tab_icon(index, name)` | Назначить иконку вкладке; `false`, если вкладки нет или иконка неизвестна |
|
||
| `app.editor:clear_tab_icon(index)` | Вернуть иконку по умолчанию (`file`) |
|
||
| `app.editor:tab_icon(index)` | Имя иконки вкладки или `nil`, если вкладки нет |
|
||
|
||
```lua
|
||
app.editor:open("src/main.rs") -- открыть, не трогая фокус
|
||
app.editor:open("src/main.rs", {}) -- фокус в редактор, курсор на месте
|
||
app.editor:open("src/main.rs", {4, 2}) -- фокус в редактор, курсор на строку 4, столбец 2
|
||
```
|
||
|
||
Строка и столбец нумеруются с нуля, как в диагностике LSP; столбец считается
|
||
в UTF-16.
|
||
|
||
### Иконки вкладок
|
||
|
||
Слева от названия вкладки показывается иконка; по умолчанию это `file`.
|
||
Имя — название иконки [Lucide](https://lucide.dev/icons/), регистр не важен.
|
||
Например:
|
||
|
||
```lua
|
||
app.editor:set_tab_icon(0, "file-code")
|
||
app.editor:set_tab_icon(1, "book-open")
|
||
app.editor:clear_tab_icon(1) -- снова "file"
|
||
```
|
||
|
||
Подчёркивания и пробелы считаются дефисом, а `icons/` и `.svg` можно
|
||
не писать: `file_code`, `file-code` и `icons/file-code.svg` равнозначны.
|
||
Неизвестное имя не назначается — метод вернёт `false`.
|
||
|
||
### Иконки на полях редактора (gutter)
|
||
|
||
Слева от редактора есть узкая колонка с иконками. Методы действуют на
|
||
активную вкладку, а номера строк нумеруются с нуля, как в LSP. На одной строке
|
||
живёт одна иконка; повторный вызов заменяет прежнюю.
|
||
|
||
| Метод | Описание |
|
||
| --- | --- |
|
||
| `app.editor:set_gutter_icon(line, icon, color)` | Поставить иконку на строку; `color` необязателен (`#RGB`, `#RGBA`, `#RRGGBB`, `#RRGGBBAA`). Без цвета иконка рисуется цветом темы. `false` — иконка или цвет неизвестны |
|
||
| `app.editor:gutter_icon(line)` | Имя иконки на строке или `nil` |
|
||
| `app.editor:gutter_color(line)` | Цвет иконки или `nil`, если цвет не задан |
|
||
| `app.editor:clear_gutter_icon(line)` | Убрать иконку со строки; `false`, если её не было |
|
||
| `app.editor:clear_gutter()` | Убрать все иконки активной вкладки |
|
||
|
||
```lua
|
||
app.editor:set_gutter_icon(4, "circle", "#e5484d") -- красная точка на 5-й строке
|
||
app.editor:set_gutter_icon(9, "play") -- цвет темы
|
||
app.editor:clear_gutter_icon(9)
|
||
```
|
||
|
||
Иконки привязаны к номерам строк и автоматически не сдвигаются при правке
|
||
текста: сценарий сам решает, когда их переставить. При прокрутке колонка
|
||
двигается вместе с редактором.
|
||
|
||
### Текст
|
||
|
||
| Метод | Описание |
|
||
| --- | --- |
|
||
| `app.editor:active_text()` | Текст активной вкладки |
|
||
| `app.editor:set_active_text(text)` | Заменить текст активной вкладки |
|
||
|
||
### Подсветка синтаксиса
|
||
|
||
Подсветка по умолчанию выключена. Поддерживаемые языки: `rust`, `go`, `js`, `json`,
|
||
`python`.
|
||
|
||
| Метод | Описание |
|
||
| --- | --- |
|
||
| `app.editor:set_language(index, "rust")` | Включить подсветку для вкладки; `false`, если вкладки нет или язык не поддерживается |
|
||
| `app.editor:language(index)` | Язык подсветки вкладки или `nil` |
|
||
| `app.editor:clear_language(index)` | Выключить подсветку для вкладки |
|
||
|
||
```lua
|
||
app.editor:set_language(0, "rust")
|
||
```
|
||
|
||
### Сохранение
|
||
|
||
| Метод | Описание |
|
||
| --- | --- |
|
||
| `app.editor:save()` | Сохранить активную вкладку в её файл |
|
||
|
||
### Файловая система
|
||
|
||
| Метод | Описание |
|
||
| --- | --- |
|
||
| `app:list_dir(path)` | Содержимое каталога: `{{ id = "src/main.rs", name = "main.rs", is_dir = false }, ...}` |
|
||
|
||
Путь отсчитывается от текущего рабочего каталога процесса. Пустая строка или
|
||
`"."` означают сам текущий каталог. Поле `id` — путь элемента относительно
|
||
текущего каталога: его можно передать в `app.editor:open(id)` и использовать как
|
||
идентификатор узла дерева. Элементы уже отсортированы по имени без учёта
|
||
регистра.
|
||
|
||
```lua
|
||
for _, entry in ipairs(app:list_dir("src")) do
|
||
print(entry.name, entry.is_dir)
|
||
end
|
||
```
|
||
|
||
### Привязка клавиш
|
||
|
||
Привязки живут у трёх видов объектов: у приложения (`app`), у редактора
|
||
(`app.editor`) и у каждого блока панели (`tree`). Встроенные сочетания можно
|
||
заменить или удалить: обработчиком принимается Lua-функция, имя встроенной
|
||
команды или `nil`.
|
||
|
||
```lua
|
||
app:map_key(modes, keys, handler) -- глобальные
|
||
app.editor:map_key(modes, keys, handler) -- только когда фокус в редакторе
|
||
app:map_completion_key(keys, handler) -- список автодополнения
|
||
```
|
||
|
||
Слой автодополнения режимами не управляется: `map_completion_key` принимает
|
||
только клавишу и обработчик.
|
||
|
||
Первым аргументом `map_key` идёт строка режимов. Порядок обработки всегда
|
||
такой: сначала глобальные привязки, затем — если фокус в редакторе — привязки
|
||
редактора, а если фокус на блоке панели — привязки этого блока. Привязки
|
||
неактивного объекта не срабатывают.
|
||
|
||
Запись клавиши в духе nvim:
|
||
|
||
| Запись | Значение |
|
||
| --- | --- |
|
||
| `"a"` | нажатие A |
|
||
| `"1"` | нажатие 1 |
|
||
| `"!a"` | отпускание A |
|
||
| `"<S-c>"` | C при зажатом Shift |
|
||
| `"<C-z>"` | Z при зажатом Ctrl |
|
||
| `"<A-!r>"` | отпускание R при зажатом Alt |
|
||
| `"<D-s>"` | S при зажатом Cmd на macOS или Super на Linux |
|
||
|
||
Клавиша сравнивается с именем клавиши от GPUI. Caps Lock не важен. На Linux
|
||
и Windows буквы берутся по аппаратному keycode, поэтому `<C-q>` работает
|
||
и на русской раскладке.
|
||
|
||
#### Режимы
|
||
|
||
У каждого объекта своя текущая режимная метка: один символ (`n`, `i`, `v`, `r`
|
||
или любой другой) либо `-` — режим по умолчанию. Режимы динамические: заранее
|
||
создавать их не нужно, достаточно переключить метку.
|
||
|
||
| Значение `modes` | Когда срабатывает |
|
||
| --- | --- |
|
||
| `""` | во всех режимах |
|
||
| `"-"` | только в режиме по умолчанию |
|
||
| `"niv"` | в режимах `n`, `i`, `v` |
|
||
| `"-iv"` | в режиме по умолчанию, а также `i` и `v` |
|
||
|
||
Текущий режим меняется методами `set_mode(mode)` и читается методом `mode()`.
|
||
Режимы у приложения, редактора и каждого блока независимы.
|
||
|
||
```lua
|
||
app:set_mode("n")
|
||
app.editor:set_mode("i")
|
||
|
||
app:map_key("n", "<C-n>", "next_tab") -- только в режиме n
|
||
app.editor:map_key("i", "<C-s>", "save") -- только в режиме i редактора
|
||
app.editor:map_key("", "<C-space>", function() end) -- в любом режиме
|
||
```
|
||
|
||
Встроенные команды:
|
||
|
||
| Имя | Действие |
|
||
| --- | --- |
|
||
| `"save"` | Сохранить активную вкладку |
|
||
| `"next_tab"` | Следующая вкладка |
|
||
| `"previous_tab"` | Предыдущая вкладка |
|
||
| `"open_tab"` | Новая пустая вкладка |
|
||
| `"close_tab"` | Закрыть активную вкладку |
|
||
| `"toggle_left_panel"` | Переключить левую панель |
|
||
| `"toggle_right_panel"` | Переключить правую панель |
|
||
| `"toggle_bottom_panel"` | Переключить нижнюю панель |
|
||
|
||
Встроенные команды списка автодополнения:
|
||
|
||
| Имя | Действие |
|
||
| --- | --- |
|
||
| `"completion_trigger"` | Открыть список |
|
||
| `"completion_next"` / `"completion_previous"` | Сдвинуть выделение |
|
||
| `"completion_page_down"` / `"completion_page_up"` | Пролистать список |
|
||
| `"completion_accept"` | Вставить выбранный элемент |
|
||
| `"completion_cancel"` | Закрыть список |
|
||
| `"completion_dismiss"` | Закрыть список и передать клавишу редактору |
|
||
|
||
При запуске уже назначены:
|
||
|
||
- `Ctrl/Cmd+S` → `save` во всех режимах;
|
||
- `Ctrl/Cmd+Space` → `completion_trigger`;
|
||
- `Enter`/`Tab`, `Esc`, `↑`/`↓`, `PageUp`/`PageDown`, `←`/`→`, `Home`/`End`
|
||
→ команды слоя автодополнения.
|
||
|
||
```lua
|
||
-- Переопределить сохранение своей функцией во всех режимах.
|
||
app:map_key("", "<C-s>", function() app.editor:save() end)
|
||
|
||
-- Убрать встроенную привязку.
|
||
app:map_key("", "<C-s>", nil)
|
||
|
||
-- Назначить встроенную команду на другую клавишу.
|
||
app:map_key("", "<C-n>", "next_tab")
|
||
app:map_key("", "<C-b>", "toggle_left_panel")
|
||
|
||
-- Навигация по списку автодополнения на Ctrl+N / Ctrl+P.
|
||
app:map_completion_key("<C-n>", "completion_next")
|
||
app:map_completion_key("<C-p>", "completion_previous")
|
||
|
||
-- Привязка, которая срабатывает только при фокусе в редакторе.
|
||
app.editor:map_key("", "<A-r>", function()
|
||
app.editor:set_active_text("editor mode")
|
||
end)
|
||
|
||
-- Привязка блока дерева: работает, когда фокус на этом дереве.
|
||
tree:map_key("v", "<A-f>", function() print("tree v") end)
|
||
```
|
||
|
||
Пока список открыт, его слой проверяется раньше действий редактора. Сочетания
|
||
с модификаторами и горизонтальные стрелки закрывают список, даже если для них
|
||
нет своей привязки, — так `Shift+↑` продолжает выделять текст.
|
||
|
||
### Окно
|
||
|
||
| Метод | Описание |
|
||
| --- | --- |
|
||
| `app:close()` | Закрыть окно |
|
||
| `app:toggle_maximize()` | Развернуть окно / вернуть прежний размер |
|
||
| `app:set_maximized(true/false)` | Развернуть (`true`) или восстановить (`false`) окно |
|
||
| `app:minimize()` | Свернуть окно |
|
||
| `app:is_maximized()` | Развёрнуто ли окно прямо сейчас |
|
||
|
||
## LSP-серверы
|
||
|
||
Запуск серверов целиком настраивается Lua-сценарием.
|
||
|
||
```lua
|
||
app.editor: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.editor:run_lsp(config)` | Запустить сервер по описанию |
|
||
| `app.editor:lsp_servers()` | Имена запущенных серверов: `{ "gopls", ... }` |
|
||
| `app.editor:stop_lsp("gopls")` | Остановить сервер; `true`, если сервер был запущен |
|
||
| `app.editor:restart_lsp("gopls")` | Перезапустить сервер с прежними настройками |
|
||
| `app.editor:on_lsp_diagnostics(delay_ms, fn)` | Обработчик диагностики всех серверов; `nil` вместо `fn` удаляет его |
|
||
|
||
`cmd` обязателен. Если `root` не задан или относителен, он отсчитывается от
|
||
текущего рабочего каталога процесса. Поле `watcher` принимает строку, таблицу
|
||
или boolean:
|
||
|
||
```lua
|
||
watcher = "**/*.go"
|
||
watcher = { pattern = "**/*.go", ignore = { "vendor/**" } }
|
||
watcher = true -- все файлы, игнорируя только стандартные каталоги сборки
|
||
```
|
||
|
||
Автодополнение включается само при вводе идентификатора; `Ctrl/Cmd+Space`
|
||
открывает список вручную. Поведение клавиш описано в разделе «Привязка
|
||
клавиш» и может быть переопределено через `app:map_completion_key`.
|
||
Документация выбранного элемента показывается второй всплывающей панелью
|
||
справа от списка.
|
||
|
||
Диагностику, которую сервер присылает после правок, редактор подчёркивает
|
||
волнистой линией: ошибки — красной, предупреждения и остальные сообщения —
|
||
жёлтой. Подчёркивание работает и при выключенной подсветке синтаксиса.
|
||
|
||
### Обработчик диагностики
|
||
|
||
`app.editor:on_lsp_diagnostics(delay_ms, fn)` регистрирует один обработчик на все
|
||
LSP-серверы. `delay_ms` — пауза тишины: если сообщения приходят чаще, вызовы
|
||
пропускаются, а обработчик получает последний снимок после того, как
|
||
обновления прекратились. Если зарегистрировать обработчик, когда диагностика
|
||
уже накоплена, он сработает сразу после этой паузы.
|
||
|
||
```lua
|
||
app.editor:on_lsp_diagnostics(1000, function(servers)
|
||
for _, entry in ipairs(servers) do
|
||
print(entry.server, entry.path, #entry.diagnostics)
|
||
end
|
||
end)
|
||
```
|
||
|
||
`servers` — массив записей по одной на пару «документ, сервер»:
|
||
|
||
```lua
|
||
{
|
||
server = "gopls", -- имя сервера, приславшего диагностику
|
||
uri = "file:///project/main.go", -- URI документа
|
||
path = "/project/main.go", -- путь файла; nil для не-file URI
|
||
diagnostics = {
|
||
{
|
||
line = 0, character = 2, -- начало (с нуля, как в LSP)
|
||
end_line = 0, end_character = 8,
|
||
severity = 1, -- 1 ошибка, 2 предупреждение, 3 info, 4 hint
|
||
severity_name = "error", -- error | warning | information | hint
|
||
message = "undefined: foo",
|
||
source = "gopls",
|
||
code = "UndeclaredName",
|
||
tags = {}, -- unnecessary, deprecated
|
||
},
|
||
},
|
||
}
|
||
```
|
||
|
||
Позиции нумеруются с нуля; для отображения прибавляйте единицу к `line` и
|
||
`character`. Остановка сервера удаляет его сообщения из следующих снимков.
|
||
|
||
Примеры запуска:
|
||
|
||
```lua
|
||
app.editor:run_lsp({ name = "gopls", cmd = "gopls", args = { "serve" },
|
||
languages = { "go" }, watcher = "**/*.go" })
|
||
|
||
app.editor:run_lsp({ name = "rust-analyzer", cmd = "rust-analyzer",
|
||
languages = { "rust" }, watcher = "**/*.rs" })
|
||
```
|
||
|
||
Пример из `init.lua` с защитой от ошибки запуска:
|
||
|
||
```lua
|
||
local gopls_ok, gopls_err = pcall(function()
|
||
app.editor: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.toolbar:add_label(text, icon, background, id, side)` | Добавить лейбл |
|
||
| `app.toolbar:add_button(text, icon, background, id, side, fn)` | Добавить кнопку |
|
||
| `app.toolbar:hide(id)` | Скрыть элемент; `false`, если элемента с таким `id` нет |
|
||
| `app.toolbar:show(id)` | Снова показать скрытый элемент |
|
||
| `app.toolbar:clear()` | Удалить все элементы панели |
|
||
|
||
- `icon` — имя иконки Lucide (`play`, `settings`, `file`, ...) или `nil`.
|
||
- `background` — `#RGB`, `#RGBA`, `#RRGGBB`, `#RRGGBBAA` или `nil` (прозрачный
|
||
фон). Цвет текста подбирается автоматически: тёмный на светлом фоне, светлый
|
||
на тёмном.
|
||
- `id` — идентификатор элемента для `hide` и `show`. Повторное добавление с тем
|
||
же `id` заменяет элемент, а не создаёт дубликат.
|
||
- `side` — `"left"` или `"right"`.
|
||
- `fn` — обработчик нажатия; вызывается без аргументов.
|
||
|
||
```lua
|
||
app.toolbar:add_label("Готово", nil, nil, "status", "left")
|
||
app.toolbar:add_label("UTF-8", nil, "#3f3f46", "encoding", "right")
|
||
app.toolbar:add_button("Запустить", "play", nil, "run", "right", function()
|
||
print("Запуск")
|
||
end)
|
||
|
||
app.toolbar:hide("run")
|
||
app.toolbar:show("run")
|
||
```
|
||
|
||
## Панели окна
|
||
|
||
По умолчанию панели закрыты, размер меняется перетаскиванием.
|
||
Для `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()
|
||
```
|
||
|
||
Если у правой панели нет блоков, она показывает состояние LSP-серверов.
|
||
Блоки нижней панели показываются вкладками, как вкладки редактора; боковые
|
||
панели рисуют все свои блоки подряд и заголовки не показывают.
|
||
|
||
### Блоки панелей
|
||
|
||
Панель может содержать блоки. Блок создаётся через API панели и живёт вместе
|
||
с ней; сейчас поддерживается один вид блока — абстрактное дерево.
|
||
|
||
```lua
|
||
local tree = app.left_panel:add_tree()
|
||
local diagnostics = app.bottom_panel:add_tree("Диагностика")
|
||
```
|
||
|
||
Необязательный аргумент `add_tree` — заголовок вкладки. В нижней панели он
|
||
показывается на вкладке; в боковых панелях заголовок сохраняется, но не
|
||
рисуется. Без заголовка используется «Дерево». Новый блок становится
|
||
активной вкладкой нижней панели.
|
||
|
||
Редактор отвечает только за отрисовку дерева. Что означает каждый узел,
|
||
решает сценарий: это могут быть файлы проекта, задачи, настройки — что угодно.
|
||
Файловое дерево проекта в `init.lua` собирается из `app:list_dir`, но сам
|
||
редактор о файловой системе в блоке ничего не знает.
|
||
|
||
| Метод | Описание |
|
||
| --- | --- |
|
||
| `tree:append(nodes, parent)` | Добавить узлы к `parent`; `parent = ""` — корень |
|
||
| `tree:clear()` | Удалить все узлы и сбросить выбор |
|
||
| `tree:remove(id)` | Удалить узел и всех потомков рекурсивно; `false`, если узла нет |
|
||
| `tree:on_toggle(fn)` | Обработчик раскрытия: `fn(expanded, id, loaded)` |
|
||
| `tree:on_click(fn)` | Обработчик клика по конечной ноде: `fn(id)` |
|
||
| `tree:selected()` | Идентификатор выбранной строки или `nil` |
|
||
| `tree:focus(id)` | Раскрыть всех родителей, выбрать узел и передать фокус дереву |
|
||
| `tree:map_key(modes, keys, handler)` | Привязка, которая действует, когда фокус на этом блоке |
|
||
| `tree:set_mode(mode)` | Переключить режим привязок блока |
|
||
| `tree:mode()` | Текущий режим привязок блока |
|
||
|
||
Узел — таблица:
|
||
|
||
```lua
|
||
{
|
||
id = "src/main.rs", -- строковый идентификатор (путь файла)
|
||
label = "main.rs", -- отображаемый текст
|
||
icon = "file-code", -- имя иконки Lucide (необязательно)
|
||
expandable = false, -- раскрываемая ли нода (папка)
|
||
expanded = false, -- раскрыта ли она сейчас
|
||
}
|
||
```
|
||
|
||
`append` добавляет узлы, а не заменяет дерево. Повторная передача того же `id`
|
||
обновляет описание узла, но не создаёт копию. `clear` удаляет всё дерево;
|
||
обработчики `on_toggle` и `on_click` продолжают действовать, поэтому дерево
|
||
удобно строить заново на каждое обновление данных.
|
||
|
||
Дети каждого уровня сортируются по `label` без учёта регистра. Сначала идут
|
||
раскрываемые узлы (`expandable = true`), затем конечные; внутри каждой группы —
|
||
по алфавиту.
|
||
|
||
В обработчик `on_toggle` приходит `loaded = true`, если дочерние узлы уже
|
||
добавляли в эту ноду; при первом раскрытии — `false`.
|
||
|
||
Клик по строке выбирает её. Выбранную строку можно перемещать стрелками
|
||
`↑`/`↓`; `↑`/`↓` двигают выбор, `→` раскрывает папку или переходит к первому
|
||
ребёнку, `←` сворачивает папку или выбирает родителя, `Enter` раскрывает папку
|
||
или вызывает `on_click` для конечной ноды. Выбор сохраняется, пока панель
|
||
закрыта; при повторном открытии панели фокус возвращается на выбранную строку.
|
||
Привязки блока (`tree:map_key`) обрабатываются дополнительно к глобальным и
|
||
только пока фокус находится на этом дереве; у каждого блока свой режим.
|
||
|
||
Пример отложенной загрузки дерева:
|
||
|
||
```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.editor: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
|
||
local diagnostics = app.bottom_panel:add_tree("Диагностика")
|
||
|
||
local icons = {
|
||
error = "circle-x",
|
||
warning = "triangle-alert",
|
||
information = "circle-alert",
|
||
hint = "lightbulb",
|
||
}
|
||
|
||
app.editor:on_lsp_diagnostics(1000, function(servers)
|
||
diagnostics:clear()
|
||
|
||
local files = {}
|
||
local order = {}
|
||
for _, entry in ipairs(servers) do
|
||
local path = entry.path or entry.uri
|
||
if not files[path] then
|
||
files[path] = {}
|
||
order[#order + 1] = path
|
||
end
|
||
for _, item in ipairs(entry.diagnostics) do
|
||
table.insert(files[path], item)
|
||
end
|
||
end
|
||
|
||
for _, path in ipairs(order) do
|
||
local groups = {}
|
||
local keys = {}
|
||
for _, item in ipairs(files[path]) do
|
||
local key = string.format("%d:%d:%d:%d", item.line, item.character,
|
||
item.end_line, item.end_character)
|
||
if not groups[key] then
|
||
groups[key] = { item = item, messages = {}, seen = {} }
|
||
keys[#keys + 1] = key
|
||
end
|
||
local group = groups[key]
|
||
if not group.seen[item.message] then
|
||
group.seen[item.message] = true
|
||
group.messages[#group.messages + 1] = item.message
|
||
end
|
||
end
|
||
|
||
diagnostics:append({
|
||
{ id = path, label = path, icon = "file",
|
||
expandable = true, expanded = true },
|
||
}, "")
|
||
|
||
local errors = {}
|
||
for index, key in ipairs(keys) do
|
||
local group = groups[key]
|
||
local item = group.item
|
||
local severity = item.severity_name or "error"
|
||
errors[#errors + 1] = {
|
||
id = string.format("diag:%s:%d:%d:%d", path, item.line,
|
||
item.character, index),
|
||
label = string.format("%d:%d %s", item.line + 1,
|
||
item.character + 1,
|
||
table.concat(group.messages, "; ")),
|
||
icon = icons[severity] or "circle-alert",
|
||
}
|
||
end
|
||
diagnostics:append(errors, path)
|
||
end
|
||
end)
|
||
|
||
diagnostics:on_click(function(id)
|
||
local path, line, character = id:match("^diag:(.*):(%d+):(%d+):%d+$")
|
||
if path then
|
||
app.editor:open(path, { tonumber(line), tonumber(character) })
|
||
else
|
||
app.editor:open(id, {})
|
||
end
|
||
end)
|
||
|
||
app.bottom_panel:open()
|
||
```
|
||
|
||
## Подключение других Lua-файлов
|
||
|
||
Файлы подключаются из каталога сценариев:
|
||
|
||
| Способ | Описание |
|
||
| --- | --- |
|
||
| `include("tools.lua")` | Выполнить файл сразу |
|
||
| `require("tools")` | Выполнить один раз (как обычный Lua-модуль) |
|
||
|
||
## Добавление новых методов
|
||
|
||
Методы добавляются в `src/api.rs`: всё, что есть в `impl AppApi` или
|
||
`impl EditorApi`, помеченном `#[mlua::userdata_impl]`, автоматически доступно
|
||
из Lua как `app:method(...)` или `app.editor:method(...)`.
|
||
Чтобы не экспортировать метод, его помечают `#[lua(skip)]`.
|
||
Новый вид блока панели должен добавить методы в свой `*Api` и отдавать свою
|
||
`KeyMap`, как это делает `TreeApi`.
|
||
|
||
## Полный пример `init.lua`
|
||
|
||
```lua
|
||
print("[qode] init.lua выполнен")
|
||
|
||
app:map_key("", "<C-q>", function() app:close() end)
|
||
app:map_key("", "<C-e>", "toggle_left_panel")
|
||
app:map_key("", "<C-t>", "toggle_bottom_panel")
|
||
|
||
app.editor:run_lsp({
|
||
name = "rust-analyzer",
|
||
cmd = "rust-analyzer",
|
||
languages = { "rust" },
|
||
watcher = "**/*.rs",
|
||
})
|
||
```
|