# Панели — `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("", "", 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() ```