editor-scripts.md 49 KB


title: Скрипты редактора

brief: Это руководство объясняет, как расширить редактор с помощью Lua

Скрипты редактора

Вы можете создавать пользовательские пункты меню и хуки жизненного цикла редактора, используя файлы 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).

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.
  • 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"}
        }
    })
})

Редактирование tilesource

Помимо свойств, отображаемых в Outline, tilesource определяет следующие дополнительные свойства:

  • animations — список узлов анимаций tilesource
  • collision_groups — список узлов групп столкновений tilesource
  • tile_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

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
    })
})

Использование shell-команд

Внутри обработчика 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-сервер. Этот сервер можно расширить с помощью скриптов редактора. Чтобы расширить 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(). Это вызовет перезагрузку ресурса и очистит историю отмен.