# Lua API Приложение выполняет `init.lua` при запуске и предоставляет сценариям объект `app` для управления окном, панелями и глобальными привязками клавиш. Вкладки, текст, gutter, подсветка, сохранение и LSP-серверы живут в `app.editor`, а блоки панелей (деревья) — в своих объектах. Сам редактор не содержит демонстрационных документов: при запуске открыта одна пустая вкладка `untitled-1.txt`. Всё остальное — файлы, деревья, серверы — настраивается сценарием. ## Запуск и каталог сценариев Каталог поиска `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` через `app.editor:open(path)` — как `vim main.go` или `nano main.go`. - `--config` меняет только каталог сценариев. Рабочий каталог процесса, а вместе с ним пути `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", "", "save") ``` Номера вкладок во всех методах нумеруются с нуля. Массив, который возвращает `app.editor:tabs()`, остаётся обычной Lua-таблицей с индексами от 1, поэтому поле `index` — единственный надёжный способ сослаться на вкладку. ### Вкладки | Метод | Описание | | --- | --- | | `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.editor:open("src/main.rs") -- открыть, не трогая фокус app.editor:open("src/main.rs", {}) -- фокус в редактор, курсор на месте app.editor:open("src/main.rs", {4, 2}) -- фокус в редактор, курсор на строку 4, столбец 2 ``` Строка и столбец нумеруются с нуля, как в диагностике LSP; столбец считается в UTF-16. ### Иконки вкладок Слева от названия вкладки показывается иконка; по умолчанию это `file`. Имя — название иконки [Lucide](https://lucide.dev/icons/), регистр не важен. Например: ```lua 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.editor:active_text()` | Текст активной вкладки | | `app.editor:set_active_text(text)` | Заменить текст активной вкладки | ### Подсветка синтаксиса Подсветка по умолчанию выключена. Поддерживаемые языки: `rust`, `go`, `js`, `json`, `python`. | Метод | Описание | | --- | --- | | `app.editor:set_language(index, "rust")` | Включить подсветку для вкладки; `false`, если вкладки нет или язык не поддерживается | | `app.editor:language(index)` | Язык подсветки вкладки или `nil` | | `app.editor:clear_language(index)` | Выключить подсветку для вкладки | ```lua app.editor:set_language(0, "rust") ``` ### Сохранение | Метод | Описание | | --- | --- | | `app.editor:save()` | Сохранить активную вкладку в её файл | ### Файловая система | Метод | Описание | | --- | --- | | `app:list_dir(path)` | Содержимое каталога: `{{ id = "src/main.rs", name = "main.rs", is_dir = false }, ...}` | Путь отсчитывается от текущего рабочего каталога процесса. Пустая строка или `"."` означают сам текущий каталог. Поле `id` — путь элемента относительно текущего каталога: его можно передать в `app.editor:open(id)` и использовать как идентификатор узла дерева. Элементы уже отсортированы по имени без учёта регистра. ```lua for _, entry in ipairs(app:list_dir("src")) do print(entry.name, entry.is_dir) end ``` ### Привязка клавиш Привязки живут у трёх видов объектов: у приложения (`app`), у редактора (`app.editor`) и у каждого блока панели (`tree`). Встроенные сочетания можно заменить или удалить: обработчиком принимается Lua-функция, имя встроенной команды или `nil`. ```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: | Запись | Значение | | --- | --- | | `"a"` | нажатие A | | `"1"` | нажатие 1 | | `"!a"` | отпускание A | | `""` | C при зажатом Shift | | `""` | Z при зажатом Ctrl | | `""` | отпускание R при зажатом Alt | | `""` | S при зажатом Cmd на macOS или Super на Linux | Клавиша сравнивается с именем клавиши от GPUI. Caps Lock не важен. На Linux и Windows буквы берутся по аппаратному keycode, поэтому `` работает и на русской раскладке. #### Режимы У каждого объекта своя текущая режимная метка: один символ (`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", "", "next_tab") -- только в режиме n app.editor:map_key("i", "", "save") -- только в режиме i редактора app.editor:map_key("", "", 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 -- Переопределить сохранение своей функцией во всех режимах. app:map_key("", "", function() app.editor:save() end) -- Убрать встроенную привязку. app:map_key("", "", nil) -- Назначить встроенную команду на другую клавишу. app:map_key("", "", "next_tab") app:map_key("", "", "toggle_left_panel") -- Навигация по списку автодополнения на Ctrl+N / Ctrl+P. app:map_completion_key("", "completion_next") app:map_completion_key("", "completion_previous") -- Привязка, которая срабатывает только при фокусе в редакторе. app.editor:map_key("", "", function() app.editor:set_active_text("editor mode") end) -- Привязка блока дерева: работает, когда фокус на этом дереве. tree:map_key("v", "", function() print("tree v") end) ``` Пока список открыт, его слой проверяется раньше действий редактора. Сочетания с модификаторами и горизонтальные стрелки закрывают список, даже если для них нет своей привязки, — так `Shift+↑` продолжает выделять текст. ### Окно | Метод | Описание | | --- | --- | | `app:close()` | Закрыть окно | | `app:toggle_maximize()` | Развернуть окно / вернуть прежний размер | | `app:set_maximized(true/false)` | Развернуть (`true`) или восстановить (`false`) окно | | `app:minimize()` | Свернуть окно | | `app:is_maximized()` | Развёрнуто ли окно прямо сейчас | ## LSP-серверы Запуск серверов целиком настраивается Lua-сценарием. ```lua app.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 сервера }) ``` | Метод | Описание | | --- | --- | | `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` принимает строку, таблицу или boolean: ```lua watcher = "**/*.go" watcher = { pattern = "**/*.go", ignore = { "vendor/**" } } watcher = true -- все файлы, игнорируя только стандартные каталоги сборки ``` Автодополнение включается само при вводе идентификатора; `Ctrl/Cmd+Space` открывает список вручную. Поведение клавиш описано в разделе «Привязка клавиш» и может быть переопределено через `app:map_completion_key`. Документация выбранного элемента показывается второй всплывающей панелью справа от списка. Диагностику, которую сервер присылает после правок, редактор подчёркивает волнистой линией: ошибки — красной, предупреждения и остальные сообщения — жёлтой. Подчёркивание работает и при выключенной подсветке синтаксиса. ### Обработчик диагностики `app.editor:on_lsp_diagnostics(delay_ms, fn)` регистрирует один обработчик на все LSP-серверы. `delay_ms` — пауза тишины: если сообщения приходят чаще, вызовы пропускаются, а обработчик получает последний снимок после того, как обновления прекратились. Если зарегистрировать обработчик, когда диагностика уже накоплена, он сработает сразу после этой паузы. ```lua app.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`. Остановка сервера удаляет его сообщения из следующих снимков. Примеры запуска: ```lua app.editor:run_lsp({ name = "gopls", cmd = "gopls", args = { "serve" }, languages = { "go" }, watcher = "**/*.go" }) app.editor:run_lsp({ name = "rust-analyzer", cmd = "rust-analyzer", languages = { "rust" }, watcher = "**/*.rs" }) ``` Пример из `init.lua` с защитой от ошибки запуска: ```lua local gopls_ok, gopls_err = pcall(function() app.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 ``` > `--config` меняет только каталог `init.lua`, поэтому `root = "."` указывает > на текущий каталог запуска — проект должен быть текущим рабочим каталогом. ## Верхняя панель редактора Полоса между вкладками и редактором. У панели две области: элементы левой выстраиваются от левого края вправо, элементы правой — от правого края влево. | Метод | Описание | | --- | --- | | `app.toolbar:add_label(text, icon, background, id, side)` | Добавить лейбл | | `app.toolbar:add_button(text, icon, background, id, side, fn)` | Добавить кнопку | | `app.toolbar:hide(id)` | Скрыть элемент; `false`, если элемента с таким `id` нет | | `app.toolbar:show(id)` | Снова показать скрытый элемент | | `app.toolbar:clear()` | Удалить все элементы панели | - `icon` — имя иконки Lucide (`play`, `settings`, `file`, ...) или `nil`. - `background` — `#RGB`, `#RGBA`, `#RRGGBB`, `#RRGGBBAA` или `nil` (прозрачный фон). Цвет текста подбирается автоматически: тёмный на светлом фоне, светлый на тёмном. - `id` — идентификатор элемента для `hide` и `show`. Повторное добавление с тем же `id` заменяет элемент, а не создаёт дубликат. - `side` — `"left"` или `"right"`. - `fn` — обработчик нажатия; вызывается без аргументов. ```lua app.toolbar:add_label("Готово", nil, nil, "status", "left") app.toolbar:add_label("UTF-8", nil, "#3f3f46", "encoding", "right") app.toolbar:add_button("Запустить", "play", nil, "run", "right", function() print("Запуск") end) app.toolbar:hide("run") app.toolbar:show("run") ``` ## Панели окна По умолчанию панели закрыты, размер меняется перетаскиванием. Для `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() ``` Если у правой панели нет блоков, она показывает состояние LSP-серверов. Блоки нижней панели показываются вкладками, как вкладки редактора; боковые панели рисуют все свои блоки подряд и заголовки не показывают. ### Блоки панелей Панель может содержать блоки. Блок создаётся через API панели и живёт вместе с ней; сейчас поддерживается один вид блока — абстрактное дерево. ```lua local tree = app.left_panel:add_tree() local diagnostics = app.bottom_panel:add_tree("Диагностика") ``` Необязательный аргумент `add_tree` — заголовок вкладки. В нижней панели он показывается на вкладке; в боковых панелях заголовок сохраняется, но не рисуется. Без заголовка используется «Дерево». Новый блок становится активной вкладкой нижней панели. Редактор отвечает только за отрисовку дерева. Что означает каждый узел, решает сценарий: это могут быть файлы проекта, задачи, настройки — что угодно. Файловое дерево проекта в `init.lua` собирается из `app:list_dir`, но сам редактор о файловой системе в блоке ничего не знает. | Метод | Описание | | --- | --- | | `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: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:map_key`) обрабатываются дополнительно к глобальным и только пока фокус находится на этом дереве; у каждого блока свой режим. Пример отложенной загрузки дерева: ```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.editor: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 local diagnostics = app.bottom_panel:add_tree("Диагностика") local icons = { error = "circle-x", warning = "triangle-alert", information = "circle-alert", hint = "lightbulb", } app.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 app.editor:open(path, { tonumber(line), tonumber(character) }) else app.editor:open(id, {}) end end) app.bottom_panel:open() ``` ## Подключение других Lua-файлов Файлы подключаются из каталога сценариев: | Способ | Описание | | --- | --- | | `include("tools.lua")` | Выполнить файл сразу | | `require("tools")` | Выполнить один раз (как обычный 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("", "", function() app:close() end) app:map_key("", "", "toggle_left_panel") app:map_key("", "", "toggle_bottom_panel") app.editor:run_lsp({ name = "rust-analyzer", cmd = "rust-analyzer", languages = { "rust" }, watcher = "**/*.rs", }) ```