266 lines
12 KiB
Markdown
266 lines
12 KiB
Markdown
# Панели — `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()
|
||
```
|