/** * lua-monaco-setup — регистрация Lua-фич в Monaco: * 1) Подсветка через встроенный 'lua' language (Monaco поставляется с basic-languages/lua) * 2) Автодополнение Roblox-API (Vector3.new, Color3.fromRGB, script.Parent, game.Players, ...) * 3) Hover-документация (наведя на Vector3 — описание + пример) * 4) Подсветка ошибок через luaparse (на этапе 7, опционально) * * Регистрируется ОДИН раз глобально через флаг monaco.__rbxLuaRegistered. */ const ROBLOX_LUA_API = [ // === Глобальные функции === { kind: 'function', name: 'print', insertText: 'print($0)', doc: 'Выводит сообщения в Output-панель.\n```lua\nprint("Привет", x, y)\n```' }, { kind: 'function', name: 'warn', insertText: 'warn($0)', doc: 'Выводит предупреждение (жёлтым).\n```lua\nwarn("Что-то не так")\n```' }, { kind: 'function', name: 'error', insertText: 'error(${1:"сообщение"})', doc: 'Бросает ошибку, останавливая текущий скрипт.\n```lua\nerror("Здоровье < 0")\n```' }, { kind: 'function', name: 'wait', insertText: 'wait(${1:1})', doc: 'Приостанавливает скрипт на N секунд (заменяется на `task.wait` в новом коде).' }, { kind: 'function', name: 'tick', insertText: 'tick()', doc: 'Возвращает количество секунд с эпохи (как `os.time()`, но дробное).' }, { kind: 'function', name: 'pcall', insertText: 'pcall(${1:fn}, $0)', doc: 'Защищённый вызов. Возвращает `success, result|error`.\n```lua\nlocal ok, err = pcall(function() risky() end)\nif not ok then warn(err) end\n```' }, { kind: 'function', name: 'xpcall', insertText: 'xpcall(${1:fn}, ${2:handler})', doc: 'Защищённый вызов с кастомным обработчиком ошибки.' }, { kind: 'function', name: 'tostring', insertText: 'tostring($0)', doc: 'Преобразует значение в строку.' }, { kind: 'function', name: 'tonumber', insertText: 'tonumber($0)', doc: 'Преобразует строку в число. Возвращает nil если не число.' }, { kind: 'function', name: 'type', insertText: 'type($0)', doc: 'Возвращает строку с типом: "nil", "number", "string", "boolean", "table", "function", "userdata".' }, { kind: 'function', name: 'typeof', insertText: 'typeof($0)', doc: 'Расширенная версия type — для Roblox-типов вернёт "Vector3", "CFrame", "Color3", "Instance".' }, { kind: 'function', name: 'ipairs', insertText: 'ipairs(${1:t})', doc: 'Итератор по числовым ключам массива.\n```lua\nfor i, v in ipairs(arr) do ... end\n```' }, { kind: 'function', name: 'pairs', insertText: 'pairs(${1:t})', doc: 'Итератор по всем ключам таблицы.\n```lua\nfor k, v in pairs(t) do ... end\n```' }, { kind: 'function', name: 'next', insertText: 'next(${1:t}, $0)', doc: 'Возвращает следующую пару ключ-значение в таблице.' }, { kind: 'function', name: 'select', insertText: 'select(${1:1}, $0)', doc: 'select("#", ...) — количество аргументов. select(n, ...) — n-й и далее аргументы.' }, { kind: 'function', name: 'unpack', insertText: 'unpack(${1:t})', doc: 'Распаковывает массив в значения. (В Lua 5.4 — `table.unpack`)' }, { kind: 'function', name: 'setmetatable', insertText: 'setmetatable(${1:t}, ${2:mt})', doc: 'Устанавливает metatable для таблицы.' }, { kind: 'function', name: 'getmetatable', insertText: 'getmetatable($0)', doc: 'Возвращает metatable или nil.' }, { kind: 'function', name: 'rawget', insertText: 'rawget(${1:t}, ${2:key})', doc: 'Чтение без вызова __index metatable.' }, { kind: 'function', name: 'rawset', insertText: 'rawset(${1:t}, ${2:key}, ${3:value})', doc: 'Запись без вызова __newindex metatable.' }, // === task.* === { kind: 'module', name: 'task', insertText: 'task', doc: 'Современный API планировщика Roblox-Lua.\nЗаменяет `wait`, `spawn`, `delay`, `defer` из старого API.' }, { kind: 'function', name: 'task.wait', insertText: 'task.wait(${1:1})', doc: 'Приостанавливает на N секунд.\nВозвращает фактическое время ожидания.\n```lua\nlocal dt = task.wait(0.5)\n```' }, { kind: 'function', name: 'task.spawn', insertText: 'task.spawn(${1:function() end})', doc: 'Немедленно запускает функцию как coroutine.\n```lua\ntask.spawn(function() heavy() end)\n```' }, { kind: 'function', name: 'task.delay', insertText: 'task.delay(${1:1}, ${2:function() end})', doc: 'Отложенный запуск функции через N секунд.\n```lua\ntask.delay(3, function() print("через 3 сек") end)\n```' }, { kind: 'function', name: 'task.defer', insertText: 'task.defer(${1:function() end})', doc: 'Запуск в следующем кадре (после Heartbeat).' }, // === Vector3 === { kind: 'class', name: 'Vector3', insertText: 'Vector3', doc: '3D-вектор в Roblox.\nКонструктор: `Vector3.new(x, y, z)`.\nКонстанты: `Vector3.zero`, `Vector3.one`, `Vector3.xAxis`, `Vector3.yAxis`, `Vector3.zAxis`.' }, { kind: 'function', name: 'Vector3.new', insertText: 'Vector3.new(${1:0}, ${2:0}, ${3:0})', doc: 'Создаёт `Vector3(x, y, z)`.\n```lua\nlocal v = Vector3.new(10, 5, 0)\nprint(v.X, v.Y, v.Z, v.Magnitude)\n```' }, { kind: 'function', name: 'Vector3.zero', insertText: 'Vector3.zero', doc: '`Vector3(0, 0, 0)`.' }, { kind: 'function', name: 'Vector3.one', insertText: 'Vector3.one', doc: '`Vector3(1, 1, 1)`.' }, { kind: 'function', name: 'Vector3.xAxis', insertText: 'Vector3.xAxis', doc: '`Vector3(1, 0, 0)`.' }, { kind: 'function', name: 'Vector3.yAxis', insertText: 'Vector3.yAxis', doc: '`Vector3(0, 1, 0)`.' }, { kind: 'function', name: 'Vector3.zAxis', insertText: 'Vector3.zAxis', doc: '`Vector3(0, 0, 1)`.' }, // === Color3 === { kind: 'class', name: 'Color3', insertText: 'Color3', doc: 'Цвет RGB в Roblox.\nКомпоненты `R`, `G`, `B` в диапазоне [0, 1].' }, { kind: 'function', name: 'Color3.new', insertText: 'Color3.new(${1:1}, ${2:1}, ${3:1})', doc: 'Создаёт `Color3(r, g, b)`, где компоненты в [0, 1].' }, { kind: 'function', name: 'Color3.fromRGB', insertText: 'Color3.fromRGB(${1:255}, ${2:255}, ${3:255})', doc: 'Создаёт `Color3` из 0-255 RGB.\n```lua\nlocal red = Color3.fromRGB(255, 0, 0)\n```' }, { kind: 'function', name: 'Color3.fromHSV', insertText: 'Color3.fromHSV(${1:0}, ${2:1}, ${3:1})', doc: 'Создаёт цвет из HSV-компонентов в [0, 1].' }, { kind: 'function', name: 'Color3.fromHex', insertText: 'Color3.fromHex(${1:"#FF0000"})', doc: 'Создаёт цвет из hex-строки.' }, // === CFrame === { kind: 'class', name: 'CFrame', insertText: 'CFrame', doc: 'Coordinate Frame — позиция + поворот в 3D.\nИспользуется для трансформаций Part.CFrame.' }, { kind: 'function', name: 'CFrame.new', insertText: 'CFrame.new(${1:0}, ${2:0}, ${3:0})', doc: 'Создаёт CFrame в указанной позиции.' }, { kind: 'function', name: 'CFrame.lookAt', insertText: 'CFrame.lookAt(${1:eye}, ${2:target})', doc: 'CFrame, направленный из eye на target.' }, { kind: 'function', name: 'CFrame.Angles', insertText: 'CFrame.Angles(${1:0}, ${2:0}, ${3:0})', doc: 'CFrame только с поворотом (в радианах).' }, { kind: 'function', name: 'CFrame.fromEulerAnglesXYZ', insertText: 'CFrame.fromEulerAnglesXYZ(${1:0}, ${2:0}, ${3:0})', doc: 'CFrame с поворотом по эйлеровым углам.' }, // === UDim2 / Vector2 === { kind: 'class', name: 'UDim2', insertText: 'UDim2', doc: 'Размер/позиция GUI: процент + пиксели по обеим осям.' }, { kind: 'function', name: 'UDim2.new', insertText: 'UDim2.new(${1:0}, ${2:0}, ${3:0}, ${4:0})', doc: '`UDim2.new(scaleX, offsetX, scaleY, offsetY)`.\n```lua\nframe.Position = UDim2.new(0.5, 0, 0.5, 0) -- центр экрана\n```' }, { kind: 'function', name: 'UDim2.fromScale', insertText: 'UDim2.fromScale(${1:0.5}, ${2:0.5})', doc: 'Только процентные размеры.' }, { kind: 'function', name: 'UDim2.fromOffset', insertText: 'UDim2.fromOffset(${1:100}, ${2:100})', doc: 'Только пиксельные размеры.' }, { kind: 'class', name: 'Vector2', insertText: 'Vector2', doc: '2D-вектор.' }, { kind: 'function', name: 'Vector2.new', insertText: 'Vector2.new(${1:0}, ${2:0})', doc: '`Vector2(x, y)`.' }, { kind: 'class', name: 'UDim', insertText: 'UDim', doc: 'Одномерная UDim (scale + offset).' }, { kind: 'function', name: 'UDim.new', insertText: 'UDim.new(${1:0}, ${2:0})', doc: '`UDim.new(scale, offset)`.' }, // === Instance === { kind: 'class', name: 'Instance', insertText: 'Instance', doc: 'Базовый класс всех объектов Roblox.' }, { kind: 'function', name: 'Instance.new', insertText: 'Instance.new("${1:Part}", ${2:workspace})', doc: 'Создаёт новый объект указанного класса.\n```lua\nlocal part = Instance.new("Part", workspace)\npart.Size = Vector3.new(4, 1, 4)\npart.Position = Vector3.new(0, 10, 0)\n```' }, // === game / services === { kind: 'variable', name: 'game', insertText: 'game', doc: 'Корень DataModel. `game:GetService("Players")` — доступ к сервисам.' }, { kind: 'variable', name: 'workspace', insertText: 'workspace', doc: 'Сокращение для `game.Workspace`. Содержит все Part-объекты сцены.' }, { kind: 'variable', name: 'script', insertText: 'script', doc: 'Текущий скрипт. `script.Parent` — объект-носитель.\n```lua\nlocal part = script.Parent\npart.Touched:Connect(function(hit) ... end)\n```' }, // === Enum === { kind: 'enum', name: 'Enum', insertText: 'Enum', doc: 'Перечисления Roblox: KeyCode, Material, UserInputType, EasingStyle, EasingDirection, HumanoidStateType.' }, { kind: 'enum', name: 'Enum.KeyCode', insertText: 'Enum.KeyCode.${1:W}', doc: 'Клавиши клавиатуры: W, A, S, D, Space, LeftShift, Q, E, F, R, T, ..., One, Two, ..., Up, Down.' }, { kind: 'enum', name: 'Enum.UserInputType', insertText: 'Enum.UserInputType.${1:MouseButton1}', doc: 'Типы ввода: MouseButton1/2/3, Keyboard, Touch, MouseMovement, MouseWheel.' }, { kind: 'enum', name: 'Enum.Material', insertText: 'Enum.Material.${1:Plastic}', doc: 'Материалы: Plastic, Wood, Metal, Neon, Glass, Sand, Ice, Grass, Concrete.' }, { kind: 'enum', name: 'Enum.HumanoidStateType', insertText: 'Enum.HumanoidStateType.${1:Running}', doc: 'Состояния Humanoid: Running, Jumping, Freefall, Landed, Dead, Climbing, Swimming, Seated.' }, ]; // === Сниппеты быстрого старта (готовые шаблоны) === const ROBLOX_LUA_SNIPPETS = [ { label: 'killbrick', documentation: 'KillBrick — убивает игрока при касании.', insertText: [ 'local part = script.Parent', 'part.Touched:Connect(function(hit)', '\tlocal humanoid = hit.Parent:FindFirstChildOfClass("Humanoid")', '\tif humanoid then', '\t\thumanoid.Health = 0', '\tend', 'end)', ].join('\n'), }, { label: 'teleportpad', documentation: 'TeleportPad — телепортирует игрока в указанную точку.', insertText: [ 'local destination = Vector3.new(${1:0}, ${2:50}, ${3:0})', 'local pad = script.Parent', 'pad.Touched:Connect(function(hit)', '\tlocal root = hit.Parent:FindFirstChild("HumanoidRootPart")', '\tif root then', '\t\troot.CFrame = CFrame.new(destination)', '\tend', 'end)', ].join('\n'), }, { label: 'coin', documentation: 'Coin — даёт игроку монету при касании, потом исчезает.', insertText: [ 'local coin = script.Parent', 'local collected = false', 'coin.Touched:Connect(function(hit)', '\tif collected then return end', '\tif hit.Parent:FindFirstChildOfClass("Humanoid") then', '\t\tcollected = true', '\t\tprint("Монета собрана!")', '\t\tcoin:Destroy()', '\tend', 'end)', ].join('\n'), }, { label: 'heartbeat', documentation: 'RunService.Heartbeat — кадровый callback.', insertText: [ 'local RunService = game:GetService("RunService")', 'RunService.Heartbeat:Connect(function(dt)', '\t${0:-- код, выполняется каждый кадр}', 'end)', ].join('\n'), }, { label: 'playeradded', documentation: 'PlayerAdded — реакция на захождение игрока.', insertText: [ 'local Players = game:GetService("Players")', 'Players.PlayerAdded:Connect(function(player)', '\tprint("Игрок зашёл:", player.Name)', '\t${0:}', 'end)', ].join('\n'), }, { label: 'spinpart', documentation: 'SpinPart — вращающаяся платформа.', insertText: [ 'local RunService = game:GetService("RunService")', 'local part = script.Parent', 'local speed = ${1:2} -- радиан/сек', 'RunService.Heartbeat:Connect(function(dt)', '\tpart.CFrame = part.CFrame * CFrame.Angles(0, speed * dt, 0)', 'end)', ].join('\n'), }, ]; export function registerLuaInMonaco(monaco) { if (monaco.__rbxLuaRegistered) return; monaco.__rbxLuaRegistered = true; // 1. CompletionProvider — автодополнение monaco.languages.registerCompletionItemProvider('lua', { triggerCharacters: ['.', ':', '"', "'"], provideCompletionItems: (model, position) => { const word = model.getWordUntilPosition(position); const range = { startLineNumber: position.lineNumber, endLineNumber: position.lineNumber, startColumn: word.startColumn, endColumn: word.endColumn, }; const suggestions = []; const kindMap = { 'function': monaco.languages.CompletionItemKind.Function, 'class': monaco.languages.CompletionItemKind.Class, 'module': monaco.languages.CompletionItemKind.Module, 'enum': monaco.languages.CompletionItemKind.Enum, 'variable': monaco.languages.CompletionItemKind.Variable, }; for (const item of ROBLOX_LUA_API) { suggestions.push({ label: item.name, kind: kindMap[item.kind] || monaco.languages.CompletionItemKind.Text, insertText: item.insertText, insertTextRules: monaco.languages.CompletionItemInsertTextRule.InsertAsSnippet, documentation: { value: item.doc, isTrusted: true }, range, }); } for (const snip of ROBLOX_LUA_SNIPPETS) { suggestions.push({ label: snip.label, kind: monaco.languages.CompletionItemKind.Snippet, insertText: snip.insertText, insertTextRules: monaco.languages.CompletionItemInsertTextRule.InsertAsSnippet, documentation: { value: snip.documentation, isTrusted: true }, detail: 'Сниппет Roblox-Lua', range, }); } return { suggestions }; }, }); // 2. HoverProvider — подсказки при наведении const lookupTable = new Map(); for (const item of ROBLOX_LUA_API) lookupTable.set(item.name, item); monaco.languages.registerHoverProvider('lua', { provideHover: (model, position) => { const word = model.getWordAtPosition(position); if (!word) return null; // Пробуем найти точное совпадение или с префиксом (Vector3.new) let found = lookupTable.get(word.word); if (!found) { // Возможно курсор на середине A.B — попробуем собрать всю цепочку const line = model.getLineContent(position.lineNumber); // Ищем имя.имя.имя на позиции const left = line.slice(0, word.endColumn - 1); const m = left.match(/[A-Za-z_][\w.]*$/); if (m) found = lookupTable.get(m[0]); } if (!found) return null; return { range: new monaco.Range( position.lineNumber, word.startColumn, position.lineNumber, word.endColumn, ), contents: [ { value: `**${found.name}**` }, { value: found.doc }, ], }; }, }); }