refactoring

This commit is contained in:
2026-10-31 15:33:00 +03:00
parent e8e2d25600
commit 838627fb33
27 changed files with 2715 additions and 1259 deletions
+265
View File
@@ -0,0 +1,265 @@
# Панели — `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()
```