gutter and keys
This commit is contained in:
@@ -1,8 +1,9 @@
|
||||
# Lua API
|
||||
|
||||
Приложение выполняет `init.lua` при запуске и предоставляет сценариям объект
|
||||
`app` для управления окном, вкладками, панелями, файловой системой, подсветкой
|
||||
и LSP-серверами.
|
||||
`app` для управления окном, панелями и глобальными привязками клавиш.
|
||||
Вкладки, текст, gutter, подсветка, сохранение и LSP-серверы живут в `app.editor`,
|
||||
а блоки панелей (деревья) — в своих объектах.
|
||||
|
||||
Сам редактор не содержит демонстрационных документов: при запуске открыта
|
||||
одна пустая вкладка `untitled-1.txt`. Всё остальное — файлы, деревья, серверы —
|
||||
@@ -21,36 +22,46 @@ cargo run -- main.go
|
||||
|
||||
- В каталоге `--config` должны лежать `init.lua` и подключаемые им модули.
|
||||
- Первый позиционный аргумент открывается после выполнения `init.lua`
|
||||
через `app:open(path)` — как `vim main.go` или `nano main.go`.
|
||||
через `app.editor:open(path)` — как `vim main.go` или `nano main.go`.
|
||||
- `--config` меняет только каталог сценариев. Рабочий каталог процесса, а
|
||||
вместе с ним пути `app:open`, `app:save` и `root` у LSP не меняются.
|
||||
вместе с ним пути `app.editor:open`, `app.editor:save` и `root` у LSP не меняются.
|
||||
|
||||
## Объект `app`
|
||||
|
||||
Объект `app` управляет окном и панелями. Всё, что относится к самому
|
||||
редактору — вкладки, текст, gutter, сохранение, LSP и привязки клавиш
|
||||
редактора, — живёт в `app.editor`:
|
||||
|
||||
```lua
|
||||
app.editor:open("src/main.rs")
|
||||
app.editor:save()
|
||||
app.editor:map_key("n", "<C-s>", "save")
|
||||
```
|
||||
|
||||
Номера вкладок во всех методах нумеруются с нуля. Массив, который возвращает
|
||||
`app:tabs()`, остаётся обычной Lua-таблицей с индексами от 1, поэтому поле
|
||||
`app.editor:tabs()`, остаётся обычной Lua-таблицей с индексами от 1, поэтому поле
|
||||
`index` — единственный надёжный способ сослаться на вкладку.
|
||||
|
||||
### Вкладки
|
||||
|
||||
| Метод | Описание |
|
||||
| --- | --- |
|
||||
| `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, position)` | Открыть файл с диска, вернуть индекс вкладки. Если файла нет — открывается пустой буфер с таким именем. Без второго аргумента фокус не меняется; `{}` — перевести фокус в редактор, оставив курсор; `{line, column}` — ещё и поставить курсор (с нуля) |
|
||||
| `app:close_tab(index)` | Закрыть вкладку; `true`, если вкладка закрыта |
|
||||
| `app:set_tab_icon(index, name)` | Назначить иконку вкладке; `false`, если вкладки нет или иконка неизвестна |
|
||||
| `app:clear_tab_icon(index)` | Вернуть иконку по умолчанию (`file`) |
|
||||
| `app:tab_icon(index)` | Имя иконки вкладки или `nil`, если вкладки нет |
|
||||
| `app.editor:tabs()` | Список вкладок: `{{ index = 0, title = "main.rs", icon = "file" }, ...}` |
|
||||
| `app.editor:active_tab()` | Индекс активной вкладки |
|
||||
| `app.editor:set_active_tab(index)` | Сделать вкладку активной; `true`, если вкладка существует |
|
||||
| `app.editor:next_tab()` | Переключить на следующую вкладку, вернуть индекс |
|
||||
| `app.editor:previous_tab()` | Переключить на предыдущую вкладку, вернуть индекс |
|
||||
| `app.editor:open_tab()` | Открыть пустую вкладку, вернуть индекс |
|
||||
| `app.editor:open(path, position)` | Открыть файл с диска, вернуть индекс вкладки. Если файла нет — открывается пустой буфер с таким именем. Без второго аргумента фокус не меняется; `{}` — перевести фокус в редактор, оставив курсор; `{line, column}` — ещё и поставить курсор (с нуля) |
|
||||
| `app.editor:close_tab(index)` | Закрыть вкладку; `true`, если вкладка закрыта |
|
||||
| `app.editor:set_tab_icon(index, name)` | Назначить иконку вкладке; `false`, если вкладки нет или иконка неизвестна |
|
||||
| `app.editor:clear_tab_icon(index)` | Вернуть иконку по умолчанию (`file`) |
|
||||
| `app.editor:tab_icon(index)` | Имя иконки вкладки или `nil`, если вкладки нет |
|
||||
|
||||
```lua
|
||||
app:open("src/main.rs") -- открыть, не трогая фокус
|
||||
app:open("src/main.rs", {}) -- фокус в редактор, курсор на месте
|
||||
app:open("src/main.rs", {4, 2}) -- фокус в редактор, курсор на строку 4, столбец 2
|
||||
app.editor:open("src/main.rs") -- открыть, не трогая фокус
|
||||
app.editor:open("src/main.rs", {}) -- фокус в редактор, курсор на месте
|
||||
app.editor:open("src/main.rs", {4, 2}) -- фокус в редактор, курсор на строку 4, столбец 2
|
||||
```
|
||||
|
||||
Строка и столбец нумеруются с нуля, как в диагностике LSP; столбец считается
|
||||
@@ -63,21 +74,45 @@ app:open("src/main.rs", {4, 2}) -- фокус в редактор, курсор
|
||||
Например:
|
||||
|
||||
```lua
|
||||
app:set_tab_icon(0, "file-code")
|
||||
app:set_tab_icon(1, "book-open")
|
||||
app:clear_tab_icon(1) -- снова "file"
|
||||
app.editor:set_tab_icon(0, "file-code")
|
||||
app.editor:set_tab_icon(1, "book-open")
|
||||
app.editor:clear_tab_icon(1) -- снова "file"
|
||||
```
|
||||
|
||||
Подчёркивания и пробелы считаются дефисом, а `icons/` и `.svg` можно
|
||||
не писать: `file_code`, `file-code` и `icons/file-code.svg` равнозначны.
|
||||
Неизвестное имя не назначается — метод вернёт `false`.
|
||||
|
||||
### Иконки на полях редактора (gutter)
|
||||
|
||||
Слева от редактора есть узкая колонка с иконками. Методы действуют на
|
||||
активную вкладку, а номера строк нумеруются с нуля, как в LSP. На одной строке
|
||||
живёт одна иконка; повторный вызов заменяет прежнюю.
|
||||
|
||||
| Метод | Описание |
|
||||
| --- | --- |
|
||||
| `app.editor:set_gutter_icon(line, icon, color)` | Поставить иконку на строку; `color` необязателен (`#RGB`, `#RGBA`, `#RRGGBB`, `#RRGGBBAA`). Без цвета иконка рисуется цветом темы. `false` — иконка или цвет неизвестны |
|
||||
| `app.editor:gutter_icon(line)` | Имя иконки на строке или `nil` |
|
||||
| `app.editor:gutter_color(line)` | Цвет иконки или `nil`, если цвет не задан |
|
||||
| `app.editor:clear_gutter_icon(line)` | Убрать иконку со строки; `false`, если её не было |
|
||||
| `app.editor:clear_gutter()` | Убрать все иконки активной вкладки |
|
||||
|
||||
```lua
|
||||
app.editor:set_gutter_icon(4, "circle", "#e5484d") -- красная точка на 5-й строке
|
||||
app.editor:set_gutter_icon(9, "play") -- цвет темы
|
||||
app.editor:clear_gutter_icon(9)
|
||||
```
|
||||
|
||||
Иконки привязаны к номерам строк и автоматически не сдвигаются при правке
|
||||
текста: сценарий сам решает, когда их переставить. При прокрутке колонка
|
||||
двигается вместе с редактором.
|
||||
|
||||
### Текст
|
||||
|
||||
| Метод | Описание |
|
||||
| --- | --- |
|
||||
| `app:active_text()` | Текст активной вкладки |
|
||||
| `app:set_active_text(text)` | Заменить текст активной вкладки |
|
||||
| `app.editor:active_text()` | Текст активной вкладки |
|
||||
| `app.editor:set_active_text(text)` | Заменить текст активной вкладки |
|
||||
|
||||
### Подсветка синтаксиса
|
||||
|
||||
@@ -86,19 +121,19 @@ app:clear_tab_icon(1) -- снова "file"
|
||||
|
||||
| Метод | Описание |
|
||||
| --- | --- |
|
||||
| `app:set_language(index, "rust")` | Включить подсветку для вкладки; `false`, если вкладки нет или язык не поддерживается |
|
||||
| `app:language(index)` | Язык подсветки вкладки или `nil` |
|
||||
| `app:clear_language(index)` | Выключить подсветку для вкладки |
|
||||
| `app.editor:set_language(index, "rust")` | Включить подсветку для вкладки; `false`, если вкладки нет или язык не поддерживается |
|
||||
| `app.editor:language(index)` | Язык подсветки вкладки или `nil` |
|
||||
| `app.editor:clear_language(index)` | Выключить подсветку для вкладки |
|
||||
|
||||
```lua
|
||||
app:set_language(0, "rust")
|
||||
app.editor:set_language(0, "rust")
|
||||
```
|
||||
|
||||
### Сохранение
|
||||
|
||||
| Метод | Описание |
|
||||
| --- | --- |
|
||||
| `app:save()` | Сохранить активную вкладку в её файл |
|
||||
| `app.editor:save()` | Сохранить активную вкладку в её файл |
|
||||
|
||||
### Файловая система
|
||||
|
||||
@@ -108,7 +143,7 @@ app:set_language(0, "rust")
|
||||
|
||||
Путь отсчитывается от текущего рабочего каталога процесса. Пустая строка или
|
||||
`"."` означают сам текущий каталог. Поле `id` — путь элемента относительно
|
||||
текущего каталога: его можно передать в `app:open(id)` и использовать как
|
||||
текущего каталога: его можно передать в `app.editor:open(id)` и использовать как
|
||||
идентификатор узла дерева. Элементы уже отсортированы по имени без учёта
|
||||
регистра.
|
||||
|
||||
@@ -120,12 +155,24 @@ end
|
||||
|
||||
### Привязка клавиш
|
||||
|
||||
Все привязки проходят через один `KeyMap`. Встроенные сочетания можно заменить
|
||||
или удалить: вторым аргументом принимается Lua-функция, имя встроенной команды
|
||||
или `nil`.
|
||||
Привязки живут у трёх видов объектов: у приложения (`app`), у редактора
|
||||
(`app.editor`) и у каждого блока панели (`tree`). Встроенные сочетания можно
|
||||
заменить или удалить: обработчиком принимается Lua-функция, имя встроенной
|
||||
команды или `nil`.
|
||||
|
||||
`app:map_key(keys, handler)` назначает привязку для обычного режима,
|
||||
`app:map_completion_key(keys, handler)` — для списка автодополнения.
|
||||
```lua
|
||||
app:map_key(modes, keys, handler) -- глобальные
|
||||
app.editor:map_key(modes, keys, handler) -- только когда фокус в редакторе
|
||||
app:map_completion_key(keys, handler) -- список автодополнения
|
||||
```
|
||||
|
||||
Слой автодополнения режимами не управляется: `map_completion_key` принимает
|
||||
только клавишу и обработчик.
|
||||
|
||||
Первым аргументом `map_key` идёт строка режимов. Порядок обработки всегда
|
||||
такой: сначала глобальные привязки, затем — если фокус в редакторе — привязки
|
||||
редактора, а если фокус на блоке панели — привязки этого блока. Привязки
|
||||
неактивного объекта не срабатывают.
|
||||
|
||||
Запись клавиши в духе nvim:
|
||||
|
||||
@@ -143,7 +190,32 @@ end
|
||||
и Windows буквы берутся по аппаратному keycode, поэтому `<C-q>` работает
|
||||
и на русской раскладке.
|
||||
|
||||
Встроенные команды обычного режима:
|
||||
#### Режимы
|
||||
|
||||
У каждого объекта своя текущая режимная метка: один символ (`n`, `i`, `v`, `r`
|
||||
или любой другой) либо `-` — режим по умолчанию. Режимы динамические: заранее
|
||||
создавать их не нужно, достаточно переключить метку.
|
||||
|
||||
| Значение `modes` | Когда срабатывает |
|
||||
| --- | --- |
|
||||
| `""` | во всех режимах |
|
||||
| `"-"` | только в режиме по умолчанию |
|
||||
| `"niv"` | в режимах `n`, `i`, `v` |
|
||||
| `"-iv"` | в режиме по умолчанию, а также `i` и `v` |
|
||||
|
||||
Текущий режим меняется методами `set_mode(mode)` и читается методом `mode()`.
|
||||
Режимы у приложения, редактора и каждого блока независимы.
|
||||
|
||||
```lua
|
||||
app:set_mode("n")
|
||||
app.editor:set_mode("i")
|
||||
|
||||
app:map_key("n", "<C-n>", "next_tab") -- только в режиме n
|
||||
app.editor:map_key("i", "<C-s>", "save") -- только в режиме i редактора
|
||||
app.editor:map_key("", "<C-space>", function() end) -- в любом режиме
|
||||
```
|
||||
|
||||
Встроенные команды:
|
||||
|
||||
| Имя | Действие |
|
||||
| --- | --- |
|
||||
@@ -169,25 +241,33 @@ end
|
||||
|
||||
При запуске уже назначены:
|
||||
|
||||
- `Ctrl/Cmd+S` → `save`;
|
||||
- `Ctrl/Cmd+S` → `save` во всех режимах;
|
||||
- `Ctrl/Cmd+Space` → `completion_trigger`;
|
||||
- `Enter`/`Tab`, `Esc`, `↑`/`↓`, `PageUp`/`PageDown`, `←`/`→`, `Home`/`End`
|
||||
→ команды слоя автодополнения.
|
||||
|
||||
```lua
|
||||
-- Переопределить сохранение своей функцией.
|
||||
app:map_key("<C-s>", function() app:save() end)
|
||||
-- Переопределить сохранение своей функцией во всех режимах.
|
||||
app:map_key("", "<C-s>", function() app.editor:save() end)
|
||||
|
||||
-- Убрать встроенную привязку.
|
||||
app:map_key("<C-s>", nil)
|
||||
app:map_key("", "<C-s>", nil)
|
||||
|
||||
-- Назначить встроенную команду на другую клавишу.
|
||||
app:map_key("<C-n>", "next_tab")
|
||||
app:map_key("<C-b>", "toggle_left_panel")
|
||||
app:map_key("", "<C-n>", "next_tab")
|
||||
app:map_key("", "<C-b>", "toggle_left_panel")
|
||||
|
||||
-- Навигация по списку автодополнения на Ctrl+N / Ctrl+P.
|
||||
app:map_completion_key("<C-n>", "completion_next")
|
||||
app:map_completion_key("<C-p>", "completion_previous")
|
||||
|
||||
-- Привязка, которая срабатывает только при фокусе в редакторе.
|
||||
app.editor:map_key("", "<A-r>", function()
|
||||
app.editor:set_active_text("editor mode")
|
||||
end)
|
||||
|
||||
-- Привязка блока дерева: работает, когда фокус на этом дереве.
|
||||
tree:map_key("v", "<A-f>", function() print("tree v") end)
|
||||
```
|
||||
|
||||
Пока список открыт, его слой проверяется раньше действий редактора. Сочетания
|
||||
@@ -209,7 +289,7 @@ app:map_completion_key("<C-p>", "completion_previous")
|
||||
Запуск серверов целиком настраивается Lua-сценарием.
|
||||
|
||||
```lua
|
||||
app:run_lsp({
|
||||
app.editor:run_lsp({
|
||||
name = "gopls", -- условное имя для stop/restart
|
||||
cmd = "gopls", -- путь к бинарнику или имя в PATH
|
||||
args = { "serve" }, -- аргументы процесса
|
||||
@@ -223,11 +303,11 @@ app:run_lsp({
|
||||
|
||||
| Метод | Описание |
|
||||
| --- | --- |
|
||||
| `app:run_lsp(config)` | Запустить сервер по описанию |
|
||||
| `app:lsp_servers()` | Имена запущенных серверов: `{ "gopls", ... }` |
|
||||
| `app:stop_lsp("gopls")` | Остановить сервер; `true`, если сервер был запущен |
|
||||
| `app:restart_lsp("gopls")` | Перезапустить сервер с прежними настройками |
|
||||
| `app:on_lsp_diagnostics(delay_ms, fn)` | Обработчик диагностики всех серверов; `nil` вместо `fn` удаляет его |
|
||||
| `app.editor:run_lsp(config)` | Запустить сервер по описанию |
|
||||
| `app.editor:lsp_servers()` | Имена запущенных серверов: `{ "gopls", ... }` |
|
||||
| `app.editor:stop_lsp("gopls")` | Остановить сервер; `true`, если сервер был запущен |
|
||||
| `app.editor:restart_lsp("gopls")` | Перезапустить сервер с прежними настройками |
|
||||
| `app.editor:on_lsp_diagnostics(delay_ms, fn)` | Обработчик диагностики всех серверов; `nil` вместо `fn` удаляет его |
|
||||
|
||||
`cmd` обязателен. Если `root` не задан или относителен, он отсчитывается от
|
||||
текущего рабочего каталога процесса. Поле `watcher` принимает строку, таблицу
|
||||
@@ -251,14 +331,14 @@ watcher = true -- все файлы, игнорируя только стан
|
||||
|
||||
### Обработчик диагностики
|
||||
|
||||
`app:on_lsp_diagnostics(delay_ms, fn)` регистрирует один обработчик на все
|
||||
`app.editor:on_lsp_diagnostics(delay_ms, fn)` регистрирует один обработчик на все
|
||||
LSP-серверы. `delay_ms` — пауза тишины: если сообщения приходят чаще, вызовы
|
||||
пропускаются, а обработчик получает последний снимок после того, как
|
||||
обновления прекратились. Если зарегистрировать обработчик, когда диагностика
|
||||
уже накоплена, он сработает сразу после этой паузы.
|
||||
|
||||
```lua
|
||||
app:on_lsp_diagnostics(1000, function(servers)
|
||||
app.editor:on_lsp_diagnostics(1000, function(servers)
|
||||
for _, entry in ipairs(servers) do
|
||||
print(entry.server, entry.path, #entry.diagnostics)
|
||||
end
|
||||
@@ -293,10 +373,10 @@ end)
|
||||
Примеры запуска:
|
||||
|
||||
```lua
|
||||
app:run_lsp({ name = "gopls", cmd = "gopls", args = { "serve" },
|
||||
app.editor:run_lsp({ name = "gopls", cmd = "gopls", args = { "serve" },
|
||||
languages = { "go" }, watcher = "**/*.go" })
|
||||
|
||||
app:run_lsp({ name = "rust-analyzer", cmd = "rust-analyzer",
|
||||
app.editor:run_lsp({ name = "rust-analyzer", cmd = "rust-analyzer",
|
||||
languages = { "rust" }, watcher = "**/*.rs" })
|
||||
```
|
||||
|
||||
@@ -304,7 +384,7 @@ app:run_lsp({ name = "rust-analyzer", cmd = "rust-analyzer",
|
||||
|
||||
```lua
|
||||
local gopls_ok, gopls_err = pcall(function()
|
||||
app:run_lsp({
|
||||
app.editor:run_lsp({
|
||||
name = "gopls",
|
||||
cmd = "gopls",
|
||||
args = { "serve" },
|
||||
@@ -409,6 +489,9 @@ local diagnostics = app.bottom_panel:add_tree("Диагностика")
|
||||
| `tree:on_click(fn)` | Обработчик клика по конечной ноде: `fn(id)` |
|
||||
| `tree:selected()` | Идентификатор выбранной строки или `nil` |
|
||||
| `tree:focus(id)` | Раскрыть всех родителей, выбрать узел и передать фокус дереву |
|
||||
| `tree:map_key(modes, keys, handler)` | Привязка, которая действует, когда фокус на этом блоке |
|
||||
| `tree:set_mode(mode)` | Переключить режим привязок блока |
|
||||
| `tree:mode()` | Текущий режим привязок блока |
|
||||
|
||||
Узел — таблица:
|
||||
|
||||
@@ -439,6 +522,8 @@ local diagnostics = app.bottom_panel:add_tree("Диагностика")
|
||||
ребёнку, `←` сворачивает папку или выбирает родителя, `Enter` раскрывает папку
|
||||
или вызывает `on_click` для конечной ноды. Выбор сохраняется, пока панель
|
||||
закрыта; при повторном открытии панели фокус возвращается на выбранную строку.
|
||||
Привязки блока (`tree:map_key`) обрабатываются дополнительно к глобальным и
|
||||
только пока фокус находится на этом дереве; у каждого блока свой режим.
|
||||
|
||||
Пример отложенной загрузки дерева:
|
||||
|
||||
@@ -467,14 +552,14 @@ end)
|
||||
|
||||
tree:on_click(function(id)
|
||||
-- Фокус в редактор, курсор остаётся на прежнем месте.
|
||||
app:open(id, {})
|
||||
app.editor:open(id, {})
|
||||
end)
|
||||
|
||||
app.left_panel:open()
|
||||
load_dir("")
|
||||
|
||||
-- При открытии панели вернуть фокус на последнюю выбранную строку.
|
||||
app:map_key("<C-b>", function()
|
||||
app:map_key("", "<C-b>", function()
|
||||
if app.left_panel:toggle() then
|
||||
local selected = tree:selected()
|
||||
if selected then
|
||||
@@ -499,7 +584,7 @@ local icons = {
|
||||
hint = "lightbulb",
|
||||
}
|
||||
|
||||
app:on_lsp_diagnostics(1000, function(servers)
|
||||
app.editor:on_lsp_diagnostics(1000, function(servers)
|
||||
diagnostics:clear()
|
||||
|
||||
local files = {}
|
||||
@@ -558,9 +643,9 @@ end)
|
||||
diagnostics:on_click(function(id)
|
||||
local path, line, character = id:match("^diag:(.*):(%d+):(%d+):%d+$")
|
||||
if path then
|
||||
app:open(path, { tonumber(line), tonumber(character) })
|
||||
app.editor:open(path, { tonumber(line), tonumber(character) })
|
||||
else
|
||||
app:open(id, {})
|
||||
app.editor:open(id, {})
|
||||
end
|
||||
end)
|
||||
|
||||
@@ -578,20 +663,23 @@ app.bottom_panel:open()
|
||||
|
||||
## Добавление новых методов
|
||||
|
||||
Методы добавляются в `src/api.rs`: всё, что есть в `impl AppApi`, помеченном
|
||||
`#[mlua::userdata_impl]`, автоматически доступно из Lua.
|
||||
Методы добавляются в `src/api.rs`: всё, что есть в `impl AppApi` или
|
||||
`impl EditorApi`, помеченном `#[mlua::userdata_impl]`, автоматически доступно
|
||||
из Lua как `app:method(...)` или `app.editor:method(...)`.
|
||||
Чтобы не экспортировать метод, его помечают `#[lua(skip)]`.
|
||||
Новый вид блока панели должен добавить методы в свой `*Api` и отдавать свою
|
||||
`KeyMap`, как это делает `TreeApi`.
|
||||
|
||||
## Полный пример `init.lua`
|
||||
|
||||
```lua
|
||||
print("[qode] init.lua выполнен")
|
||||
|
||||
app:map_key("<C-q>", function() app:close() end)
|
||||
app:map_key("<C-e>", "toggle_left_panel")
|
||||
app:map_key("<C-t>", "toggle_bottom_panel")
|
||||
app:map_key("", "<C-q>", function() app:close() end)
|
||||
app:map_key("", "<C-e>", "toggle_left_panel")
|
||||
app:map_key("", "<C-t>", "toggle_bottom_panel")
|
||||
|
||||
app:run_lsp({
|
||||
app.editor:run_lsp({
|
||||
name = "rust-analyzer",
|
||||
cmd = "rust-analyzer",
|
||||
languages = { "rust" },
|
||||
|
||||
Reference in New Issue
Block a user