# Lua API Приложение выполняет `init.lua` при запуске и предоставляет сценариям объект `app` для управления окном, вкладками, панелями, файловой системой, подсветкой и LSP-серверами. ## Запуск и каталог сценариев Каталог поиска `init.lua` задаётся константой `SCRIPT_DIR` в `src/constants.rs` (по умолчанию — текущий каталог). Его можно переопределить аргументом командной строки: ```sh gpui-editor-test --config /path/to/project main.go cargo run -- --config ./config cargo run -- main.go ``` - В каталоге `--config` должны лежать `init.lua` и подключаемые им модули. - Первый позиционный аргумент открывается после выполнения `init.lua` через `app:open(path)` — как `vim main.go` или `nano main.go`. ## Объект `app` Номера вкладок во всех методах нумеруются с нуля. ### Вкладки | Метод | Описание | | --- | --- | | `app:tabs()` | Список вкладок: `{{ index = 0, title = "main.rs", icon = "file" }, ...}` | | `app:active_tab()` | Индекс активной вкладки | | `app:set_active_tab(index)` | Сделать вкладку активной; `true`, если вкладка существует | | `app:next_tab()` | Переключить на следующую вкладку, вернуть индекс | | `app:previous_tab()` | Переключить на предыдущую вкладку, вернуть индекс | | `app:open_tab()` | Открыть пустую вкладку, вернуть индекс | | `app:open(path)` | Открыть файл с диска, вернуть индекс вкладки. Если файла нет — открывается пустой буфер с таким именем | | `app:close_tab(index)` | Закрыть вкладку; `true`, если вкладка закрыта | | `app:set_tab_icon(index, name)` | Назначить иконку вкладке; `false`, если вкладки нет или иконка неизвестна | | `app:clear_tab_icon(index)` | Вернуть иконку по умолчанию (`file`) | | `app:tab_icon(index)` | Имя иконки вкладки или `nil`, если вкладки нет | ### Иконки вкладок Слева от названия вкладки показывается иконка; по умолчанию это `file`. Имя — название иконки [Lucide](https://lucide.dev/icons/), регистр не важен. Например: ```lua app:set_tab_icon(0, "file-code") -- main.rs app:set_tab_icon(1, "book-open") -- notes.md app:clear_tab_icon(1) -- снова "file" ``` Подчёркивания и пробелы считаются дефисом, а `icons/` и `.svg` можно не писать: `file_code`, `file-code` и `icons/file-code.svg` равнозначны. Неизвестное имя не назначается — метод вернёт `false`. ### Текст | Метод | Описание | | --- | --- | | `app:active_text()` | Текст активной вкладки | | `app:set_active_text(text)` | Заменить текст активной вкладки | ### Подсветка синтаксиса Подсветка по умолчанию выключена. Поддерживаемые языки: `rust`, `go`, `js`, `json`, `python`. | Метод | Описание | | --- | --- | | `app:set_language(index, "rust")` | Включить подсветку для вкладки; `false`, если вкладки нет или язык не поддерживается | | `app:language(index)` | Язык подсветки вкладки или `nil` | | `app:clear_language(index)` | Выключить подсветку для вкладки | ```lua app:set_language(0, "rust") ``` ### Сохранение | Метод | Описание | | --- | --- | | `app:save()` | Сохранить активную вкладку в её файл | ### Файловая система | Метод | Описание | | --- | --- | | `app:list_dir(path)` | Содержимое каталога: `{{ id = "src/main.rs", name = "main.rs", is_dir = false }, ...}` | Путь отсчитывается от текущего рабочего каталога процесса. Пустая строка или `"."` означают сам текущий каталог. Поле `id` — путь элемента относительно текущего каталога: его можно передать в `app:open(id)` и использовать как идентификатор узла дерева. Элементы уже отсортированы по имени без учёта регистра. ```lua for _, entry in ipairs(app:list_dir("src")) do print(entry.name, entry.is_dir) end ``` ### Привязка клавиш `app:map_key(keys, fn)` назначает обработчик на нажатие или отпускание клавиши (синтаксис в духе nvim): | Запись | Значение | | --- | --- | | `"a"` | нажатие A | | `"1"` | нажатие 1 | | `"!a"` | отпускание A | | `""` | C при зажатом Shift | | `""` | Z при зажатом Ctrl | | `""` | отпускание R при зажатом Alt | Клавиша сравнивается с именем клавиши от GPUI. Caps Lock не важен. На Linux и Windows буквы берутся по аппаратному keycode, поэтому `` работает и на русской раскладке. ```lua app:map_key("", function() app:save() end) app:map_key("", function() app:close() end) app:map_key("", function() app:close() end) app:map_key("", function() app.right_panel:toggle() end) app:map_key("", function() app.bottom_panel:toggle() end) ``` ### Окно | Метод | Описание | | --- | --- | | `app:close()` | Закрыть окно | | `app:toggle_maximize()` | Развернуть окно / вернуть прежний размер | | `app:set_maximized(true/false)` | Развернуть (`true`) или восстановить (`false`) окно | | `app:minimize()` | Свернуть окно | | `app:is_maximized()` | Развёрнуто ли окно прямо сейчас | ## LSP-серверы Запуск серверов целиком настраивается Lua-сценарием. ```lua app:run_lsp({ name = "gopls", -- условное имя для stop/restart cmd = "gopls", -- путь к бинарнику или имя в PATH args = { "serve" }, -- аргументы процесса env = { GOFLAGS = "-mod=mod" }, -- переменные окружения languages = { "go" }, -- какие languageId обслуживает watcher = "**/*.go", -- шаблон файлового вотчера root = ".", -- корень workspace (по умолчанию cwd) initialization_options = {}, -- initializationOptions сервера }) ``` | Метод | Описание | | --- | --- | | `app:run_lsp(config)` | Запустить сервер по описанию | | `app:lsp_servers()` | Имена запущенных серверов: `{ "gopls", ... }` | | `app:stop_lsp("gopls")` | Остановить сервер; `true`, если сервер был запущен | | `app:restart_lsp("gopls")` | Перезапустить сервер с прежними настройками | Поле `watcher` принимает строку или таблицу: ```lua watcher = "**/*.go" watcher = { pattern = "**/*.go", ignore = { "vendor/**" } } ``` Автодополнение включается само при вводе идентификатора; `Ctrl+Space` открывает список вручную. Пока список открыт, `↑`/`↓` и `PageUp`/`PageDown` перемещают выделение (курсор при этом стоит на месте), `Enter` или `Tab` вставляют выбранный элемент, `Esc` закрывает список. Горизонтальные стрелки, `Home` и `End` закрывают список и работают с курсором как обычно. Документация выбранного элемента показывается второй всплывающей панелью справа от списка. Примеры запуска: ```lua app:run_lsp({ name = "gopls", cmd = "gopls", args = { "serve" }, languages = { "go" }, watcher = "**/*.go" }) app:run_lsp({ name = "rust-analyzer", cmd = "rust-analyzer", languages = { "rust" }, watcher = "**/*.rs" }) ``` Пример из `init.lua` с защитой от ошибки запуска: ```lua local gopls_ok, gopls_err = pcall(function() app:run_lsp({ name = "gopls", cmd = "gopls", args = { "serve" }, root = ".", languages = { "go" }, watcher = { pattern = "**/*.go", ignore = { "vendor/**" } }, initialization_options = { usePlaceholders = true, completeUnimported = true, }, }) end) if not gopls_ok then print("[lsp] gopls не запущен: " .. tostring(gopls_err)) end ``` > `--config` меняет только каталог `init.lua`, поэтому `root = "."` указывает > на текущий каталог запуска — проект должен быть текущим рабочим каталогом. ## Панели окна По умолчанию панели закрыты, размер меняется перетаскиванием. Для `app.left_panel`, `app.right_panel` и `app.bottom_panel` доступны одинаковые методы: | Метод | Описание | | --- | --- | | `app.left_panel:open()` | Открыть панель | | `app.left_panel:close()` | Закрыть панель | | `app.left_panel:toggle()` | Переключить панель; вернуть `true`, если панель открыта | | `app.left_panel:is_open()` | Открыта ли панель | ```lua app.left_panel:open() app.right_panel:toggle() app.bottom_panel:close() ``` ### Блоки панелей Панель может содержать блоки. Блок создаётся через API панели и живёт вместе с ней; сейчас поддерживается один вид блока — абстрактное дерево. ```lua local tree = app.left_panel:add_tree() ``` Редактор отвечает только за отрисовку дерева. Что означает каждый узел, решает сценарий: это могут быть файлы проекта, задачи, настройки — что угодно. Файловое дерево проекта в `init.lua` собирается из `app:list_dir`, но сам редактор о файловой системе в блоке ничего не знает. | Метод | Описание | | --- | --- | | `tree:append(nodes, parent)` | Добавить узлы к `parent`; `parent = ""` — корень | | `tree:remove(id)` | Удалить узел и всех потомков рекурсивно; `false`, если узла нет | | `tree:on_toggle(fn)` | Обработчик раскрытия: `fn(expanded, id, loaded)` | | `tree:on_click(fn)` | Обработчик клика по конечной ноде: `fn(id)` | | `tree:selected()` | Идентификатор выбранной строки или `nil` | | `tree:focus(id)` | Раскрыть всех родителей, выбрать узел и передать фокус дереву | Узел — таблица: ```lua { id = "src/main.rs", -- строковый идентификатор (путь файла) label = "main.rs", -- отображаемый текст icon = "file-code", -- имя иконки Lucide (необязательно) expandable = false, -- раскрываемая ли нода (папка) expanded = false, -- раскрыта ли она сейчас } ``` `append` добавляет узлы, а не заменяет дерево. Повторная передача того же `id` обновляет описание узла, но не создаёт копию. Дети каждого уровня сортируются по `label` без учёта регистра. Сначала идут раскрываемые узлы (`expandable = true`), затем конечные; внутри каждой группы — по алфавиту. В обработчик `on_toggle` приходит `loaded = true`, если дочерние узлы уже добавляли в эту ноду; при первом раскрытии — `false`. Клик по строке выбирает её. Выбранную строку можно перемещать стрелками `↑`/`↓`; `↑`/`↓` двигают выбор, `→` раскрывает папку или переходит к первому ребёнку, `←` сворачивает папку или выбирает родителя, `Enter` раскрывает папку или вызывает `on_click` для конечной ноды. Выбор сохраняется, пока панель закрыта; при повторном открытии панели фокус возвращается на выбранную строку. Пример отложенной загрузки дерева: ```lua local tree = app.left_panel:add_tree() local function load_dir(path) local nodes = {} for _, entry in ipairs(app:list_dir(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) app:open(id) end) app.left_panel:open() load_dir("") -- При открытии панели вернуть фокус на последнюю выбранную строку. app:map_key("", function() if app.left_panel:toggle() then local selected = tree:selected() if selected then tree:focus(selected) end end end) ``` ## Подключение других Lua-файлов Файлы подключаются из каталога сценариев: | Способ | Описание | | --- | --- | | `include("tools.lua")` | Выполнить файл сразу | | `require("tools")` | Выполнить один раз (как обычный Lua-модуль) | ## Добавление новых методов Методы добавляются в `src/api.rs`: всё, что есть в `impl AppApi`, помеченном `#[mlua::userdata_impl]`, автоматически доступно из Lua. Чтобы не экспортировать метод, его помечают `#[lua(skip)]`. ## Полный пример `init.lua` ```lua print("[lua] init.lua выполнен") app:set_language(0, "rust") app:next_tab() app:map_key("", function() app:close() end) app:map_key("", function() app.right_panel:toggle() end) app:map_key("", function() app.bottom_panel:toggle() end) app:open_tab() ```