--- title: Скрипты редактора brief: Это руководство объясняет, как расширить редактор с помощью Lua --- # Скрипты редактора Вы можете создавать пользовательские пункты меню и хуки жизненного цикла редактора, используя файлы Lua со специальным расширением: `.editor_script`. Используя эту систему, вы можете настраивать редактор для улучшения рабочего процесса разработки. ## Время выполнения скрипта редактора Сценарии редактора выполняются внутри редактора, в Lua VM, эмулированной Java VM. Все скрипты работают в одном едином окружении, а это означает, что они могут взаимодействовать друг с другом. Вы можете подключать модули Lua, также как и файлы `.script`, но версия Lua, работающая внутри редактора, отличается, поэтому убедитесь, что ваш общий код совместим. Редактор использует Lua версии 5.2.x, а точнее [luaj](https://github.com/luaj/luaj) — в настоящее время это единственное жизнеспособное решение для запуска Lua на JVM. Кроме того, есть некоторые ограничения: - отсутствует пакет `debug`; - отсутствует `os.execute`, однако предоставляется аналог — `editor.execute()`; - отсутствуют `os.tmpname` и `io.tmpfile` — в настоящее время скрипты редактора могут обращаться к файлам только внутри каталога проекта; - отсутствует `os.rename`, хотя мы планируем его добавить; - отсутствуют `os.exit` и `os.setlocale`; - нельзя использовать некоторые функции, выполнение которых занимает длительное время, в контекстах, где редактор ожидает немедленного ответа от скрипта. См. раздел [Режимы выполнения](#режимы-выполнения) для подробностей. Все расширения редактора, определённые в скриптах редактора, загружаются при открытии проекта. При извлечении библиотек расширения перезагружаются, так как в библиотеках, от которых вы зависите, могут появиться новые скрипты редактора. Во время этой перезагрузки изменения в ваших собственных скриптах редактора не учитываются, поскольку вы можете находиться в процессе их изменения. Чтобы перезагрузить и их, нужно выполнить команду **Project → Reload Editor Scripts**. ## Анатомия `.editor_script` Каждый скрипт редактора должен возвращать модуль, подобный этому: ```lua local M = {} function M.get_commands() -- TODO - define editor commands end function M.get_language_servers() -- TODO - define language servers end function M.get_prefs_schema() -- TODO - define preferences end return M ``` Затем редактор собирает все скрипты редактора, определенные в проекте и библиотеках, загружает их в единую Lua VM и вызывает их при необходимости (подробнее об этом в разделах [commands](#commands) и [lifecycle hooks](#lifecycle-hooks)). ## API редактора Вы можете взаимодействовать с редактором, используя пакет `editor`, который определяет этот API: - `editor.platform` — строка: `"x86_64-win32"` для Windows, `"x86_64-macos"` для macOS или `"x86_64-linux"` для Linux. - `editor.version` — строка, версия Defold, например `"1.4.8"`. - `editor.engine_sha1` — строка, SHA1 движка Defold. - `editor.editor_sha1` — строка, SHA1 редактора Defold. - `editor.get(node_id, property)` — получить значение узла в редакторе. Узлы в редакторе — это различные сущности, такие как скриптовые или коллекционные файлы, игровые объекты в коллекциях, json-файлы, загруженные как ресурсы и т.д. `node_id` — это userdata, переданная редактором скрипту. Вместо `node_id` можно также передать путь к ресурсу, например `"/main/game.script"`. `property` — строка. Поддерживаются следующие свойства: - `"path"` — путь к файлу из каталога проекта для *ресурсов* — сущностей, существующих как файлы или директории. Пример: `"/main/game.script"` - `"children"` — список путей к дочерним ресурсам (для директорий) - `"text"` — текстовое содержимое ресурса, редактируемое как текст (например, скрипты или json). Пример: `"function init(self)\nend"`. Это не то же самое, что чтение через `io.open()`, так как можно редактировать файл без сохранения, и такие изменения доступны только при обращении к свойству `"text"`. - для атласов: `images` (список узлов изображений в атласе) и `animations` (список узлов анимаций) - для анимаций атласа: `images` (то же, что и в атласе) - для тайлмапов: `layers` (список узлов слоёв в тайлмапе) - для слоёв тайлмапа: `tiles` (неограниченная двумерная сетка тайлов), см. `tilemap.tiles.*` для подробностей - свойства, отображаемые в панели Properties при выделении объекта в Outline. Поддерживаются следующие типы: - `strings` - `booleans` - `numbers` - `vec2` / `vec3` / `vec4` - `resources` Обратите внимание, что некоторые из этих свойств могут быть доступны только для чтения или недоступны в определённых контекстах, поэтому перед чтением используйте `editor.can_get`, а перед установкой — `editor.can_set`. Наведите курсор на имя свойства в панели Properties, чтобы увидеть подсказку с именем свойства для скриптов редактора. Чтобы задать свойству значение `nil`, передайте пустую строку `""`. - `editor.can_get(node_id, property)` — проверить, можно ли безопасно получить это свойство с помощью `editor.get()`, не вызвав ошибку. - `editor.can_set(node_id, property)` — проверить, приведёт ли попытка установки свойства с помощью `editor.tx.set()` к ошибке. - `editor.create_directory(resource_path)` — создать директорию (и все отсутствующие родительские директории), если она не существует. - `editor.delete_directory(resource_path)` — удалить директорию, если она существует, включая все вложенные директории и файлы. - `editor.execute(cmd, [...args], [options])` — выполнить shell-команду, при необходимости получив её вывод. - `editor.save()` — сохранить все несохранённые изменения на диск. - `editor.transact(txs)` — изменить внутреннее состояние редактора с помощью одной или нескольких транзакций, созданных функциями `editor.tx.*`. - `editor.ui.*` — различные функции, связанные с пользовательским интерфейсом. См. [UI manual](/manuals/editor-scripts-ui). - `editor.prefs.*` — функции для работы с настройками редактора. См. раздел [preferences](#preferences). Полную документацию по API редактора можно найти [здесь](https://defold.com/ref/alpha/editor/). ## Команды Если модуль сценария редактора определяет функцию `get_commands`, она будет вызываться при перезагрузке расширения, и возвращенные команды будут доступны для использования внутри редактора в строке меню или в контекстных меню на панелях Assets и Outline. Например: ```lua local M = {} function M.get_commands() return { { label = "Remove Comments", locations = {"Edit", "Assets"}, query = { selection = {type = "resource", cardinality = "one"} }, active = function(opts) local path = editor.get(opts.selection, "path") return ends_with(path, ".lua") or ends_with(path, ".script") end, run = function(opts) local text = editor.get(opts.selection, "text") editor.transact({ editor.tx.set(opts.selection, "text", strip_comments(text)) }) end }, { label = "Minify JSON", locations = {"Assets"}, query = { selection = {type = "resource", cardinality = "one"} }, active = function(opts) return ends_with(editor.get(opts.selection, "path"), ".json") end, run = function(opts) local path = editor.get(opts.selection, "path") editor.execute("./scripts/minify-json.sh", path:sub(2)) end } } end return M ``` Редактор ожидает, что `get_commands()` вернёт массив таблиц, каждая из которых описывает отдельную команду. Описание команды состоит из следующих полей: - `label` (обязательный) — текст пункта меню, который будет отображён пользователю. - `locations` (обязательный) — массив из следующих значений: `"Edit"`, `"View"`, `"Project"`, `"Debug"`, `"Assets"`, `"Bundle"` или `"Outline"`. Определяет, где команда должна быть доступна. `"Edit"`, `"View"`, `"Project"` и `"Debug"` относятся к верхнему меню, `"Assets"` — к контекстному меню в панели Assets, `"Outline"` — к контекстному меню в панели Outline, а `"Bundle"` — к подменю **Project → Bundle**. - `query` — способ для команды запросить у редактора необходимую информацию и определить, с какими данными она работает. Для каждого ключа в таблице `query` будет соответствующий ключ в таблице `opts`, который обратные вызовы `active` и `run` получают в качестве аргумента. Поддерживаемые ключи: - `selection` — команда работает, когда есть выбранные элементы, и действует на них. - `type` — тип интересующих команду узлов. Поддерживаемые значения: - `"resource"` — в Assets и Outline: выбранный ресурс с файлом; в меню — открытый файл; - `"outline"` — элемент в панели Outline; в меню — открытый файл; - `cardinality` — количество выбранных элементов. Значение `"one"` означает, что в `opts.selection` передаётся один id узла. Значение `"many"` — массив из одного или нескольких id узлов. - `argument` — аргумент команды. В настоящее время используется только в командах, расположенных в `"Bundle"`. Значение `true` означает, что пользователь явно выбрал команду упаковки, а `false` — что она вызвана повторно. - `id` — строковый идентификатор команды, например, используется для запоминания последней использованной команды упаковки в `prefs`. - `active` — функция, вызываемая для определения, активна ли команда. Возвращает `true` или `false`. Для `Assets` и `Outline` вызывается при открытии контекстного меню. Для `Edit`, `View`, `Project` и `Debug` вызывается при каждом взаимодействии пользователя с редактором (клавиатура, мышь и т.д.), поэтому функция должна быть максимально быстрой. - `run` — функция, вызываемая при выборе команды пользователем. ### Использование команд для изменения состояния редактора в памяти Внутри обработчика `run` вы можете запрашивать и изменять состояние редактора в памяти. Запросы выполняются с помощью функции `editor.get()`, с помощью которой можно получить текущее состояние файлов и выделений (если используется `query = {selection = ...}`). Вы можете получить свойство `"text"` у скриптовых файлов, а также некоторые свойства, отображаемые в панели Properties — наведите курсор на имя свойства, чтобы увидеть подсказку с информацией о том, как это свойство называется в скриптах редактора. Изменение состояния редактора выполняется с помощью `editor.transact()`, где вы объединяете одну или несколько модификаций в один шаг, который можно отменить. Например, если вы хотите сбросить трансформацию игрового объекта, можно написать такую команду: ```lua { label = "Reset transform", locations = {"Outline"}, query = {selection = {type = "outline", cardinality = "one"}}, active = function(opts) local node = opts.selection return editor.can_set(node, "position") and editor.can_set(node, "rotation") and editor.can_set(node, "scale") end, run = function(opts) local node = opts.selection editor.transact({ editor.tx.set(node, "position", {0, 0, 0}), editor.tx.set(node, "rotation", {0, 0, 0}), editor.tx.set(node, "scale", {1, 1, 1}) }) end } ``` #### Редактирование атласов Помимо чтения и записи свойств атласа, вы можете читать и изменять изображения и анимации в атласе. Атлас определяет свойства-списки узлов `images` и `animations`, а анимации определяют собственное свойство-список узлов `images`. Вы можете использовать шаги транзакций `editor.tx.add`, `editor.tx.remove` и `editor.tx.clear` с этими свойствами. Например, чтобы добавить изображение в атлас, выполните следующий код в обработчике `run` команды: ```lua editor.transact({ editor.tx.add("/main.atlas", "images", {image="/assets/hero.png"}) }) ``` To find a set of all images in an atlas, execute the following code: ```lua local all_images = {} ---@type table -- first, collect all "bare" images local image_nodes = editor.get("/main.atlas", "images") for i = 1, #image_nodes do all_images[editor.get(image_nodes[i], "image")] = true end -- second, collect all images used in animations local animation_nodes = editor.get("/main.atlas", "animations") for i = 1, #animation_nodes do local animation_image_nodes = editor.get(animation_nodes[i], "images") for j = 1, #animation_image_nodes do all_images[editor.get(animation_image_nodes[j], "image")] = true end end pprint(all_images) -- { -- ["/assets/hero.png"] = true, -- ["/assets/enemy.png"] = true, -- }} ``` To replace all animations in an atlas: ```lua editor.transact({ editor.tx.clear("/main.atlas", "animations"), editor.tx.add("/main.atlas", "animations", { id = "hero_run", images = { {image = "/assets/hero_run_1.png"}, {image = "/assets/hero_run_2.png"}, {image = "/assets/hero_run_3.png"}, {image = "/assets/hero_run_4.png"} } }) }) ``` #### Редактирование tilesource Помимо свойств, отображаемых в Outline, tilesource определяет следующие дополнительные свойства: - `animations` — список узлов анимаций tilesource - `collision_groups` — список узлов групп столкновений tilesource - `tile_collision_groups` — таблица назначений групп столкновений для конкретных тайлов в tilesource Например, вот как можно настроить tilesource: ```lua local tilesource = "/game/world.tilesource" editor.transact({ editor.tx.add(tilesource, "animations", {id = "idle", start_tile = 1, end_tile = 1}), editor.tx.add(tilesource, "animations", {id = "walk", start_tile = 2, end_tile = 6, fps = 10}), editor.tx.add(tilesource, "collision_groups", {id = "player"}), editor.tx.add(tilesource, "collision_groups", {id = "obstacle"}), editor.tx.set(tilesource, "tile_collision_groups", { [1] = "player", [7] = "obstacle", [8] = "obstacle" }) }) ``` #### Редактирование tilemap Tilemap определяет свойство `layers` — список узлов слоёв tilemap. Каждый слой также имеет свойство `tiles`, представляющее собой неограниченную двумерную сетку тайлов на этом слое. Это отличается от движка: тайлы не имеют жёстких границ и могут быть размещены в любых координатах, включая отрицательные. Для редактирования тайлов API скриптов редактора предоставляет модуль `tilemap.tiles` со следующими функциями: - `tilemap.tiles.new()` — создаёт новую структуру данных для неограниченной двумерной сетки тайлов (в редакторе, в отличие от движка, tilemap не имеет границ, и координаты могут быть отрицательными); - `tilemap.tiles.get_tile(tiles, x, y)` — возвращает индекс тайла в заданной координате; - `tilemap.tiles.get_info(tiles, x, y)` — возвращает полную информацию о тайле по координатам (структура данных совпадает с `tilemap.get_tile_info` из движка); - `tilemap.tiles.iterator(tiles)` — создаёт итератор по всем тайлам в tilemap; - `tilemap.tiles.clear(tiles)` — удаляет все тайлы из tilemap; - `tilemap.tiles.set(tiles, x, y, tile_or_info)` — задаёт тайл в определённой координате; - `tilemap.tiles.remove(tiles, x, y)` — удаляет тайл из определённой координаты. Пример: как напечатать содержимое всей tilemap: ```lua local layers = editor.get("/level.tilemap", "layers") for i = 1, #layers do local layer = layers[i] local id = editor.get(layer, "id") local tiles = editor.get(layer, "tiles") print("layer " .. id .. ": {") for x, y, tile in tilemap.tiles.iterator(tiles) do print(" [" .. x .. ", " .. y .. "] = " .. tile) end print("}") end ``` Вот пример, который показывает, как добавить слой с тайлами в tilemap: ```lua local tiles = tilemap.tiles.new() tilemap.tiles.set(tiles, 1, 1, 2) editor.transact({ editor.tx.add("/level.tilemap", "layers", { id = "new_layer", tiles = tiles }) }) ``` ### Использование shell-команд Внутри обработчика `run` вы можете записывать данные в файлы (используя модуль `io`) и выполнять shell-команды (с помощью функции `editor.execute()`). При выполнении shell-команды можно захватить её вывод как строку и использовать далее в коде. Например, если вы хотите создать команду для форматирования JSON, которая вызывает глобально установленный [`jq`](https://jqlang.github.io/jq/), можно написать следующую команду: ```lua { label = "Format JSON", locations = {"Assets"}, query = {selection = {type = "resource", cardinality = "one"}}, action = function(opts) local path = editor.get(opts.selection, "path") return path:match(".json$") ~= nil end, run = function(opts) local text = editor.get(opts.selection, "text") local new_text = editor.execute("jq", "-n", "--argjson", "data", text, "$data", { reload_resources = false, -- don't reload resources since jq does not touch disk out = "capture" -- return text output instead of nothing }) editor.transact({ editor.tx.set(opts.selection, "text", new_text) }) end } ``` Поскольку эта команда вызывает shell-программу в режиме только для чтения (и уведомляет редактор об этом с помощью `reload_resources = false`), вы получаете преимущество: действие можно отменить. ::: sidenote Если вы хотите распространять свой скрипт редактора как библиотеку, возможно, стоит включить бинарную программу для платформ редактора в состав зависимости. Подробнее см. в разделе [Скрипты редактора в библиотеках](#editor-scripts-in-libraries). ::: ## Хуки жизненного цикла Существует специально обработанный файл скрипта редактора: `hooks.editor_script`, расположенный в корне вашего проекта, в том же каталоге, что и *game.project*. Этот и только этот скрипт редактора будет получать события жизненного цикла от редактора. Пример такого файла: ```lua local M = {} function M.on_build_started(opts) local file = io.open("assets/build.json", "w") file:write("{\"build_time\": \"".. os.date() .."\"}") file:close() end return M ``` Мы решили ограничить хуки жизненного цикла одним файлом скрипта редактора, потому что порядок, в котором выполняются хуки сборки, важнее, чем простота добавления очередного шага. Команды независимы друг от друга, поэтому порядок их отображения в меню не играет большой роли — в конечном итоге пользователь сам выбирает нужную команду. А вот с хуками порядок критичен: например, вы, вероятно, захотите создать контрольную сумму контента после того, как он был сжат... Наличие одного файла, который явно вызывает каждый шаг сборки в нужном порядке, позволяет решить эту проблему. Существующие хуки жизненного цикла, которые может определять `/hooks.editor_script`: - `on_build_started(opts)` — вызывается, когда игра собирается для запуска локально или на удалённой платформе через Project Build или Debug Start. Изменения, произведённые в этом хуке, попадут в собранную игру. Ошибка, выброшенная из этого хука, прервёт сборку. `opts` — таблица со следующими ключами: - `platform` — строка в формате `%arch%-%os%`, описывающая платформу сборки. Обычно совпадает со значением `editor.platform`. - `on_build_finished(opts)` — вызывается после завершения сборки, независимо от её успешности. `opts` содержит: - `platform` — как в `on_build_started` - `success` — флаг, указывающий на успех сборки: `true` или `false` - `on_bundle_started(opts)` — вызывается при создании билда или HTML5-версии игры. Аналогично `on_build_started`, изменения из этого хука попадут в билд, а ошибки его прервут. `opts` содержит: - `output_directory` — путь к каталогу с результатами сборки, например: `"/path/to/project/build/default/__htmlLaunchDir"` - `platform` — платформа, для которой создаётся сборка. Список возможных значений см. в [руководстве по Bob](/manuals/bob). - `variant` — тип сборки: `"debug"`, `"release"` или `"headless"` - `on_bundle_finished(opts)` — вызывается по завершении сборки, вне зависимости от результата. `opts` содержит те же поля, что и `on_bundle_started`, плюс ключ `success`. - `on_target_launched(opts)` — вызывается, когда пользователь запускает игру, и она успешно стартует. `opts` содержит ключ `url`, указывающий на адрес запущенного движка, например `"http://127.0.0.1:35405"` - `on_target_terminated(opts)` — вызывается, когда запущенная игра закрывается. Использует те же `opts`, что и `on_target_launched` Обратите внимание: хуки жизненного цикла работают только в редакторе и не выполняются при сборке через Bob из командной строки. ## Языковые серверы Редактор поддерживает подмножество [Language Server Protocol](https://microsoft.github.io/language-server-protocol/). Хотя в будущем мы планируем расширить поддержку LSP-функций, на данный момент редактор может только отображать диагностические сообщения (например, предупреждения линтера) и предлагать автодополнение. Чтобы определить языковой сервер, необходимо отредактировать функцию `get_language_servers` в вашем скрипте редактора следующим образом: ```lua function M.get_language_servers() local command = 'build/plugins/my-ext/plugins/bin/' .. editor.platform .. '/lua-lsp' if editor.platform == 'x86_64-win32' then command = command .. '.exe' end return { { languages = {'lua'}, watched_files = { { pattern = '**/.luacheckrc' } }, command = {command, '--stdio'} } } end ``` Редактор запустит языковой сервер, используя указанный `command`, через стандартный ввод и вывод процесса сервера для взаимодействия. Таблица определения языкового сервера может содержать следующие поля: - `languages` (обязательно) — список языков, поддерживаемых сервером, как указано [здесь](https://code.visualstudio.com/docs/languages/identifiers#_known-language-identifiers) (также поддерживаются расширения файлов); - `command` (обязательно) — массив, содержащий команду и её аргументы; - `watched_files` — массив таблиц с ключом `pattern` (глоб-шаблон), которые вызовут уведомление сервера о [изменении отслеживаемых файлов](https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#workspace_didChangeWatchedFiles). ## HTTP-сервер Каждый запущенный экземпляр редактора включает в себя HTTP-сервер. Этот сервер можно расширить с помощью скриптов редактора. Чтобы расширить HTTP-сервер редактора, необходимо добавить функцию `get_http_server_routes` в ваш скрипт редактора — она должна возвращать дополнительные маршруты: ```lua print("My route: " .. http.server.url .. "/my-extension") function M.get_http_server_routes() return { http.server.route("/my-extension", "GET", function(request) return http.server.response(200, "Hello world!") end) } end ``` После перезагрузки скриптов редактора вы увидите следующий вывод в консоли: `My route: http://0.0.0.0:12345/my-extension`. Если вы откроете эту ссылку в браузере, вы увидите сообщение `"Hello world!"`. Аргумент `request` на входе — это простая таблица Lua с информацией о запросе. Она содержит следующие ключи: `path` (URL-путь, начинающийся с `/`), `method` (HTTP-метод запроса, например `"GET"`), `headers` (таблица с именами заголовков в нижнем регистре), и, опционально, `query` (строка запроса) и `body` (если маршрут указывает, как интерпретировать тело запроса). Например, если вы хотите создать маршрут, который принимает тело в формате JSON, определите его с параметром-конвертером `"json"`: ```lua http.server.route("/my-extension/echo-request", "POST", "json", function(request) return http.server.json_response(request) end) ``` Вы можете протестировать этот эндпоинт в командной строке с помощью `curl` и `jq`: ```sh curl 'http://0.0.0.0:12345/my-extension/echo-request?q=1' -X POST --data '{"input": "json"}' | jq { "path": "/my-extension/echo-request", "method": "POST", "query": "q=1", "headers": { "host": "0.0.0.0:12345", "content-type": "application/x-www-form-urlencoded", "accept": "*/*", "user-agent": "curl/8.7.1", "content-length": "17" }, "body": { "input": "json" } } ``` Путь маршрута поддерживает шаблоны, которые могут быть извлечены из пути запроса и переданы в функцию-обработчик как часть запроса, например: ```lua http.server.route("/my-extension/setting/{category}.{key}", function(request) return http.server.response(200, tostring(editor.get("/game.project", request.category .. "." .. request.key))) end) ``` Теперь, если вы откроете, например, `http://0.0.0.0:12345/my-extension/setting/project.title`, вы увидите название вашей игры, взятое из файла `/game.project`. Помимо шаблона с одним сегментом пути, вы также можете сопоставить оставшуюся часть URL-пути, используя синтаксис `{*name}`. Например, вот простой эндпоинт файлового сервера, который обслуживает файлы из корня проекта: ```lua http.server.route("/my-extension/files/{*file}", function(request) local attrs = editor.external_file_attributes(request.file) if attrs.is_file then return http.server.external_file_response(request.file) else return 404 end end) ``` Теперь, если открыть, например, `http://0.0.0.0:12345/my-extension/files/main/main.collection` в браузере, отобразится содержимое файла `main/main.collection`. ## Скрипты редактора в библиотеках Вы можете публиковать библиотеки с командами для использования другими пользователями, и редактор автоматически их подхватит. Хуки, с другой стороны, не могут быть подхвачены автоматически, так как они должны быть определены в файле, расположенном в корневой папке проекта, в то время как библиотеки предоставляют только подкаталоги. Это сделано для того, чтобы предоставить больше контроля над процессом сборки: вы всё ещё можете создавать хуки жизненного цикла как обычные функции в `.lua`-файлах, чтобы пользователи вашей библиотеки могли подключать их в своём `/hooks.editor_script`. Также обратите внимание, что хотя зависимости отображаются в окне Assets, они не существуют как обычные файлы (это записи внутри zip-архива). Однако можно заставить редактор извлекать определённые файлы из зависимостей в папку `build/plugins/`. Чтобы это сделать, необходимо создать файл `ext.manifest` в вашей библиотеке и затем создать папку `plugins/bin/${platform}` в той же директории, где находится `ext.manifest`. Файлы в этой папке будут автоматически извлечены в `/build/plugins/${extension-path}/plugins/bin/${platform}`, чтобы ваши скрипты редактора могли ссылаться на них. ## Предпочтения Скрипты редактора могут определять и использовать предпочтения — постоянные, не зафиксированные в системе управления версиями данные, хранящиеся на компьютере пользователя. Эти предпочтения обладают тремя основными характеристиками: - типизированные: каждое предпочтение имеет определение схемы, которое включает тип данных и дополнительные метаданные, такие как значение по умолчанию; - с областью действия: предпочтения могут иметь область действия на уровне проекта или на уровне пользователя; - вложенные: каждый ключ предпочтения представляет собой строку с точечной нотацией, где первый сегмент определяет скрипт редактора, а остальные — вложенные свойства. Все предпочтения должны быть зарегистрированы путём определения их схемы: ```lua function M.get_prefs_schema() return { ["my_json_formatter.jq_path"] = editor.prefs.schema.string(), ["my_json_formatter.indent.size"] = editor.prefs.schema.integer({default = 2, scope = editor.prefs.SCOPE.PROJECT}), ["my_json_formatter.indent.type"] = editor.prefs.schema.enum({values = {"spaces", "tabs"}, scope = editor.prefs.SCOPE.PROJECT}), } end ``` После перезагрузки такого скрипта редактора редактор зарегистрирует эту схему. Затем скрипт может получать и задавать предпочтения, например: ```lua -- Get a specific preference editor.prefs.get("my_json_formatter.indent.type") -- Returns: "spaces" -- Get an entire preference group editor.prefs.get("my_json_formatter") -- Returns: -- { -- jq_path = "", -- indent = { -- size = 2, -- type = "spaces" -- } -- } -- Set multiple nested preferences at once editor.prefs.set("my_json_formatter.indent", { type = "tabs", size = 1 }) ``` ## Режимы выполнения Среда выполнения скриптов редактора использует 2 режима выполнения, которые в основном прозрачны для скриптов: **немедленный (immediate)** и **долгосрочный (long-running)**. **Немедленный** режим используется, когда редактору необходимо получить ответ от скрипта как можно быстрее. Например, обратные вызовы `active` у команд меню выполняются в немедленном режиме, поскольку эти проверки выполняются в UI-потоке редактора в ответ на действия пользователя и должны обновить интерфейс в пределах одного кадра. **Долгосрочный** режим используется, когда редактору не требуется мгновенный ответ от скрипта. Например, обратные вызовы `run` у команд меню выполняются в **долгосрочном** режиме, позволяя скрипту выполнять работу дольше. Некоторые функции, используемые в скриптах редактора, могут выполняться достаточно долго. Например, `editor.execute("git", "status", {reload_resources=false, out="capture"})` может занять до секунды на достаточно крупных проектах. Чтобы сохранить отзывчивость редактора и производительность, функции, которые могут выполняться долго, запрещены в контекстах, где от скрипта ожидается немедленный ответ. Попытка использовать такую функцию в немедленном контексте приведёт к ошибке: `Cannot use long-running editor function in immediate context`. Чтобы избежать этой ошибки, избегайте вызова таких функций в немедленных контекстах. Следующие функции считаются долгосрочными и не могут использоваться в немедленном режиме: - `editor.create_directory()`, `editor.delete_directory()`, `editor.save()`, `os.remove()` и `file:write()`: эти функции изменяют файлы на диске, вызывая необходимость синхронизации состояния ресурсов в памяти с данными на диске, что может занять несколько секунд в больших проектах. - `editor.execute()`: выполнение команд оболочки может занимать непредсказуемое количество времени. - `editor.transact()`: крупные транзакции с широко используемыми узлами могут занимать сотни миллисекунд, что слишком долго для обновления UI. Следующие контексты выполнения кода используют немедленный режим: - Обратные вызовы `active` у команд меню: редактору нужен ответ от скрипта в пределах одного UI-кадра. - Верхний уровень скриптов редактора: предполагается, что перезагрузка скриптов не должна иметь побочных эффектов. ## Действия ::: sidenote Ранее редактор взаимодействовал с Lua VM в блокирующем режиме, поэтому была строгая необходимость в том, чтобы скрипты редактора не блокировали выполнение, поскольку некоторые взаимодействия должны выполняться из UI-потока редактора. По этой причине, например, отсутствовали функции `editor.execute()` и `editor.transact()`. Выполнение скриптов и изменение состояния редактора осуществлялось путём возврата массива "действий" из хуков и обработчиков `run` команд. Теперь редактор взаимодействует с Lua VM в неблокирующем режиме, поэтому в этих действиях больше нет необходимости: использование функций, таких как `editor.execute()`, более удобно, лаконично и мощно. Эти действия теперь считаются **УСТАРЕВШИМИ**, хотя мы не планируем их удаление. ::: Скрипты редактора могут возвращать массив действий из функции `run` команды или из функций хуков в файле `/hooks.editor_script`. Эти действия затем будут выполнены редактором. Действие — это таблица, описывающая, что редактор должен сделать. Каждое действие содержит ключ `action`. Существуют два типа действий: отменяемые и неотменяемые. ### Отменяемые действия ::: sidenote Предпочтительно использовать `editor.transact()`. ::: Отменяемое действие может быть отменено после его выполнения. Если команда возвращает несколько отменяемых действий, они выполняются вместе и отменяются также вместе. Следует использовать отменяемые действия, когда это возможно. Их недостаток заключается в том, что они более ограничены по возможностям. Существующие отменяемые действия: - `"set"` — установка свойства узла в редакторе на некоторое значение. Пример: ```lua { action = "set", node_id = opts.selection, property = "text", value = "current time is " .. os.date() } ``` `"set"` действие требует наличия этих ключей: - `node_id` — идентификатора узла данных пользователя. Также вы можете использовать путь к ресурсу, вместо идентификатора узла, полученного от редактора, например `"/main/game.script"`; - `property` — свойство узла для установки, в настоящее время поддерживается только `"text"`; - `value` — новое значение для свойства. Для свойства `"text"` это должна быть строка. ### Неотменяемые действия ::: sidenote Предпочтительно использовать `editor.execute()`. ::: Неотменяемые действие, очищает историю отмены, поэтому, если вы хотите отменить такое действие, вам придется использовать другие средства, например, контроль версий. Существующие неотменяемые действия: - `"shell"` — выполняет сценарий оболочки. Пример: ```lua { action = "shell", command = { "./scripts/minify-json.sh", editor.get(opts.selection, "path"):sub(2) -- trim leading "/" } } ``` Действие `"shell"` требует ключ `command`, который представляет собой массив с командой и её аргументами. ### Смешивание действий и побочных эффектов Вы можете комбинировать отменяемые и неотменяемые действия. Действия выполняются последовательно, поэтому в зависимости от порядка выполнения вы можете потерять возможность отменить часть команды. Вместо возврата действий из функций, которые этого ожидают, вы можете напрямую читать и записывать файлы с помощью `io.open()`. Это вызовет перезагрузку ресурса и очистит историю отмен.