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
+99
View File
@@ -0,0 +1,99 @@
# qode — конструктор IDE на Lua
`qode` — настольный редактор кода на GPUI (`gpui-kit`), который расширяется
Lua-сценариями. При запуске приложение выполняет `init.lua` и предоставляет
сценарию объект `api`.
## Идея
Rust-часть даёт только базовые универсальные примитивы:
- окно и его элементы;
- вкладки, текстовый редактор, подсветка, gutter, автодополнение;
- панели и абстрактное дерево;
- плавающее поле ввода;
- файловая система;
- привязки клавиш;
- LSP-клиент по stdio.
Вся бизнес-логика IDE — файловое дерево, дерево диагностики, команды,
раскладки, проектные сценарии, оформление и поведение — живёт в Lua.
Если поведение зависит от проекта или рабочего процесса, его место в
`init.lua` или подключаемых модулях, а не в Rust.
Новую возможность в Rust стоит добавлять, только если это универсальный
примитив, нужный разным сценариям, или если её нельзя выразить через уже
существующее API (доступ к GPUI, ОС, протоколу LSP). Бизнес-правила
(какие файлы показывать, как создавать документы, что писать в статус,
какие команды запускать) остаются в Lua.
## Запуск и каталог сценариев
Каталог поиска `init.lua` задаётся константой `SCRIPT_DIR` в
`src/constants.rs` (по умолчанию — текущий каталог). Его можно переопределить
аргументом командной строки:
```sh
qode --config /path/to/project main.go
cargo run -- --config ./config
cargo run -- main.go
```
- В каталоге `--config` должны лежать `init.lua` и подключаемые им модули.
- Первый позиционный аргумент открывается после выполнения `init.lua`
через `api.editor:open(path)` — как `vim main.go` или `nano main.go`.
- `--config` меняет только каталог сценариев. Рабочий каталог процесса, а
вместе с ним пути `api.editor:open`, `api.editor:save`, `api.path:*` и
`root` у LSP не меняются.
## Объект `api`
Объект `api` разбит на группы. Методы вызываются через двоеточие:
```lua
api.editor:open("src/main.rs")
api.editor:save()
api.panels.left:open()
api.path:list("src")
api.window:close()
api.keymap:map_key("", "<C-s>", "save")
```
| Группа | Назначение | Документация |
| --- | --- | --- |
| `api.editor` | Вкладки, текст, gutter, подсветка, сохранение, LSP, привязки редактора | [editor.md](editor.md) |
| `api.panels` | Левая, правая и нижняя панели, верхняя панель редактора, блоки-деревья | [panels.md](panels.md) |
| `api.path` | Файловая система: чтение, запись, создание и удаление | [path.md](path.md) |
| `api.window` | Окно и плавающее поле ввода | [window.md](window.md) |
| `api.keymap` | Глобальные привязки клавиш и режимы | [keymap.md](keymap.md) |
| `api.editor` (LSP) | Запуск серверов и обработка диагностики | [lsp.md](lsp.md) |
Номера вкладок во всех методах нумеруются с нуля. Массив, который возвращает
`api.editor:tabs()`, остаётся обычной Lua-таблицей с индексами от 1, поэтому
поле `index` — единственный надёжный способ сослаться на вкладку.
## Подключение других Lua-файлов
Файлы подключаются из каталога сценариев:
| Способ | Описание |
| --- | --- |
| `include("tools.lua")` | Выполнить файл сразу |
| `require("tools")` | Выполнить один раз (как обычный Lua-модуль) |
## Добавление новых методов
Методы добавляются в `src/api.rs`. Публичные методы `impl`-блока с
`#[mlua::userdata_impl]` автоматически доступны из Lua:
- методы `AppApi` — у объекта `api`;
- методы `EditorApi` — у `api.editor`;
- методы `PanelApi` — у `api.panels.left`, `.right`, `.bottom`;
- методы `ToolbarApi` — у `api.panels.toolbar`;
- методы `PathApi` — у `api.path`;
- методы `WindowApi` — у `api.window`;
- методы `KeymapApi` — у `api.keymap`.
Чтобы не экспортировать метод, его помечают `#[lua(skip)]`.
Новый вид блока панели должен добавить методы в свой `*Api` и отдавать свою
`KeyMap`, как это делает `TreeApi`.
+132
View File
@@ -0,0 +1,132 @@
# Редактор — `api.editor`
Группа `api.editor` управляет вкладками, текстом, gutter, подсветкой и
сохранением. LSP-серверы описаны отдельно в [lsp.md](lsp.md), привязки
клавиш редактора — в [keymap.md](keymap.md).
Номера вкладок нумеруются с нуля.
## Вкладки
| Метод | Описание |
| --- | --- |
| `api.editor:tabs()` | Список вкладок: `{{ index = 0, title = "main.rs", icon = "file" }, ...}` |
| `api.editor:active_tab()` | Индекс активной вкладки |
| `api.editor:set_active_tab(index)` | Сделать вкладку активной; `true`, если вкладка существует |
| `api.editor:next_tab()` | Переключить на следующую вкладку, вернуть индекс |
| `api.editor:previous_tab()` | Переключить на предыдущую вкладку, вернуть индекс |
| `api.editor:open_tab()` | Открыть пустую вкладку, вернуть индекс |
| `api.editor:open(path, position)` | Открыть файл с диска, вернуть индекс вкладки |
| `api.editor:close_tab(index)` | Закрыть вкладку; `true`, если вкладка закрыта |
| `api.editor:focus()` | Перевести фокус в редактор, не меняя вкладку и курсор |
Если файла нет, `open` создаёт пустой буфер с таким именем — как в vim.
Язык подсветки определяется по расширению, если для него есть грамматика.
Первый аргумент принимает и строку `"src/main.rs"`, и массив сегментов
`{ "src", "main.rs" }`.
Второй аргумент `open` необязателен:
| Значение | Поведение |
| --- | --- |
| нет аргумента / `nil` | Открыть вкладку, не трогая фокус |
| `{}` | Перевести фокус в редактор, курсор оставить на месте |
| `{line, column}` | Перевести фокус и поставить курсор (с нуля) |
```lua
api.editor:open("src/main.rs") -- открыть, не трогая фокус
api.editor:open("src/main.rs", {}) -- фокус в редактор, курсор на месте
api.editor:open("src/main.rs", {4, 2}) -- фокус в редактор, курсор на строку 4, столбец 2
```
Строка и столбец нумеруются с нуля, как в диагностике LSP; столбец считается
в UTF-16.
## Иконки вкладок
Слева от названия вкладки показывается иконка; по умолчанию это `file`.
Имя — название иконки [Lucide](https://lucide.dev/icons/), регистр не важен.
Подчёркивания и пробелы считаются дефисом, а `icons/` и `.svg` можно не
писать: `file_code`, `file-code` и `icons/file-code.svg` равнозначны.
| Метод | Описание |
| --- | --- |
| `api.editor:set_tab_icon(index, name)` | Назначить иконку вкладке; `false`, если вкладки нет или иконка неизвестна |
| `api.editor:clear_tab_icon(index)` | Вернуть иконку по умолчанию (`file`) |
| `api.editor:tab_icon(index)` | Имя иконки вкладки или `nil`, если вкладки нет |
```lua
api.editor:set_tab_icon(0, "file-code")
api.editor:set_tab_icon(1, "book-open")
api.editor:clear_tab_icon(1) -- снова "file"
```
## Иконки на полях редактора (gutter)
Слева от редактора есть узкая колонка с иконками. Методы действуют на
активную вкладку, а номера строк нумеруются с нуля, как в LSP. На одной строке
живёт одна иконка; повторный вызов заменяет прежнюю.
| Метод | Описание |
| --- | --- |
| `api.editor:set_gutter_icon(line, icon, color)` | Поставить иконку на строку; `color` необязателен (`#RGB`, `#RGBA`, `#RRGGBB`, `#RRGGBBAA`). Без цвета иконка рисуется цветом темы. `false` — иконка или цвет неизвестны |
| `api.editor:gutter_icon(line)` | Имя иконки на строке или `nil` |
| `api.editor:gutter_color(line)` | Цвет иконки или `nil`, если цвет не задан |
| `api.editor:clear_gutter_icon(line)` | Убрать иконку со строки; `false`, если её не было |
| `api.editor:clear_gutter()` | Убрать все иконки активной вкладки |
```lua
api.editor:set_gutter_icon(4, "circle", "#e5484d") -- красная точка на 5-й строке
api.editor:set_gutter_icon(9, "play") -- цвет темы
api.editor:clear_gutter_icon(9)
```
Иконки привязаны к номерам строк и автоматически не сдвигаются при правке
текста: сценарий сам решает, когда их переставить. При прокрутке колонка
двигается вместе с редактором.
## Текст
| Метод | Описание |
| --- | --- |
| `api.editor:active_text()` | Текст активной вкладки |
| `api.editor:set_active_text(text)` | Заменить текст активной вкладки |
## Сохранение
| Метод | Описание |
| --- | --- |
| `api.editor:save()` | Сохранить активную вкладку в её файл |
Путь считается относительно текущего рабочего каталога процесса. При успешной
записи с вкладки снимается маркер несохранённых изменений.
## Подсветка синтаксиса
Подсветка по умолчанию выключена. Поддерживаемые языки: `rust`, `go`, `js`,
`json`, `python`, `lua`, `bash`, `yaml`, `make`, `dockerfile`, `sql`,
`markdown`, `css`, `c`, `cpp`, `html`, `xml`. Для `set_language` принимаются
и короткие имена (`rs`, `golang`, `javascript`, `py`, `sh`, `yml`, `makefile`,
`md`, `c++`…).
| Метод | Описание |
| --- | --- |
| `api.editor:set_language(index, "rust")` | Включить подсветку для вкладки; `false`, если вкладки нет или язык не поддерживается |
| `api.editor:language(index)` | Язык подсветки вкладки или `nil` |
| `api.editor:clear_language(index)` | Выключить подсветку для вкладки |
```lua
api.editor:set_language(0, "rust")
```
## Привязки редактора
`api.editor:map_key`, `api.editor:set_mode` и `api.editor:mode` работают так
же, как глобальные привязки из [keymap.md](keymap.md), но срабатывают
дополнительно к глобальным и только когда фокус находится в редакторе.
```lua
api.editor:map_key("", "<A-r>", function()
api.editor:set_active_text("editor mode")
end)
```
+126
View File
@@ -0,0 +1,126 @@
# Привязки клавиш — `api.keymap`
Привязки живут у трёх видов объектов:
- у приложения — `api.keymap` (глобальные привязки);
- у редактора — `api.editor` (срабатывают, когда фокус в редакторе);
- у каждого блока панели — `tree` (срабатывают, когда фокус на этом блоке).
Встроенные сочетания можно заменить или удалить: обработчиком принимается
Lua-функция, имя встроенной команды или `nil`.
```lua
api.keymap:map_key(modes, keys, handler) -- глобальные
api.editor:map_key(modes, keys, handler) -- только когда фокус в редакторе
api.keymap:map_completion_key(keys, handler) -- список автодополнения
```
Слой автодополнения режимами не управляется: `map_completion_key` принимает
только клавишу и обработчик.
Порядок обработки всегда такой: сначала глобальные привязки, затем — если
фокус в редакторе — привязки редактора, а если фокус на блоке панели —
привязки этого блока. Привязки неактивного объекта не срабатывают.
## Запись клавиши
| Запись | Значение |
| --- | --- |
| `"a"` | нажатие A |
| `"1"` | нажатие 1 |
| `"!a"` | отпускание A |
| `"<S-c>"` | C при зажатом Shift |
| `"<C-z>"` | Z при зажатом Ctrl |
| `"<A-!r>"` | отпускание R при зажатом Alt |
| `"<D-s>"` | S при зажатом Cmd на macOS или Super на Linux |
Клавиша сравнивается с именем клавиши от GPUI. Caps Lock не важен. На Linux
и Windows буквы берутся по аппаратному keycode, поэтому `<C-q>` работает
и на русской раскладке.
## Режимы
У каждого объекта своя текущая режимная метка: один символ (`n`, `i`, `v`, `r`
или любой другой) либо `-` — режим по умолчанию. Режимы динамические: заранее
создавать их не нужно, достаточно переключить метку.
| Значение `modes` | Когда срабатывает |
| --- | --- |
| `""` | во всех режимах |
| `"-"` | только в режиме по умолчанию |
| `"niv"` | в режимах `n`, `i`, `v` |
| `"-iv"` | в режиме по умолчанию, а также `i` и `v` |
Текущий режим меняется методами `set_mode(mode)` и читается методом `mode()`.
Режимы у приложения, редактора и каждого блока независимы.
```lua
api.keymap:set_mode("n")
api.editor:set_mode("i")
api.keymap:map_key("n", "<C-n>", "next_tab") -- только в режиме n
api.editor:map_key("i", "<C-s>", "save") -- только в режиме i редактора
api.editor:map_key("", "<C-space>", function() end) -- в любом режиме
```
## Встроенные команды
| Имя | Действие |
| --- | --- |
| `"save"` | Сохранить активную вкладку |
| `"next_tab"` | Следующая вкладка |
| `"previous_tab"` | Предыдущая вкладка |
| `"open_tab"` | Новая пустая вкладка |
| `"close_tab"` | Закрыть активную вкладку |
| `"toggle_left_panel"` | Переключить левую панель |
| `"toggle_right_panel"` | Переключить правую панель |
| `"toggle_bottom_panel"` | Переключить нижнюю панель |
Встроенные команды списка автодополнения:
| Имя | Действие |
| --- | --- |
| `"completion_trigger"` | Открыть список |
| `"completion_next"` / `"completion_previous"` | Сдвинуть выделение |
| `"completion_page_down"` / `"completion_page_up"` | Пролистать список |
| `"completion_accept"` | Вставить выбранный элемент |
| `"completion_cancel"` | Закрыть список |
| `"completion_dismiss"` | Закрыть список и передать клавишу редактору |
При запуске уже назначены:
- `Ctrl/Cmd+S` → `save` во всех режимах;
- `Ctrl/Cmd+Space` → `completion_trigger`;
- `Enter`/`Tab`, `Esc`, `↑`/`↓`, `PageUp`/`PageDown`, `←`/`→`, `Home`/`End`
→ команды слоя автодополнения.
## Примеры
```lua
-- Переопределить сохранение своей функцией во всех режимах.
api.keymap:map_key("", "<C-s>", function() api.editor:save() end)
-- Убрать встроенную привязку.
api.keymap:map_key("", "<C-s>", nil)
-- Назначить встроенную команду на другую клавишу.
api.keymap:map_key("", "<C-n>", "next_tab")
api.keymap:map_key("", "<C-b>", "toggle_left_panel")
-- Навигация по списку автодополнения на Ctrl+N / Ctrl+P.
api.keymap:map_completion_key("<C-n>", "completion_next")
api.keymap:map_completion_key("<C-p>", "completion_previous")
-- Привязка, которая срабатывает только при фокусе в редакторе.
api.editor:map_key("", "<A-r>", function()
api.editor:set_active_text("editor mode")
end)
-- Привязка блока дерева: работает, когда фокус на этом дереве.
tree:map_key("v", "<A-f>", function() print("tree v") end)
```
Пока список автодополнения открыт, его слой проверяется раньше действий
редактора. Сочетания с модификаторами и горизонтальные стрелки закрывают
список, даже если для них нет своей привязки, — так `Shift+↑` продолжает
выделять текст.
+115
View File
@@ -0,0 +1,115 @@
# LSP-серверы — `api.editor`
Запуск серверов целиком настраивается Lua-сценарием. Редактор предоставляет
только клиент по stdio, синхронизацию документов и доставку диагностики.
## Запуск сервера
```lua
api.editor: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 сервера
})
```
| Метод | Описание |
| --- | --- |
| `api.editor:run_lsp(config)` | Запустить сервер по описанию |
| `api.editor:lsp_servers()` | Имена запущенных серверов: `{ "gopls", ... }` |
| `api.editor:stop_lsp("gopls")` | Остановить сервер; `true`, если сервер был запущен |
| `api.editor:restart_lsp("gopls")` | Перезапустить сервер с прежними настройками |
| `api.editor:on_lsp_diagnostics(delay_ms, fn)` | Обработчик диагностики всех серверов; `nil` вместо `fn` удаляет его |
`cmd` обязателен. Если `root` не задан или относителен, он отсчитывается от
текущего рабочего каталога процесса. Поле `watcher` принимает строку, таблицу
или boolean:
```lua
watcher = "**/*.go"
watcher = { pattern = "**/*.go", ignore = { "vendor/**" } }
watcher = true -- все файлы, игнорируя только стандартные каталоги сборки
```
> `--config` меняет только каталог `init.lua`, поэтому `root = "."` указывает
> на текущий каталог запуска — проект должен быть текущим рабочим каталогом.
## Автодополнение и подсказки
Автодополнение включается само при вводе идентификатора; `Ctrl/Cmd+Space`
открывает список вручную. Поведение клавиш описано в [keymap.md](keymap.md) и
может быть переопределено через `api.keymap:map_completion_key`.
Документация выбранного элемента показывается второй всплывающей панелью
справа от списка. Наведение мыши показывает Markdown-документацию символа.
Диагностику, которую сервер присылает после правок, редактор подчёркивает
волнистой линией: ошибки — красной, предупреждения и остальные сообщения —
жёлтой. Подчёркивание работает и при выключенной подсветке синтаксиса.
## Обработчик диагностики
`api.editor:on_lsp_diagnostics(delay_ms, fn)` регистрирует один обработчик на
все LSP-серверы. `delay_ms` — пауза тишины: если сообщения приходят чаще,
вызовы пропускаются, а обработчик получает последний снимок после того, как
обновления прекратились. Если зарегистрировать обработчик, когда диагностика
уже накоплена, он сработает сразу после этой паузы.
```lua
api.editor:on_lsp_diagnostics(1000, function(servers)
for _, entry in ipairs(servers) do
print(entry.server, entry.path, #entry.diagnostics)
end
end)
```
`servers` — массив записей по одной на пару «документ, сервер»:
```lua
{
server = "gopls", -- имя сервера, приславшего диагностику
uri = "file:///project/main.go", -- URI документа
path = "/project/main.go", -- путь файла; nil для не-file URI
diagnostics = {
{
line = 0, character = 2, -- начало (с нуля, как в LSP)
end_line = 0, end_character = 8,
severity = 1, -- 1 ошибка, 2 предупреждение, 3 info, 4 hint
severity_name = "error", -- error | warning | information | hint
message = "undefined: foo",
source = "gopls",
code = "UndeclaredName",
tags = {}, -- unnecessary, deprecated
},
},
}
```
Позиции нумеруются с нуля; для отображения прибавляйте единицу к `line` и
`character`. Остановка сервера удаляет его сообщения из следующих снимков.
Пример из `init.lua` с защитой от ошибки запуска:
```lua
local gopls_ok, gopls_err = pcall(function()
api.editor: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
```
+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()
```
+125
View File
@@ -0,0 +1,125 @@
# Файловая система — `api.path`
Группа `api.path` даёт базовые операции с файлами и каталогами. Все методы,
принимающие путь, понимают две формы:
```lua
api.path:list("src/main") -- строка с разделителями
api.path:list({ "src", "main" }) -- массив сегментов
```
Путь отсчитывается от текущего рабочего каталога процесса. Пустой путь,
`nil` и `{ }` означают сам текущий каталог (для `list` и `mkdir`).
## Методы
| Метод | Описание |
| --- | --- |
| `api.path:list(path)` | Содержимое каталога |
| `api.path:mkdir(path)` | Создать каталоги, включая родительские; вернуть созданный путь |
| `api.path:remove(path)` | Удалить файл или каталог рекурсивно |
| `api.path:touch(path)` | Создать пустой файл, если его нет, и обновить время изменения |
| `api.path:read(path)` | Прочитать файл в строку |
| `api.path:write(path, text, append)` | Записать строку в файл и вернуть путь |
### `list`
Возвращает массив элементов каталога:
```lua
{
{ id = "src/main.rs", name = "main.rs", is_dir = false },
{ id = "src/tools", name = "tools", is_dir = true },
}
```
- `id` — путь элемента относительно текущего каталога, если переданный путь
тоже относительный. Его можно сразу передать в `api.editor:open(id)` и
использовать как идентификатор узла дерева.
- `name` — имя без пути.
- `is_dir` — является ли элемент каталогом.
Элементы отсортированы по имени без учёта регистра. Ошибка чтения каталога
становится ошибкой Lua.
```lua
for _, entry in ipairs(api.path:list("src")) do
print(entry.name, entry.is_dir)
end
```
### `mkdir`
Создаёт каталоги последовательно, вместе со всеми отсутствующими родителями,
и возвращает созданный путь с разделителями `/`. Пустые сегменты
пропускаются. Для пустого пути ничего не создаёт и возвращает пустую строку.
```lua
api.path:mkdir({ "foo", "bar" }) -- foo, затем foo/bar; вернёт "foo/bar"
api.path:mkdir("foo/bar") -- то же самое
```
### `remove`
Удаляет файл, символическую ссылку или каталог вместе со всем содержимым.
Для каталога используется рекурсивное удаление.
Возвращает `true`, если что-то было удалено, и `false`, если пути не
существовало. Пустой путь удалять нельзя — это ошибка Lua.
```lua
api.path:remove("build")
api.path:remove({ "src", "generated" })
```
### `touch`
Создаёт пустой файл, если его нет. Содержимое существующего файла не
меняется, обновляется только время изменения. Родительский каталог должен
существовать — при необходимости создайте его через `mkdir`.
Возвращает путь с разделителями `/`.
```lua
api.path:touch({ "src", "notes.txt" })
```
### `read`
Читает файл целиком и возвращает строку. Ошибка чтения становится ошибкой Lua.
```lua
local text = api.path:read("src/main.rs")
```
### `write`
Записывает строку в файл:
```lua
api.path:write(path, text, append)
```
- `append = true` — дописать содержимое в конец;
- `append = false` или `nil` — перезаписать файл с нуля.
Файл создаётся, если его нет. Родительский каталог должен существовать.
Ошибка записи становится ошибкой Lua. Метод возвращает путь с разделителями `/`.
```lua
api.path:write("notes.txt", "first\n")
api.path:write("notes.txt", "second\n", true) -- "first\nsecond\n"
api.path:write("notes.txt", "reset\n") -- "reset\n"
```
## Пример: создание файла из Lua
```lua
local function create_file(parent, name, content)
api.path:mkdir(parent)
local path = api.path:write({ parent, name }, content or "")
api.editor:open(path, {})
end
create_file("src", "new.lua", "-- новый файл\n")
```
+41
View File
@@ -0,0 +1,41 @@
# Окно и ввод — `api.window`
Группа `api.window` управляет окном приложения и плавающим полем ввода.
## Окно
| Метод | Описание |
| --- | --- |
| `api.window:close()` | Закрыть окно |
| `api.window:toggle_maximize()` | Развернуть окно / вернуть прежний размер |
| `api.window:set_maximized(true/false)` | Развернуть (`true`) или восстановить (`false`) окно |
| `api.window:minimize()` | Свернуть окно |
| `api.window:is_maximized()` | Развёрнуто ли окно прямо сейчас |
Команды окна не выполняются в момент вызова из Lua: они складываются в
очередь и применяются видом в начале следующего кадра.
## Плавающее поле ввода
`api.window:open_input(options)` открывает однострочное поле ввода чуть выше
центра окна. Параметры передаются таблицей:
| Поле | Описание |
| --- | --- |
| `title` | Текст над полем; если не задан, окно состоит из одной строки ввода |
| `placeholder` | Подсказка внутри пустого поля ввода |
| `callback` | `function(text)` — вызывается после Enter, когда окно уже закрыто |
Пока окно открыто, сочетания клавиш приложения не работают. Текст набирается
в поле; `Enter` подтверждает ввод и запускает `callback`, `Esc` и клик мышью
вне окна закрывают его без вызова колбэка.
```lua
api.window:open_input({
title = "Имя файла",
placeholder = "src/main.rs",
callback = function(text)
api.editor:open(text, {})
end,
})
```