title: Скрипты редактора
Вы можете создавать пользовательские пункты меню и хуки жизненного цикла редактора, используя файлы Lua со специальным расширением: .editor_script
. Используя эту систему, вы можете настраивать редактор для улучшения рабочего процесса разработки.
Сценарии редактора выполняются внутри редактора, в Lua VM, эмулированной Java VM. Все скрипты работают в одном едином окружении, а это означает, что они могут взаимодействовать друг с другом. Вы можете подключать модули Lua, также как и файлы .script
, но версия Lua, работающая внутри редактора, отличается, поэтому убедитесь, что ваш общий код совместим. Редактор использует Lua версии 5.2.x, а точнее luaj — в настоящее время это единственное жизнеспособное решение для запуска Lua на JVM. Кроме того, есть некоторые ограничения:
debug
;os.execute
, однако предоставляется аналог — editor.execute()
;os.tmpname
и io.tmpfile
— в настоящее время скрипты редактора могут обращаться к файлам только внутри каталога проекта;os.rename
, хотя мы планируем его добавить;os.exit
и os.setlocale
;Все расширения редактора, определённые в скриптах редактора, загружаются при открытии проекта. При извлечении библиотек расширения перезагружаются, так как в библиотеках, от которых вы зависите, могут появиться новые скрипты редактора. Во время этой перезагрузки изменения в ваших собственных скриптах редактора не учитываются, поскольку вы можете находиться в процессе их изменения. Чтобы перезагрузить и их, нужно выполнить команду Project → Reload Editor Scripts.
.editor_script
Каждый скрипт редактора должен возвращать модуль, подобный этому:
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 и lifecycle hooks).
Вы можете взаимодействовать с редактором, используя пакет 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.*
для подробностей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.editor.prefs.*
— функции для работы с настройками редактора. См. раздел preferences.Полную документацию по API редактора можно найти здесь.
Если модуль сценария редактора определяет функцию get_commands
, она будет вызываться при перезагрузке расширения, и возвращенные команды будут доступны для использования внутри редактора в строке меню или в контекстных меню на панелях Assets и Outline. Например:
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()
, где вы объединяете одну или несколько модификаций в один шаг, который можно отменить. Например, если вы хотите сбросить трансформацию игрового объекта, можно написать такую команду:
{
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
команды:
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:
local all_images = {} ---@type table<string, true>
-- 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:
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"}
}
})
})
Помимо свойств, отображаемых в Outline, tilesource определяет следующие дополнительные свойства:
animations
— список узлов анимаций tilesourcecollision_groups
— список узлов групп столкновений tilesourcetile_collision_groups
— таблица назначений групп столкновений для конкретных тайлов в tilesourceНапример, вот как можно настроить tilesource:
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 определяет свойство 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:
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:
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
})
})
Внутри обработчика run
вы можете записывать данные в файлы (используя модуль io
) и выполнять shell-команды (с помощью функции editor.execute()
). При выполнении shell-команды можно захватить её вывод как строку и использовать далее в коде. Например, если вы хотите создать команду для форматирования JSON, которая вызывает глобально установленный jq
, можно написать следующую команду:
{
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 Если вы хотите распространять свой скрипт редактора как библиотеку, возможно, стоит включить бинарную программу для платформ редактора в состав зависимости. Подробнее см. в разделе Скрипты редактора в библиотеках. :::
Существует специально обработанный файл скрипта редактора: hooks.editor_script
, расположенный в корне вашего проекта, в том же каталоге, что и game.project. Этот и только этот скрипт редактора будет получать события жизненного цикла от редактора. Пример такого файла:
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.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. Хотя в будущем мы планируем расширить поддержку LSP-функций, на данный момент редактор может только отображать диагностические сообщения (например, предупреждения линтера) и предлагать автодополнение.
Чтобы определить языковой сервер, необходимо отредактировать функцию get_language_servers
в вашем скрипте редактора следующим образом:
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
(обязательно) — список языков, поддерживаемых сервером, как указано здесь (также поддерживаются расширения файлов);command
(обязательно) — массив, содержащий команду и её аргументы;watched_files
— массив таблиц с ключом pattern
(глоб-шаблон), которые вызовут уведомление сервера о изменении отслеживаемых файлов.Каждый запущенный экземпляр редактора включает в себя HTTP-сервер. Этот сервер можно расширить с помощью скриптов редактора. Чтобы расширить HTTP-сервер редактора, необходимо добавить функцию get_http_server_routes
в ваш скрипт редактора — она должна возвращать дополнительные маршруты:
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"
:
http.server.route("/my-extension/echo-request", "POST", "json", function(request)
return http.server.json_response(request)
end)
Вы можете протестировать этот эндпоинт в командной строке с помощью curl
и jq
:
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"
}
}
Путь маршрута поддерживает шаблоны, которые могут быть извлечены из пути запроса и переданы в функцию-обработчик как часть запроса, например:
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}
. Например, вот простой эндпоинт файлового сервера, который обслуживает файлы из корня проекта:
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}
, чтобы ваши скрипты редактора могли ссылаться на них.
Скрипты редактора могут определять и использовать предпочтения — постоянные, не зафиксированные в системе управления версиями данные, хранящиеся на компьютере пользователя. Эти предпочтения обладают тремя основными характеристиками:
Все предпочтения должны быть зарегистрированы путём определения их схемы:
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
После перезагрузки такого скрипта редактора редактор зарегистрирует эту схему. Затем скрипт может получать и задавать предпочтения, например:
-- 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"
— установка свойства узла в редакторе на некоторое значение. Пример:
{
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"
— выполняет сценарий оболочки. Пример:
{
action = "shell",
command = {
"./scripts/minify-json.sh",
editor.get(opts.selection, "path"):sub(2) -- trim leading "/"
}
}
Действие "shell"
требует ключ command
, который представляет собой массив с командой и её аргументами.
Вы можете комбинировать отменяемые и неотменяемые действия. Действия выполняются последовательно, поэтому в зависимости от порядка выполнения вы можете потерять возможность отменить часть команды.
Вместо возврата действий из функций, которые этого ожидают, вы можете напрямую читать и записывать файлы с помощью io.open()
. Это вызовет перезагрузку ресурса и очистит историю отмен.