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

12 KiB
Raw Permalink Blame History

Панели — 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
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 — обработчик нажатия; вызывается без аргументов.
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 панели и живёт вместе с ней; сейчас поддерживается один вид блока — абстрактное дерево.

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() Текущий режим привязок блока

Узел — таблица:

{
  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() возвращает состояние фокуса на момент последнего события клавиатуры — его удобно проверять в глобальных обработчиках.

Пример отложенной загрузки дерева

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.

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()