Skip to content

Instantly share code, notes, and snippets.

@pablomayobre
Last active August 29, 2015 14:12
Show Gist options
  • Select an option

  • Save pablomayobre/a6a0734b5b317d41dc3f to your computer and use it in GitHub Desktop.

Select an option

Save pablomayobre/a6a0734b5b317d41dc3f to your computer and use it in GitHub Desktop.
LÖVE Error handler and Notepad++
--No credit nor license is required for this function
--It is based on the original code by LÖVE developers
--Just minor functionality was added, if at all.
return function (src)
local dir = string.gsub(src,"([\\])","/")
local src = dir:match("/$") and dir or (dir.."/")
function love.errhand(msg)
msg = tostring(msg)
print("Error:")
print(debug.traceback(tostring(msg), 3):gsub("\n[^\n]+$", ""))
local j = string.gsub(msg,"^(Syntax error: )","")
local t = {}
for i=1,2 do
local pos1, pos2 = string.find(j,":", 1, true)
if not pos1 then
t[#t + 1] = j
break
end
t[#t + 1] = string.sub(j, 1, pos1 - 1)
j = string.sub(j, pos2 + 1)
end
if string.match(t[1] or "","^%[.+%]$") then
print("The error is in the internal function: "..(t[1] or ""))
else
local f,l = ((src or "")..t[1] or ""),t[2] or ""
print (string.format("FILE = %s, LINE = %s",f,l))
end
if not love.window or not love.graphics or not love.event then
return
end
if not love.graphics.isCreated() or not love.window.isCreated() then
if not pcall(love.window.setMode, 800, 600) then
return
end
end
if love.mouse then
love.mouse.setVisible(true)
love.mouse.setGrabbed(false)
end
if love.joystick then
for i,v in ipairs(love.joystick.getJoysticks()) do
v:setVibration()
end
end
if love.audio then love.audio.stop() end
local pxs = love.window.getPixelScale and love.window.getPixelScale() or 1
love.graphics.reset()
local font = love.graphics.setNewFont(math.floor(14 * pxs))
local sRGB = select(3, love.window.getMode()).srgb
if sRGB then
love.graphics.setBackgroundColor(love.math.gammaToLinear(89, 157, 220))
else
love.graphics.setBackgroundColor(89, 157, 220)
end
love.graphics.setColor(255, 255, 255, 255)
local trace = debug.traceback()
love.graphics.clear()
love.graphics.origin()
local err = {}
table.insert(err, "Error\n")
table.insert(err, msg.."\n\n")
for l in string.gmatch(trace, "(.-)\n") do
if not string.match(l, "boot.lua") then
l = string.gsub(l, "stack traceback:", "Traceback\n")
table.insert(err, l)
end
end
local p = table.concat(err, "\n")
p = string.gsub(p, "\t", "")
p = string.gsub(p, "%[string \"(.-)\"%]", "%1")
local function draw()
local pos = 70 * pxs
love.graphics.clear()
love.graphics.printf(p, pos, pos, love.graphics.getWidth() - pos)
love.graphics.present()
end
while true do
love.event.pump()
for e, a, b, c in love.event.poll() do
if e == "quit" then
return
end
if e == "keypressed" and a == "escape" then
return
end
end
draw()
if love.timer then
love.timer.sleep(0.1)
end
end
end
end
--1
-- Modified version of lurker
-- Changes:
-- Doesnt depend on Lume
-- Better error message (now with the FILE and LINE)
-- Draw function can be easily changed
-- Stops all the stuff that can be annoying to the user
-- Works better when sRGB or HighDPI is enabled
-- Less stuff printed to the console (because that can turn to be annoying)
-- More callbacks supported
--
-- Copyright (c) 2015 rxi
--
-- This library is free software; you can redistribute it and/or modify it
-- under the terms of the MIT license. See LICENSE for details.
local lurker = { _version = "1.0.1" }
local lovecallbacknames = {
"update",
"load",
"draw",
"mousepressed",
"mousereleased",
"keypressed",
"keyreleased",
"focus",
"quit",
}
function lurker.init(src)
local src = string.gsub(src,"([\\])","/")
lurker.src = src:match("/$") and src or (src.."/")
lurker.path = "."
lurker.preswap = function() end
lurker.postswap = function() end
lurker.interval = .5
lurker.protected = true
lurker.quiet = false
lurker.lastscan = 0
lurker.lasterrorfile = nil
lurker.files = {}
lurker.funcwrappers = {}
lurker.lovefuncs = {}
lurker.state = "init"
for _, v in ipairs(lurker.getchanged()) do lurker.resetfile(v) end
return lurker
end
function lurker.listdir(path, recursive, skipdotfiles, t)
path = (path == ".") and "" or path
local function fullpath(x) return path .. "/" .. x end
local t = {}
for _, f in ipairs(love.filesystem.getDirectoryItems(path)) do
local f = fullpath(f)
if not skipdotfiles or not f:match("/%.[^/]*$") then
if recursive and love.filesystem.isDirectory(f) then
lurker.listdir(f, true, skipdotfiles, t)
else
table.insert(t, f:match("^[/]*(.-)[/]*$"))
end
end
end
return t
end
function lurker.initwrappers()
for _, v in pairs(lovecallbacknames) do
lurker.funcwrappers[v] = function(...)
local args = {...}
xpcall(function()
return lurker.lovefuncs[v] and lurker.lovefuncs[v](unpack(args))
end, lurker.onerror)
end
lurker.lovefuncs[v] = love[v]
end
lurker.updatewrappers()
end
function lurker.updatewrappers()
for _, v in pairs(lovecallbacknames) do
if love[v] ~= lurker.funcwrappers[v] then
lurker.lovefuncs[v] = love[v]
love[v] = lurker.funcwrappers[v]
end
end
end
function lurker.draw ()
local sRGB = select(3, love.window.getMode()).srgb
if sRGB then
love.graphics.setBackgroundColor(love.math.gammaToLinear(89, 157, 220))
else
love.graphics.setBackgroundColor(89, 157, 220)
end
love.graphics.setColor(255, 255, 255, 255)
local pxs = love.window.getPixelScale and love.window.getPixelScale() or 1
local pos = 70 * pxs
love.graphics.printf(lurker.errmsg, pos, pos, love.graphics.getWidth() - pos*2)
end
function lurker.onerror(e, nostacktrace)
lurker.state = "error"
-- Set up callbacks
for _, v in pairs(lovecallbacknames) do
love[v] = function() end
end
msg = tostring(e)
print("Error:")
print(debug.traceback(msg, 2):gsub("\n[^\n]+$", ""))
local j = string.gsub(msg,"^(Syntax error: )","")
local t = {}
for i=1, 2 do
local pos1, pos2 = string.find(j,":", 1, true)
if not pos1 then
t[#t + 1] = j
break
end
t[#t + 1] = string.sub(j, 1, pos1 - 1)
j = string.sub(j, pos2 + 1)
end
if string.match(t[1] or "","^%[.+%]$") then
print("The error is in the internal function: "..(t[1] or ""))
else
local f,l = ((lurker.src or "")..t[1] or ""),t[2] or ""
print (string.format("FILE = %s, LINE = %s",f,l))
end
if not love.window or not love.graphics or not love.event then
love.event.quit()
end
if not love.graphics.isCreated() or not love.window.isCreated() then
if not pcall(love.window.setMode, 800, 600) then
love.event.quit()
end
end
if love.mouse then
love.mouse.setVisible(true)
love.mouse.setGrabbed(false)
end
if love.joystick then
for i,v in ipairs(love.joystick.getJoysticks()) do
v:setVibration()
end
end
if love.audio then love.audio.stop() end
local err = {}
table.insert(err, "Error\n")
table.insert(err, msg.."\n\n")
local trace = debug.traceback()
for l in string.gmatch(trace, "(.-)\n") do
if not string.match(l, "boot.lua") then
l = string.gsub(l, "stack traceback:", "Traceback\n")
table.insert(err, l)
end
end
lurker.errmsg = table.concat(err, "\n")
lurker.errmsg = string.gsub(lurker.errmsg, "\t", "")
lurker.errmsg = string.gsub(lurker.errmsg, "%[string \"(.-)\"%]", "%1")
local pxs = love.window.getPixelScale and love.window.getPixelScale() or 1
local font = love.graphics.setNewFont(math.ceil(14 * pxs))
love.draw = lurker.draw or function () end
love.graphics.push("all") --Go back to default settings but save old ones
love.graphics.reset()
love.update = lurker.update
love.keypressed = function(k)
if k == "escape" then
love.event.quit()
end
end
end
function lurker.exitinitstate()
lurker.state = "normal"
if lurker.protected then
lurker.initwrappers()
end
end
function lurker.exiterrorstate()
lurker.state = "normal"
for _, v in pairs(lovecallbacknames) do
love[v] = lurker.funcwrappers[v]
end
love.graphics.pop()
end
function lurker.update()
if lurker.state == "init" then
lurker.exitinitstate()
end
local diff = love.timer.getTime() - lurker.lastscan
if diff > lurker.interval then
lurker.lastscan = lurker.lastscan + diff
local changed = lurker.scan()
if #changed > 0 and lurker.lasterrorfile then
local f = lurker.lasterrorfile
lurker.lasterrorfile = nil
lurker.hotswapfile(f)
end
end
end
function lurker.getchanged()
local function fn(f)
return f:match("%.lua$") and lurker.files[f] ~= love.filesystem.getLastModified(f)
end
local t = lurker.listdir(lurker.path, true, true)
local rtn = {}
for k, v in ipairs(t) do
if fn(v) then rtn[#rtn + 1] = v end
end
return rtn
end
function lurker.modname(f)
return (f:gsub("%.lua$", ""):gsub("[/\\]", "."))
end
function lurker.resetfile(f)
lurker.files[f] = love.filesystem.getLastModified(f)
end
function lurker.hotswapfile(f)
if lurker.state == "error" then
lurker.exiterrorstate()
end
if lurker.preswap(f) then
lurker.resetfile(f)
return
end
local modname = lurker.modname(f)
local oldglobal = {}
for k, v in pairs(_G) do oldglobal[k] = v end
local updated = {}
local function update(old, new)
if updated[old] then return end
updated[old] = true
local oldmt, newmt = getmetatable(old), getmetatable(new)
if oldmt and newmt then update(oldmt, newmt) end
for k, v in pairs(new) do
if type(v) == "table" then update(old[k], v) else old[k] = v end
end
end
local err = nil
local function onerror(e)
for k, v in pairs(_G) do _G[k] = oldglobal[k] end
err = e
end
local ok, oldmod = pcall(require, modname)
oldmod = ok and oldmod or nil
xpcall(function()
package.loaded[modname] = nil
local newmod = require(modname)
if type(oldmod) == "table" then update(oldmod, newmod) end
for k, v in pairs(oldglobal) do
if v ~= _G[k] and type(v) == "table" then
update(v, _G[k])
_G[k] = v
end
end
end, onerror)
package.loaded[modname] = oldmod
if err then
if not lurker.quiet and lurker.protected then
lurker.lasterrorfile = f
lurker.onerror(err, true)
lurker.resetfile(f)
return
end
end
lurker.resetfile(f)
lurker.postswap(f)
if lurker.protected then
lurker.updatewrappers()
end
end
function lurker.scan()
if lurker.state == "init" then
lurker.exitinitstate()
end
local changed = lurker.getchanged()
for _, v in ipairs(changed) do lurker.hotswapfile(v) end
return changed
end
return setmetatable(lurker,{__call = function(self,...) return self.init(...) end})
@pablomayobre

Copy link
Copy Markdown
Author

Description

This is an error handler for LÖVE, it does the same thing as the standard error handler, but adds a line to the error description printed to the console.

This extra line is useful to determine where the error is present, and it has this format :

FILE = C:/path/to the/error/file.lua, LINE = n

This, in turn, can be used with Notepad++ Plugin NppExec to add a link that will take you to that place

Including this error handler in your code

To use this error handler you need to require it in your main.lua, like this:

io.stdout:setvbuf("no") -- Print to Notepad++ console
love.load = function (args)
    require "errors" (args[1])
end

Configuring N++

Install NppExec plugin in Notepad++ and add this Filter to Console Output Filters... > Highlight

FILE = %ABSFILE%, LINE = %LINE%

You'll need to open the file with NppExec to get the output to the console. There are some problems opening LÖVE directly from NppExec, so we instead call it from a .bat file, called run.bat, with the following code:

@echo off
:: Your LÖVE installation here
cd "C:\Program Files\Love 0.9.1\"
:there
    if %1 neq "" goto here
    echo No file found
    goto end
:more
    if "%1" neq "" goto here
    goto end
:here
    call love.exe %1
    shift
    goto more
:end
    exit

To run this bat configure this command in NppExec and save it as Love (NPP_SAVEALL is not mandatory)

NPP_SAVEALL
"C:\path\to\run.bat" "$(CURRENT_DIRECTORY)"

Then in Plugin > NppExec > Advance Options > Menu Item Add an Item called Love that triggers the Love command.

After that go to Execute > Modify Shortcuts / Delete Commands > Plugin Command and add a key shortcut to this command, I prefer CTRL + L

Problems

  • LÖVE can't handle two consoles at the same time, that is why even if in your conf.lua you define t.console = true the console will never be spawn, even then, you can still get the console output in the NppExec console.
  • If you add more filters the file may be detected incorrectly, try to keep your filters down. Also, this filter should be always the first one, or others will take precedence instead of this one.

@pablomayobre

Copy link
Copy Markdown
Author

PS: It also fix the errors in HighDPI screens like Mac OS X Retina Displays and a bug in the color when in sRGB mode. This bugs where fixed in 0.9.2, but this file works in all 0.9.X versions.

@pablomayobre

Copy link
Copy Markdown
Author

If you use strong or a similar library in your project, you can replace this:

for i=1,2 do
    local pos1, pos2 = string.find(j,":", 1, true)
    if not pos1 then
        t[#t + 1] = j
        break
    end
    t[#t + 1] = string.sub(j, 1, pos1 - 1)
    j = string.sub(j, pos2 + 1)
end 

with this:

t = string.split(j,":",false) --False because it's not a pattern

Strong also supports this:

t = j / ":"

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment