Files
2026-10-31 15:33:00 +03:00

266 lines
12 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Панели — `api.panels`
Группа `api.panels` управляет панелями окна и верхней панелью редактора:
| Объект | Назначение |
| --- | --- |
| `api.panels.left` | Левая панель |
| `api.panels.right` | Правая панель |
| `api.panels.bottom` | Нижняя панель |
| `api.panels.toolbar` | Верхняя панель редактора: лейблы и кнопки |
По умолчанию панели закрыты, размер меняется перетаскиванием разделителя.
Боковые панели рисуют все свои блоки подряд и не показывают заголовки; блоки
нижней панели показываются вкладками.
Если у правой панели нет блоков, она показывает состояние LSP-серверов.
## Панели
Для `api.panels.left`, `api.panels.right` и `api.panels.bottom` доступны
одинаковые методы:
| Метод | Описание |
| --- | --- |
| `api.panels.left:open()` | Открыть панель |
| `api.panels.left:close()` | Закрыть панель |
| `api.panels.left:toggle()` | Переключить панель; вернуть `true`, если панель открыта |
| `api.panels.left:is_open()` | Открыта ли панель |
| `api.panels.left:add_tree(title)` | Добавить блок-дерево и вернуть его API |
```lua
api.panels.left:open()
api.panels.right:toggle()
api.panels.bottom:close()
```
## Верхняя панель редактора
Полоса между вкладками и редактором. У панели две области: элементы левой
выстраиваются от левого края вправо, элементы правой — от правого края влево.
| Метод | Описание |
| --- | --- |
| `api.panels.toolbar:add_label(text, icon, background, id, side)` | Добавить лейбл |
| `api.panels.toolbar:add_button(text, icon, background, id, side, fn)` | Добавить кнопку |
| `api.panels.toolbar:hide(id)` | Скрыть элемент; `false`, если элемента с таким `id` нет |
| `api.panels.toolbar:show(id)` | Снова показать скрытый элемент |
| `api.panels.toolbar:clear()` | Удалить все элементы панели |
- `icon` — имя иконки Lucide (`play`, `settings`, `file`, ...) или `nil`.
- `background` — `#RGB`, `#RGBA`, `#RRGGBB`, `#RRGGBBAA` или `nil` (прозрачный
фон). Цвет текста подбирается автоматически: тёмный на светлом фоне, светлый
на тёмном.
- `id` — идентификатор элемента для `hide` и `show`. Повторное добавление с тем
же `id` заменяет элемент, а не создаёт дубликат.
- `side` — `"left"` или `"right"`.
- `fn` — обработчик нажатия; вызывается без аргументов.
```lua
api.panels.toolbar:add_label("Готово", nil, nil, "status", "left")
api.panels.toolbar:add_label("UTF-8", nil, "#3f3f46", "encoding", "right")
api.panels.toolbar:add_button("Запустить", "play", nil, "run", "right", function()
print("Запуск")
end)
api.panels.toolbar:hide("run")
api.panels.toolbar:show("run")
```
## Блоки панелей
Панель может содержать блоки. Блок создаётся через API панели и живёт вместе
с ней; сейчас поддерживается один вид блока — абстрактное дерево.
```lua
local tree = api.panels.left:add_tree()
local diagnostics = api.panels.bottom:add_tree("Диагностика")
```
Необязательный аргумент `add_tree` — заголовок вкладки. В нижней панели он
показывается на вкладке; в боковых панелях заголовок сохраняется, но не
рисуется. Без заголовка используется «Дерево». Новый блок становится
активной вкладкой нижней панели.
Редактор отвечает только за отрисовку дерева. Что означает каждый узел,
решает сценарий: это могут быть файлы проекта, задачи, настройки — что угодно.
Файловое дерево проекта в `init.lua` собирается из `api.path:list`, но сам
редактор о файловой системе в блоке ничего не знает.
| Метод | Описание |
| --- | --- |
| `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:focus()` | Передать фокус дереву, не меняя выбранную строку |
| `tree:has_focus()` | Владеет ли дерево клавиатурным фокусом прямо сейчас |
| `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:focus()` без
аргумента передаёт фокус дереву, даже если ни одна строка ещё не выбрана.
`tree:has_focus()` возвращает состояние фокуса на момент последнего события
клавиатуры — его удобно проверять в глобальных обработчиках.
## Пример отложенной загрузки дерева
```lua
local tree = api.panels.left:add_tree()
local function load_dir(path)
local nodes = {}
for _, entry in ipairs(api.path:list(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)
-- Фокус в редактор, курсор остаётся на прежнем месте.
api.editor:open(id, {})
end)
api.panels.left:open()
load_dir("")
-- При открытии панели вернуть фокус на последнюю выбранную строку.
api.keymap:map_key("", "<C-b>", function()
if api.panels.left:toggle() then
local selected = tree:selected()
if selected then
tree:focus(selected)
end
end
end)
```
## Пример дерева диагностики
На первом уровне — файлы с полным путём, на втором — сообщения с иконкой
уровня. Сообщения с одинаковыми началом и концом диапазона объединяются в одну
запись; клик открывает файл на нужной позиции. Источник данных — обработчик
`api.editor:on_lsp_diagnostics`, описанный в [lsp.md](lsp.md).
```lua
local diagnostics = api.panels.bottom:add_tree("Диагностика")
local icons = {
error = "circle-x",
warning = "triangle-alert",
information = "circle-alert",
hint = "lightbulb",
}
api.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
api.editor:open(path, { tonumber(line), tonumber(character) })
else
api.editor:open(id, {})
end
end)
api.panels.bottom:open()
```