627 lines
23 KiB
Lua
627 lines
23 KiB
Lua
--[[
|
|
*
|
|
* Singe 3
|
|
* Copyright (C) 2006-2026 Scott Duensing <scott@kangaroopunch.com>
|
|
*
|
|
* This program is free software; you can redistribute it and/or
|
|
* modify it under the terms of the GNU General Public License
|
|
* as published by the Free Software Foundation; either version 3
|
|
* of the License, or (at your option) any later version.
|
|
*
|
|
* This program is distributed in the hope that it will be useful,
|
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
* GNU General Public License for more details.
|
|
*
|
|
* You should have received a copy of the GNU General Public License
|
|
* along with this program; if not, write to the Free Software
|
|
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
|
|
* 02110-1301, USA.
|
|
*
|
|
*
|
|
--]]
|
|
|
|
|
|
-- Singe for ZeroBrane Studio. Adds Singe as an interpreter, so Project > Run (F6) and Project >
|
|
-- Start Debugging (F5) launch the game, with breakpoints, stepping, the Stack and Watch windows, and
|
|
-- a stop in the IDE when an error is about to end the game; autocompletion and tooltips for the whole
|
|
-- Singe API, in ZeroBrane Studio's language when the Reference is kept in it; .singe files edited as
|
|
-- Lua, .rml as XML and .rcss as CSS; a toolbar button; and a way into the Reference from the Help
|
|
-- menu and from the word under the mouse.
|
|
--
|
|
-- Install by adding one line to ZeroBrane Studio's user.lua (Edit > Preferences > Settings: User),
|
|
-- pointing at the copy Singe unpacks into its own folder, which stays current as Singe is updated:
|
|
--
|
|
-- package "/path/to/Singe/Singe/ZeroBrane/singe.lua"
|
|
--
|
|
-- or by copying singe.lua and the singe folder beside it into ZeroBrane Studio's packages folder.
|
|
--
|
|
-- Settings, all optional, also in user.lua:
|
|
--
|
|
-- path.singe = "/path/to/Singe-v3.00-Linux-x86_64" -- otherwise found, or asked for once
|
|
-- singe = { reload = false } -- do not pass --reload
|
|
-- singe = { reference = "/path/to/Reference.html" } -- otherwise found beside Singe
|
|
|
|
|
|
local MENU_CHOOSE = ID("singe.choose")
|
|
local MENU_DEBUG = ID("singe.debug")
|
|
local MENU_LOOKUP = ID("singe.lookup")
|
|
local MENU_REFERENCE = ID("singe.reference")
|
|
local MENU_RELOAD = ID("singe.reload")
|
|
local INTERPRETER = "singe"
|
|
local STOP_FILE = "Debugger.singe" -- Where Singe stops when an error is about to end the game
|
|
local LOOPBACK = "127.0.0.1"
|
|
|
|
-- What each platform's Singe is called: Singe-v3.00b1-Linux-x86_64, Singe-v3.00-Windows-x86_64.exe.
|
|
local PLATFORM = {
|
|
Windows = { name = "Windows", suffix = "%.exe$" },
|
|
Unix = { name = "Linux", suffix = "$" },
|
|
Macintosh = { name = "Macos", suffix = "$" },
|
|
}
|
|
-- ZeroBrane Studio's own word for a Linux machine's architecture, from uname, as Singe spells it.
|
|
local ARCHITECTURE = { x64 = "x86_64", x86 = "x86", aarch64 = "aarch64", armhf = "armhf" }
|
|
|
|
-- The package's own words, in ZeroBrane Studio's language when the Reference is kept in it.
|
|
-- Written in decimal escapes, so this file stays ASCII: \195\169 is a UTF-8 e acute.
|
|
local WORDS = {
|
|
en = {
|
|
debug = "Debug With Singe",
|
|
choose = "Choose Singe Executable...",
|
|
reload = "Reload Singe When a Script Changes",
|
|
reference = "Singe Reference",
|
|
lookUp = "Look Up in Singe Reference",
|
|
chooseTitle = "Choose the Singe executable",
|
|
noReference = "The Singe Reference was not found. Unpack the documentation download (Singe-Docs) beside Singe, or set singe = { reference = \"/path/to/Reference.html\" } in user.lua.",
|
|
stopped = "Singe stopped at an error: %s",
|
|
stoppedHelp = "The Stack window shows where it happened; Continue (F5) lets the game end as it would have.",
|
|
nothingRun = "Singe: no executable chosen, so nothing was run.",
|
|
using = "Singe: using %s.",
|
|
},
|
|
fr = {
|
|
debug = "D\195\169boguer avec Singe",
|
|
choose = "Choisir l'ex\195\169cutable de Singe...",
|
|
reload = "Relancer Singe quand un script change",
|
|
reference = "R\195\169f\195\169rence de Singe",
|
|
lookUp = "Chercher dans la R\195\169f\195\169rence de Singe",
|
|
chooseTitle = "Choisissez l'ex\195\169cutable de Singe",
|
|
noReference = "La R\195\169f\195\169rence de Singe est introuvable. D\195\169compressez le t\195\169l\195\169chargement de la documentation (Singe-Docs) \195\160 c\195\180t\195\169 de Singe, ou indiquez singe = { reference = \"/chemin/vers/Reference.html\" } dans user.lua.",
|
|
stopped = "Singe s'est arr\195\170t\195\169 sur une erreur : %s",
|
|
stoppedHelp = "La fen\195\170tre Pile d'ex\195\169cution montre o\195\185 elle s'est produite ; Continuer (F5) laisse le jeu se terminer comme il l'aurait fait.",
|
|
nothingRun = "Singe : aucun ex\195\169cutable choisi, rien n'a \195\169t\195\169 lanc\195\169.",
|
|
using = "Singe : utilise %s.",
|
|
},
|
|
it = {
|
|
debug = "Debug con Singe",
|
|
choose = "Scegli l'eseguibile di Singe...",
|
|
reload = "Ricarica Singe quando cambia uno script",
|
|
reference = "Riferimento di Singe",
|
|
lookUp = "Cerca nel Riferimento di Singe",
|
|
chooseTitle = "Scegli l'eseguibile di Singe",
|
|
noReference = "Il Riferimento di Singe non \195\168 stato trovato. Decomprimi il download della documentazione (Singe-Docs) accanto a Singe, oppure imposta singe = { reference = \"/percorso/di/Reference.html\" } in user.lua.",
|
|
stopped = "Singe si \195\168 fermato su un errore: %s",
|
|
stoppedHelp = "La finestra Stack mostra dove \195\168 successo; Continua (F5) lascia che il gioco finisca come avrebbe fatto.",
|
|
nothingRun = "Singe: nessun eseguibile scelto, quindi non \195\168 stato avviato nulla.",
|
|
using = "Singe: uso %s.",
|
|
},
|
|
de = {
|
|
debug = "Mit Singe debuggen",
|
|
choose = "Singe-Programm w\195\164hlen...",
|
|
reload = "Singe neu laden, wenn sich ein Skript \195\164ndert",
|
|
reference = "Singe-Referenz",
|
|
lookUp = "In der Singe-Referenz nachschlagen",
|
|
chooseTitle = "Das Singe-Programm w\195\164hlen",
|
|
noReference = "Die Singe-Referenz wurde nicht gefunden. Entpacke den Dokumentations-Download (Singe-Docs) neben Singe, oder setze singe = { reference = \"/pfad/zu/Reference.html\" } in user.lua.",
|
|
stopped = "Singe hat bei einem Fehler angehalten: %s",
|
|
stoppedHelp = "Das Fenster Stack zeigt, wo er passiert ist; Fortsetzen (F5) l\195\164sst das Spiel enden, wie es geendet h\195\164tte.",
|
|
nothingRun = "Singe: kein Programm gew\195\164hlt, daher wurde nichts gestartet.",
|
|
using = "Singe: verwende %s.",
|
|
},
|
|
es = {
|
|
debug = "Depurar con Singe",
|
|
choose = "Elegir el ejecutable de Singe...",
|
|
reload = "Recargar Singe cuando cambie un script",
|
|
reference = "Referencia de Singe",
|
|
lookUp = "Buscar en la Referencia de Singe",
|
|
chooseTitle = "Elige el ejecutable de Singe",
|
|
noReference = "No se encontr\195\179 la Referencia de Singe. Descomprime la descarga de la documentaci\195\179n (Singe-Docs) junto a Singe, o pon singe = { reference = \"/ruta/a/Reference.html\" } en user.lua.",
|
|
stopped = "Singe se detuvo en un error: %s",
|
|
stoppedHelp = "La ventana Pila muestra d\195\179nde ocurri\195\179; Continuar (F5) deja que el juego termine como lo habr\195\173a hecho.",
|
|
nothingRun = "Singe: no se eligi\195\179 ning\195\186n ejecutable, as\195\173 que no se ejecut\195\179 nada.",
|
|
using = "Singe: se usa %s.",
|
|
},
|
|
}
|
|
|
|
-- The sizes of the dragon Singe unpacks beside this file, smallest first.
|
|
local ICON_SIZES = { 16, 24, 32, 48 }
|
|
|
|
|
|
local api = {} -- The loaded API, which also says where Look Up opens the Reference
|
|
local apiTable = nil
|
|
local architectures = nil
|
|
local findSinge = nil
|
|
local gameScript = nil
|
|
local isStopFile = nil
|
|
local language = nil
|
|
local lookUp = nil
|
|
local lookUpPosition = nil -- Where the editor's context menu was opened
|
|
local openReference = nil
|
|
local packageDir = nil
|
|
local parentFolder = nil
|
|
local plugin = nil
|
|
local referencePage = nil
|
|
local reloading = nil
|
|
local saveSetting = nil
|
|
local singeFolder = nil
|
|
local singeVersion = nil
|
|
local specmapOld = {}
|
|
local subfolders = nil
|
|
local toolbarBitmap = nil
|
|
local withSeparator = nil
|
|
local word = nil
|
|
|
|
|
|
-- The API for ZeroBrane Studio's language, or English: api-fr.lua beside api.lua, and so on.
|
|
function apiTable()
|
|
local folder = packageDir() .. "singe/"
|
|
local file = folder .. "api-" .. language() .. ".lua"
|
|
local chunk = nil
|
|
|
|
if (language() == "en") or not wx.wxFileExists(file) then
|
|
file = folder .. "api.lua"
|
|
end
|
|
chunk = loadfile(file)
|
|
if not chunk then
|
|
ide:Print(("Singe: could not read the API file %s."):format(file))
|
|
return {}
|
|
end
|
|
return chunk()
|
|
end
|
|
|
|
|
|
-- The builds of Singe this machine runs, its own first: Singe-v...-<OS>-<this>. ZeroBrane Studio's
|
|
-- ide.osarch is no help outside Linux: on Windows it holds the WINDIR path, and on a Mac it says x64
|
|
-- on Apple silicon too, since ZeroBrane Studio is itself an Intel program there. Windows 11 on ARM
|
|
-- and Rosetta both run the x86_64 build when there is no native one.
|
|
function architectures()
|
|
local cpu = ""
|
|
local probe = nil
|
|
|
|
if ide.osname == "Windows" then
|
|
-- ZeroBrane Studio is a 32-bit program: the machine's own architecture is in
|
|
-- PROCESSOR_ARCHITEW6432, and PROCESSOR_ARCHITECTURE is its own.
|
|
cpu = (os.getenv("PROCESSOR_ARCHITEW6432") or os.getenv("PROCESSOR_ARCHITECTURE") or ""):upper()
|
|
return (cpu == "ARM64") and { "aarch64", "x86_64" } or { "x86_64" }
|
|
end
|
|
if ide.osname == "Macintosh" then
|
|
-- Answers 1 on Apple silicon even to a program running under Rosetta.
|
|
probe = io.popen("sysctl -n hw.optional.arm64 2>/dev/null")
|
|
if probe then
|
|
cpu = probe:read("*l") or ""
|
|
probe:close()
|
|
end
|
|
return (cpu == "1") and { "aarch64", "x86_64" } or { "x86_64" }
|
|
end
|
|
return { ARCHITECTURE[ide.osarch] or "x86_64" }
|
|
end
|
|
|
|
|
|
-- Singe's executable: path.singe if set, then the one chosen before, then the newest for this
|
|
-- machine in the project's folder or the folders above it, then on the PATH. With ask, a missing
|
|
-- one is asked for and remembered.
|
|
function findSinge(ask)
|
|
local platform = PLATFORM[ide.osname] or PLATFORM.Unix
|
|
local kinds = architectures()
|
|
local settings = plugin:GetSettings() or {}
|
|
local folder = ide:GetProject()
|
|
local chosen = nil
|
|
|
|
if ide.config.path.singe and wx.wxFileExists(ide.config.path.singe) then
|
|
return ide.config.path.singe
|
|
end
|
|
if settings.singe and wx.wxFileExists(settings.singe) then
|
|
return settings.singe
|
|
end
|
|
for _ = 1, 4 do
|
|
if not folder then
|
|
break
|
|
end
|
|
-- The machine's own build first, newest of it; one it runs under emulation only failing that.
|
|
local files = ide:GetFileList(folder, false, "Singe*")
|
|
for _, kind in ipairs(kinds) do
|
|
local wanted = "^Singe%-v[%d%.]+b?%d*%-" .. platform.name .. "%-" .. kind .. platform.suffix
|
|
local best = nil
|
|
local bestKey = -1
|
|
for _, file in ipairs(files) do
|
|
local name = file:match("[^/\\]+$")
|
|
if name:match(wanted) and (singeVersion(name) > bestKey) then
|
|
best = file
|
|
bestKey = singeVersion(name)
|
|
end
|
|
end
|
|
if best then
|
|
return best
|
|
end
|
|
end
|
|
folder = parentFolder(folder)
|
|
end
|
|
for directory in (os.getenv("PATH") or ""):gmatch("[^" .. ((ide.osname == "Windows") and ";" or ":") .. "]+") do
|
|
for _, name in ipairs({ "Singe", "singe", "Singe.exe" }) do
|
|
if wx.wxFileExists(withSeparator(directory) .. name) then
|
|
return withSeparator(directory) .. name
|
|
end
|
|
end
|
|
end
|
|
if not ask then
|
|
return nil
|
|
end
|
|
chosen = wx.wxFileSelector(word("chooseTitle"), ide:GetProject() or "", "", "", "*", wx.wxFD_OPEN + wx.wxFD_FILE_MUST_EXIST, ide:GetMainFrame())
|
|
if chosen and (chosen ~= "") then
|
|
saveSetting("singe", chosen)
|
|
return chosen
|
|
end
|
|
return nil
|
|
end
|
|
|
|
|
|
-- The script to run. A .singe file is a game; anything else is one of its modules, so the
|
|
-- project's only .singe file runs instead, when it has exactly one.
|
|
function gameScript(wfilename)
|
|
local file = wfilename:GetFullPath()
|
|
local project = ide:GetProject()
|
|
local scripts = nil
|
|
|
|
if file:lower():match("%.singe$") or not project then
|
|
return file
|
|
end
|
|
scripts = ide:GetFileList(project, false, "*.singe")
|
|
if #scripts == 1 then
|
|
return scripts[1]
|
|
end
|
|
return file
|
|
end
|
|
|
|
|
|
function isStopFile(file)
|
|
return (file ~= nil) and (file:gsub("\\", "/"):match("Singe/" .. STOP_FILE:gsub("%.", "%%.") .. "$") ~= nil)
|
|
end
|
|
|
|
|
|
-- ZeroBrane Studio's language as two letters, English when it has none set.
|
|
function language()
|
|
local code = tostring(ide.config.language or ""):lower():sub(1, 2)
|
|
|
|
return (code ~= "") and code or "en"
|
|
end
|
|
|
|
|
|
-- The Reference at the word under the mouse: every entry's anchor is its name in lower case.
|
|
function lookUp(editor, position)
|
|
local first = editor:WordStartPosition(position, true)
|
|
local last = editor:WordEndPosition(position, true)
|
|
local word = editor:GetTextRange(first, last)
|
|
|
|
if word:match("^[%a_][%w_]*$") then
|
|
openReference((api[word] and api[word].anchor) or word:lower())
|
|
else
|
|
openReference()
|
|
end
|
|
end
|
|
|
|
|
|
function openReference(anchor)
|
|
local page = referencePage()
|
|
|
|
if not page then
|
|
wx.wxMessageBox(word("noReference"), word("reference"), wx.wxICON_INFORMATION + wx.wxOK + wx.wxCENTRE, ide:GetMainFrame())
|
|
return
|
|
end
|
|
wx.wxLaunchDefaultBrowser("file://" .. page:gsub("\\", "/"):gsub("^([^/])", "/%1") .. (anchor and ("#" .. anchor) or ""), 0)
|
|
end
|
|
|
|
|
|
-- This file's folder, wherever ZeroBrane Studio loaded it from.
|
|
function packageDir()
|
|
return GetPathWithSep(plugin:GetFilePath())
|
|
end
|
|
|
|
|
|
-- The folder above, normalized, or nil at the top.
|
|
function parentFolder(folder)
|
|
local name = wx.wxFileName.DirName(folder)
|
|
|
|
name:Normalize()
|
|
if name:GetDirCount() == 0 then
|
|
return nil
|
|
end
|
|
name:RemoveLastDir()
|
|
return name:GetPath(wx.wxPATH_GET_VOLUME)
|
|
end
|
|
|
|
|
|
-- Reference.html: the configured one, or the newest documentation download beside Singe, in
|
|
-- ZeroBrane Studio's language when there is one in it.
|
|
function referencePage()
|
|
local configured = (plugin:GetConfig() or {}).reference
|
|
local folder = singeFolder()
|
|
local best = nil
|
|
local bestKey = -1
|
|
|
|
if configured and wx.wxFileExists(configured) then
|
|
return configured
|
|
end
|
|
if not folder then
|
|
return nil
|
|
end
|
|
for _, suffix in ipairs({ "-" .. language(), "" }) do
|
|
for _, directory in ipairs(subfolders(folder, "Singe-Docs-v*")) do
|
|
local name = directory:match("[^/\\]+$")
|
|
local page = withSeparator(directory) .. "Reference.html"
|
|
if (name:match("^Singe%-Docs%-v[%d%.]+b?%d*" .. suffix:gsub("%-", "%%-") .. "$")) and wx.wxFileExists(page) and (singeVersion(name) > bestKey) then
|
|
best = page
|
|
bestKey = singeVersion(name)
|
|
end
|
|
end
|
|
if best then
|
|
return best
|
|
end
|
|
end
|
|
if wx.wxFileExists(folder .. "Reference.html") then
|
|
return folder .. "Reference.html"
|
|
end
|
|
return nil
|
|
end
|
|
|
|
|
|
function reloading()
|
|
local config = plugin:GetConfig() or {}
|
|
local settings = plugin:GetSettings() or {}
|
|
|
|
if config.reload ~= nil then
|
|
return config.reload
|
|
end
|
|
return settings.reload ~= false
|
|
end
|
|
|
|
|
|
function saveSetting(key, value)
|
|
local settings = plugin:GetSettings() or {}
|
|
|
|
settings[key] = value
|
|
plugin:SetSettings(settings)
|
|
end
|
|
|
|
|
|
-- The folder Singe runs in: the one holding the executable, where its Singe folder and games are.
|
|
function singeFolder()
|
|
local singe = findSinge(false)
|
|
|
|
return singe and wx.wxFileName(singe):GetPath(wx.wxPATH_GET_VOLUME + wx.wxPATH_GET_SEPARATOR) or nil
|
|
end
|
|
|
|
|
|
-- A number that sorts versions the way Singe does: v3.00 above v3.00b2 above v3.00b1.
|
|
function singeVersion(name)
|
|
local major, minor, beta = name:match("v(%d+)%.(%d+)b?(%d*)")
|
|
|
|
if not major then
|
|
return 0
|
|
end
|
|
return tonumber(major) * 1000000 + tonumber(minor) * 1000 + ((beta ~= "") and tonumber(beta) or 999)
|
|
end
|
|
|
|
|
|
-- The folders directly inside folder whose names match the wildcard mask, as full paths.
|
|
function subfolders(folder, mask)
|
|
local found = {}
|
|
local dir = wx.wxDir(folder)
|
|
local more = false
|
|
local name = nil
|
|
|
|
if not dir:IsOpened() then
|
|
return found
|
|
end
|
|
more, name = dir:GetFirst(mask, wx.wxDIR_DIRS)
|
|
while more do
|
|
found[#found + 1] = withSeparator(folder) .. name
|
|
more, name = dir:GetNext()
|
|
end
|
|
return found
|
|
end
|
|
|
|
-- A folder with its separator on the end: ZeroBrane Studio's GetPathWithSep takes a bare folder
|
|
-- for a file and answers its parent.
|
|
function withSeparator(folder)
|
|
if folder:match("[/\\]$") then
|
|
return folder
|
|
end
|
|
return folder .. GetPathSeparator()
|
|
end
|
|
|
|
|
|
-- The dragon at the toolbar's size: from beside this file, or from Singe's own folder when the
|
|
-- package was loaded from somewhere else, the nearest size up scaled to fit when none is exact. A
|
|
-- stock picture stands in when neither has it.
|
|
function toolbarBitmap(size)
|
|
local folders = { packageDir() .. "singe/" }
|
|
local folder = singeFolder()
|
|
|
|
if folder then
|
|
folders[#folders + 1] = folder .. "Singe/ZeroBrane/singe/"
|
|
end
|
|
for _, directory in ipairs(folders) do
|
|
local found = nil
|
|
for _, have in ipairs(ICON_SIZES) do
|
|
local file = directory .. "icon-" .. have .. ".png"
|
|
if wx.wxFileExists(file) then
|
|
found = { file = file, size = have }
|
|
if have >= size then
|
|
break
|
|
end
|
|
end
|
|
end
|
|
if found then
|
|
local image = wx.wxImage(found.file, wx.wxBITMAP_TYPE_PNG)
|
|
if found.size ~= size then
|
|
image:Rescale(size, size, wx.wxIMAGE_QUALITY_HIGH)
|
|
end
|
|
return wx.wxBitmap(image)
|
|
end
|
|
end
|
|
return wx.wxArtProvider.GetBitmap(wx.wxART_EXECUTABLE_FILE, wx.wxART_TOOLBAR, wx.wxSize(size, size))
|
|
end
|
|
|
|
-- One of the package's own words, in ZeroBrane Studio's language or English.
|
|
function word(key)
|
|
return (WORDS[language()] or WORDS.en)[key] or WORDS.en[key]
|
|
end
|
|
|
|
local interpreter = {
|
|
name = "Singe",
|
|
description = "Singe, the engine for laserdisc and video games",
|
|
api = { "baselib", "singe" },
|
|
luaversion = "5.4",
|
|
fname = INTERPRETER,
|
|
hasdebugger = true,
|
|
skipcompile = true, -- ZeroBrane Studio's own Lua is older than Singe's 5.4
|
|
takeparameters = true,
|
|
unhideanywindow = true,
|
|
fexepath = function(self) return findSinge(false) end,
|
|
fworkdir = function(self, wfilename) return singeFolder() or self:fprojdir(wfilename) end,
|
|
frun = function(self, wfilename, rundebug)
|
|
local singe = findSinge(true)
|
|
local script = gameScript(wfilename)
|
|
local params = self:GetCommandLineArg()
|
|
local folder = nil
|
|
local cmd = nil
|
|
local debug = ""
|
|
|
|
if not singe then
|
|
ide:Print(word("nothingRun"))
|
|
return
|
|
end
|
|
folder = wx.wxFileName(singe):GetPath(wx.wxPATH_GET_VOLUME + wx.wxPATH_GET_SEPARATOR)
|
|
if rundebug then
|
|
local debugger = ide:GetDebugger()
|
|
-- Names are reported relative to where Singe runs, so breakpoints are matched from there.
|
|
debugger:SetOptions({ runstart = ide.config.debugger.runonstart ~= false, basedir = folder })
|
|
debug = (" --debug=%s:%d"):format(ide.config.debugger.hostname or LOOPBACK, debugger:GetPortNumber())
|
|
end
|
|
cmd = ('"%s"%s%s%s "%s"'):format(singe, debug, reloading() and " --reload" or "", (params and (#params > 0)) and (" " .. params) or "", script)
|
|
return CommandLineRun(cmd, folder, true, true)
|
|
end,
|
|
}
|
|
|
|
|
|
return {
|
|
name = "Singe",
|
|
description = "Runs and debugs Singe games, with autocompletion and tooltips for the Singe API.",
|
|
author = "Scott Duensing",
|
|
version = 2.0,
|
|
dependencies = "1.70",
|
|
|
|
onRegister = function(self)
|
|
local project = ide:FindTopMenu("&Project")
|
|
local help = ide:FindTopMenu("&Help")
|
|
local frame = ide:GetMainFrame()
|
|
local toolbar = ide:GetToolBar()
|
|
local specmap = ide.config.editor.specmap
|
|
|
|
plugin = self
|
|
api = apiTable()
|
|
ide:AddAPI("lua", "singe", api)
|
|
ide:AddInterpreter(INTERPRETER, interpreter)
|
|
for extension, spec in pairs({ singe = "lua", rml = "xml", rcss = "css" }) do
|
|
specmapOld[extension] = specmap[extension]
|
|
specmap[extension] = specmap[extension] or spec
|
|
end
|
|
|
|
project:AppendSeparator()
|
|
project:Append(MENU_DEBUG, word("debug"))
|
|
project:Append(MENU_CHOOSE, word("choose"))
|
|
project:AppendCheckItem(MENU_RELOAD, word("reload"))
|
|
if help then
|
|
help:Append(MENU_REFERENCE, word("reference"))
|
|
end
|
|
frame:Connect(MENU_DEBUG, wx.wxEVT_COMMAND_MENU_SELECTED, function()
|
|
if ide:GetInterpreter() ~= ide:GetInterpreter(INTERPRETER) then
|
|
ide:SetInterpreter(INTERPRETER)
|
|
end
|
|
ProjectDebug()
|
|
end)
|
|
frame:Connect(MENU_CHOOSE, wx.wxEVT_COMMAND_MENU_SELECTED, function()
|
|
local chosen = wx.wxFileSelector(word("chooseTitle"), singeFolder() or "", "", "", "*", wx.wxFD_OPEN + wx.wxFD_FILE_MUST_EXIST, frame)
|
|
if chosen and (chosen ~= "") then
|
|
saveSetting("singe", chosen)
|
|
ide:Print(word("using"):format(chosen))
|
|
end
|
|
end)
|
|
frame:Connect(MENU_RELOAD, wx.wxEVT_COMMAND_MENU_SELECTED, function(event)
|
|
saveSetting("reload", event:IsChecked())
|
|
end)
|
|
frame:Connect(MENU_RELOAD, wx.wxEVT_UPDATE_UI, function(event)
|
|
event:Check(reloading())
|
|
end)
|
|
frame:Connect(MENU_REFERENCE, wx.wxEVT_COMMAND_MENU_SELECTED, function()
|
|
openReference()
|
|
end)
|
|
frame:Connect(MENU_LOOKUP, wx.wxEVT_COMMAND_MENU_SELECTED, function()
|
|
local editor = ide:GetEditor()
|
|
if editor then
|
|
lookUp(editor, lookUpPosition or editor:GetCurrentPos())
|
|
end
|
|
end)
|
|
|
|
toolbar:AddTool(MENU_DEBUG, "Singe", toolbarBitmap(toolbar:GetToolBitmapSize():GetWidth()), word("debug"))
|
|
toolbar:Realize()
|
|
end,
|
|
|
|
onUnRegister = function(self)
|
|
local toolbar = ide:GetToolBar()
|
|
local specmap = ide.config.editor.specmap
|
|
|
|
toolbar:DeleteTool(MENU_DEBUG)
|
|
toolbar:Realize()
|
|
for _, item in ipairs({ MENU_DEBUG, MENU_CHOOSE, MENU_RELOAD, MENU_REFERENCE }) do
|
|
ide:RemoveMenuItem(item)
|
|
end
|
|
for extension, spec in pairs(specmapOld) do
|
|
specmap[extension] = spec
|
|
end
|
|
ide:RemoveInterpreter(INTERPRETER)
|
|
ide:RemoveAPI("lua", "singe")
|
|
end,
|
|
|
|
-- The word under the mouse, looked up in the Reference; the caret stays where it was.
|
|
onMenuEditor = function(self, menu, editor, event)
|
|
local point = editor:ScreenToClient(event:GetPosition())
|
|
local position = editor:PositionFromPoint(point)
|
|
|
|
lookUpPosition = ((point:GetX() >= 0) and (point:GetY() >= 0)) and position or nil
|
|
menu:AppendSeparator()
|
|
menu:Append(MENU_LOOKUP, word("lookUp"))
|
|
end,
|
|
|
|
-- Singe stops in its own Debugger.singe when an error is about to end the game. Opened here, so
|
|
-- the IDE shows the stop rather than stepping out of a file it has not got open.
|
|
onDebuggerPreActivate = function(self, debugger, file, line)
|
|
-- Not opened here: this runs inside the debugger's coroutine, where an editor cannot be made.
|
|
-- ZeroBrane Studio opens a pending activation on its next update, still stopped meanwhile.
|
|
if isStopFile(file) and wx.wxFileExists(file) and not ide:FindDocument(file) then
|
|
debugger.activate = { file, line }
|
|
return false
|
|
end
|
|
end,
|
|
|
|
-- And says what the error was, which Debugger.singe holds as "message".
|
|
onDebuggerActivate = function(self, debugger, file, line)
|
|
if not isStopFile(file) then
|
|
return
|
|
end
|
|
debugger:EvalAsync("message", function(value)
|
|
local chunk = (loadstring or load)("return " .. tostring(value))
|
|
local ok, text = pcall(chunk or function() return value end)
|
|
ide:Print(word("stopped"):format(ok and tostring(text) or tostring(value)))
|
|
ide:Print(word("stoppedHelp"))
|
|
end)
|
|
end,
|
|
}
|