This translation is community contributed and may not be up to date. We only maintain the English version of the documentation. Read this manual in English

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

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

Если вы хотите распространять свой скрипт редактора как библиотеку, возможно, стоит включить бинарную программу для платформ редактора в состав зависимости. Подробнее см. в разделе Скрипты редактора в библиотеках.

Хуки жизненного цикла

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

Действия

Ранее редактор взаимодействовал с Lua VM в блокирующем режиме, поэтому была строгая необходимость в том, чтобы скрипты редактора не блокировали выполнение, поскольку некоторые взаимодействия должны выполняться из UI-потока редактора. По этой причине, например, отсутствовали функции editor.execute() и editor.transact(). Выполнение скриптов и изменение состояния редактора осуществлялось путём возврата массива “действий” из хуков и обработчиков run команд.

Теперь редактор взаимодействует с Lua VM в неблокирующем режиме, поэтому в этих действиях больше нет необходимости: использование функций, таких как editor.execute(), более удобно, лаконично и мощно. Эти действия теперь считаются УСТАРЕВШИМИ, хотя мы не планируем их удаление.

Скрипты редактора могут возвращать массив действий из функции run команды или из функций хуков в файле /hooks.editor_script. Эти действия затем будут выполнены редактором.

Действие — это таблица, описывающая, что редактор должен сделать. Каждое действие содержит ключ action. Существуют два типа действий: отменяемые и неотменяемые.

Отменяемые действия

Предпочтительно использовать 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" это должна быть строка.

Неотменяемые действия

Предпочтительно использовать editor.execute().

Неотменяемые действие, очищает историю отмены, поэтому, если вы хотите отменить такое действие, вам придется использовать другие средства, например, контроль версий.

Существующие неотменяемые действия:

  • "shell" — выполняет сценарий оболочки. Пример:
    {
      action = "shell",
      command = {
        "./scripts/minify-json.sh",
        editor.get(opts.selection, "path"):sub(2) -- trim leading "/"
      }
    }
    

    Действие "shell" требует ключ command, который представляет собой массив с командой и её аргументами.

Смешивание действий и побочных эффектов

Вы можете комбинировать отменяемые и неотменяемые действия. Действия выполняются последовательно, поэтому в зависимости от порядка выполнения вы можете потерять возможность отменить часть команды.

Вместо возврата действий из функций, которые этого ожидают, вы можете напрямую читать и записывать файлы с помощью io.open(). Это вызовет перезагрузку ресурса и очистит историю отмен.