diff --git a/BugSack.toc b/BugSack.toc index e92293c..d6ba3d1 100644 --- a/BugSack.toc +++ b/BugSack.toc @@ -27,6 +27,9 @@ embeds.xml +parsestack.lua +plugins.lua +plugin_stack_compact.lua locales.lua core.lua sack.lua diff --git a/config.lua b/config.lua index c888426..79f414f 100644 --- a/config.lua +++ b/config.lua @@ -65,9 +65,27 @@ frame:SetScript("OnShow", function(frame) mute:SetChecked(addon.db.mute) mute:SetPoint("TOPLEFT", minimap, "BOTTOMLEFT", 0, -8) + local info = {} + local pluginFormatterDropdown = CreateFrame("Frame", "BugSackPluginFormatter", frame, "UIDropDownMenuTemplate") + pluginFormatterDropdown:SetPoint("TOPLEFT", mute, "BOTTOMLEFT", -15, -10) + pluginFormatterDropdown.initialize = function() + for name,formatter in pairs(addon.Plugins.formatters) do + wipe(info) + info.text = formatter.label or formatter.name + info.value = formatter.name + info.func = function(self) + addon.db.pluginFormatter = self.value + addon:UpdateDisplay() + end + info.checked = name == addon.db.pluginFormatter + UIDropDownMenu_AddButton(info) + end + end + pluginFormatterDropdown.Text:SetText(L["Formatter plugin"]) + local info = {} local fontSizeDropdown = CreateFrame("Frame", "BugSackFontSize", frame, "UIDropDownMenuTemplate") - fontSizeDropdown:SetPoint("TOPLEFT", mute, "BOTTOMLEFT", -15, -10) + fontSizeDropdown:SetPoint("TOPLEFT", pluginFormatterDropdown, "BOTTOMLEFT", -15, -10) fontSizeDropdown.initialize = function() wipe(info) local fonts = {"GameFontHighlightSmall", "GameFontHighlight", "GameFontHighlightMedium", "GameFontHighlightLarge"} diff --git a/core.lua b/core.lua index 597513d..96784a7 100644 --- a/core.lua +++ b/core.lua @@ -124,8 +124,19 @@ do if type(sv.fontSize) ~= "string" then sv.fontSize = "GameFontHighlight" end if type(sv.altwipe) ~= "boolean" then sv.altwipe = false end if type(sv.useMaster) ~= "boolean" then sv.useMaster = false end + if type(sv.pluginFormatter) ~= "string" then sv.pluginFormatter = "default" end addon.db = sv + -- add our default formatter + addon.Plugins:RegisterFormatter({ + name="default", + label="Default", + description="Classic BugSack stacktrace", + formatStack=addon.ColorStack, + formatMessage=addon.ColorMessage, + formatLocals=addon.ColorLocals, + }) + -- Make sure we grab any errors fired before bugsack loaded. local session = addon:GetErrors(BugGrabber:GetSessionId()) if #session > 0 then onError() end @@ -213,18 +224,29 @@ do end addon.ColorLocals = colorLocals - local errorFormat = "%dx %s" - local errorFormatLocals = "%dx %s\n\nLocals:\n%s" + local errorFormatMessage = "%dx %s" + + local function colorMessage(counter,message) + message = message:gsub("^%[string \"(.-)\"%]:","%1:") + return errorFormatMessage:format(counter,message) + end + addon.ColorMessage = colorMessage + + local errorFormatStack = "\n\nStack:\n%s" + local errorFormatLocals = "\n\nLocals:\n%s" + function addon:FormatError(err) - if not err.locals then - local s = colorStack(tostring(err.message) .. (err.stack and "\n"..tostring(err.stack) or "")) - local l = colorLocals(tostring(err.locals)) - return errorFormat:format(err.counter or -1, s, l) - else - local s = colorStack(tostring(err.message) .. (err.stack and "\n"..tostring(err.stack) or "")) - local l = colorLocals(tostring(err.locals)) - return errorFormatLocals:format(err.counter or -1, s, l) - end + local formatter = addon.Plugins:GetFormatter() + + local message,stack,locals = err.message,err.stack,err.locals + + message,stack,locals = formatter.preformatError(message,stack,locals) + + local ret = "" + ret = ret .. formatter.formatMessage(err.counter or -1,message) + ret = ret .. errorFormatStack:format(formatter.formatStack(stack)) + if locals then ret = ret .. errorFormatLocals:format(formatter.formatLocals(tostring(locals))) end + return ret end end diff --git a/locales.lua b/locales.lua index d947670..a06f581 100644 --- a/locales.lua +++ b/locales.lua @@ -13,6 +13,7 @@ L["chatFrameDesc"] = "Prints a reminder to the chat frame when an error is encou L["Chatframe output"] = "Chatframe output" L["Current session"] = "Current session" L["%d bugs have been sent to %s. He must have BugSack to be able to examine them."] = "%d bugs have been sent to %s. He must have BugSack to be able to examine them." +L["Formatter plugin"] = "Formatter plugin" L["Failure to deserialize incoming data from %s."] = "Failure to deserialize incoming data from %s." L["Filter"] = "Filter" L["Filter addon mistakes"] = "Filter addon mistakes" @@ -577,4 +578,6 @@ elseif locale == "itIT" then --L.useMasterDesc = "Play the chosen error sound over the 'Master' sound channel instead of the default one." end +setmetatable(L,{__index=function(t,k) return k end}) + addon.L = L diff --git a/parsestack.lua b/parsestack.lua new file mode 100644 index 0000000..42f8142 --- /dev/null +++ b/parsestack.lua @@ -0,0 +1,98 @@ +ParseStack = {} + +--[[ + [string "=[C]"]: in function `RunScript' + [string "=(tail call)"]: in main chunk + [string "@Interface\SharedXML\UIDropDownMenu.lua"]:71: in function `UIDropDownMenu_Initialize' + [string "@Interface\FrameXML\ChatFrame.lua"]:2174: in function `?' + [string "@Interface\FrameXML\EasyMenu.lua"]:21: in function `EasyMenu' + [string "EasyMenu("qqq")"]:1: in main chunk + [string "*:OnEnterPressed"]:1: in function <[string "*:OnEnterPressed"]:1> + [string "*ChatFrame.xml:127_OnEnterPressed"]:1: in function <[string "*ChatFrame.xml:127_OnEnterPressed"]:1> +--]] + +function ParseStack.parsePath(path) + local is_addon = path:lower():match("^interface/addons/") + local is_blizz + path = path:gsub("^[Ii]nterface/","") + path = path:gsub("^[Aa]dd[Oo]ns/","") + + local parts = {strsplit(":",path)} + local fileparts = {strsplit("/",parts[1])} + local addon,folder,file + if #fileparts>0 then + addon = is_addon and tremove(fileparts,1) + is_blizz = is_addon and addon:find("Blizzard_",1,true) or (fileparts[1]=="FrameXML" or fileparts[1]=="SharedXML") + if addon and is_blizz then tinsert(fileparts,1,addon) addon=nil end -- back into the path, Blizzard_ addons are not addons + file = tremove(fileparts,#fileparts) + if #fileparts>0 then folder=table.concat(fileparts,"/") end + end + return addon,folder,file,not not is_addon,not not is_blizz +end +local parsePath=ParseStack.parsePath + +function ParseStack.parseStack(stack) + if type(stack)=="string" then stack={strsplit("\n",stack)} end + local parsedStack = {} + for i,line in ipairs(stack) do + local orig_line = line + local p = {raw=line} + local _ + p.source,_ = line:match("^%[string \"(.-)\"%]:(.*)") -- [string "source"]: | 1: in function `blabla' + if _ then line=_ end + p.linenum,_ = line:match("^(%d+): (.*)") -- 1: in function + if _ then line=_ else line=line:gsub("^ ","") end + + if not p.source then + -- + + elseif p.source=="=[C]" then + p.source_type="c" + + elseif p.source=="=(tail call)" then + p.source_type="tailcall" + + elseif p.source:sub(1,1)=="@" then -- source file + p.source_type="lua" + p.source_addon,p.source_folder,p.source_file,p.source_is_addon,p.source_is_blizz=parsePath(p.source:sub(2)) + + elseif p.source:sub(1,1)=="*" then -- xml? + p.source_type="xml" + p.source_addon,p.source_folder,p.source_file,p.source_is_addon,p.source_is_blizz=parsePath(p.source:sub(2)) + p.source_file,p.source_linenumx,p.source_handler = p.source:match("^.([^:]+):(%d+)_(.*)") + + elseif p.source:find(".xml:<",1,true) then -- xml inline + p.source_type="xml_inline" + local path,tag = p.source:match("(.*):<(.-)>") + p.source_addon,p.source_folder,p.source_file,p.source_is_addon,p.source_is_blizz=parsePath(path) + p.source_xmltag = tag + + else + p.source_type="string" + end + + if line=="in main chunk" then + p.function_main = line=="in main chunk" + elseif line=="?" then + p.function_unknown = true + else + p.function_name = line:match("^in function `(.-)'") + p.function_angle = line:match("^in function <(.-)>") + if p.function_angle then + p.function_file,p.function_startline = p.function_angle:match("([^/]+):(%d+)$") -- just file name and line + if p.function_file then p.function_angle=nil end + if p.function_file=="[string \""..p.source.."\"]" then + p.function_same=true + p.function_file=nil + p.function_name=nil + end + end + if not p.function_name and not p.function_file and not p.function_same then + p.function_raw = line + end + end + + tinsert(parsedStack,p) + end + return parsedStack +end diff --git a/plugin_stack_compact.lua b/plugin_stack_compact.lua new file mode 100644 index 0000000..d69cb63 --- /dev/null +++ b/plugin_stack_compact.lua @@ -0,0 +1,195 @@ +local addonName, addon = ... + +if not addon.Plugins then return end + +local function _cr(color,str) return str and #str>0 and "|c"..color..str.."|r" or "" end -- not a huge overhead, so do use this for wrapping colors, just to be 100% sure you don't miss a |r. +local colors = { + addon = {addon='ff88ff00',path='FF559200',file='FFC4FF81',file2='FF92C25B'}, -- MyAddon /Folder/ File.lua + blizz = {addon='ff9966ff',path='FF563A8F',file='FFAB81FF',file2='FF9984C4',line='FFBAA7E0'}, -- Blizzard_CoreAddon /Folder/ File.lua + ccode = 'FF864AFF', -- [C] + tailcall = 'FF5C74FF', -- (tail call) + string = 'ffffffff', -- "string" + funcname = 'FFFFCF31', + xml = {obj="ffff8888",handler='ffffaaaa'}, -- + line = "FFD0FF00", -- :123 + line2 = "FFBDD162", -- :123 + ver ="ffffff00", -- (1.0.0) + val = { + ['nil'] = "FFCC00FF", + ['true'] = "FF1EFF00", + ['false'] = "FFFF0000", + num = "ffff7fff", + func = "ffffea00", + tablekey = "ffffff80" + } +} + +local col = {} +do + local function wrapcolor(t,into) + for k,v in pairs(t) do + if type(v)=="string" then into[k]=function(str) return _cr(v,str) end + elseif type(v)=="table" then into[k]={} wrapcolor(v,into[k]) end + end + end + wrapcolor(colors,col) +end + +local function colorPath(path, addon,folder,file,isAddon,isBlizz) + if path and not addon and not file then + addon,folder,file,isAddon,isBlizz = ParseStack.parsePath(path) + end + if file then + local col_type = isBlizz and col.blizz or col.addon + return col_type.addon(addon or "") .. col_type.path((addon and "/" or "")..(folder and folder.."/" or "")) .. col_type.file(file) + end + --[[ + else -- framexml or something + local folder,file = path:match("^.-/(.-)([^/]+%.[luaxm]+)$") + if folder then + return col.blizz.path(folder)..col.blizz.file(file) + end + end + --]] + return path +end + +local function colorStack(stack) + stack = tostring(stack) or "" -- Yes, it gets called with nonstring from somewhere /mikk + stack = stack:gsub("^[%s\n]*","") -- trim leading space/lines + + local parsed_stack = ParseStack and ParseStack.parseStack(stack) + if not parsed_stack then return stack end + + local out="" + + for i,entry in ipairs(parsed_stack) do + + -- color source first + local source,linenum,line + + if entry.source_type=="c" then + source = col.ccode("[C code]") + + elseif entry.source_type=="tailcall" then + source = col.tailcall("[tail call]") + + elseif entry.source_type=="lua" then + source = colorPath(entry.source, entry.source_addon,entry.source_folder,entry.source_file,entry.source_is_addon,entry.source_is_blizz) + + elseif entry.source_type=="xml" then + if entry.source_file then + --"(XML) "..col.xml.obj(entry.source_file)..":"..col.line(entry.source_linenumx).." ("..col.xml.handler(entry.source_handler)..")" + source = col.addon.file(entry.source_file)..":"..col.line(entry.source_linenumx).." ("..col.xml.handler(entry.source_handler)..")" + else + source = col.blizz.file(entry.source:sub(2)) + end + + elseif entry.source_type=="xml_inline" then + --source = "(XML) "..col.xml.obj(entry.source_file)..":"..col.line(entry.source_linenumx).." <"..col.xml.handler(entry.source_xmltag)..">" + source = colorPath(entry.source, entry.source_addon,entry.source_folder,entry.source_file,entry.source_is_addon,entry.source_is_blizz).." <"..col.xml.handler(entry.source_xmltag)..">" + + elseif entry.source then + source = "\""..col.string(entry.source).."\"" + -- source is plain string, leave it alone for now + end + + -- color linenum, if any + if entry.linenum then linenum=(entry.source_is_blizz and col.blizz.line or col.line)(entry.linenum) end + + -- color function last + --scope = scope:gsub("<[iI]nterface/[aA]dd[oO]ns/","<") + if entry.function_name then + line="in function '"..(entry.source_is_blizz and col.blizz.file or col.funcname)(entry.function_name).."'" -- straighten quotes around function name + + elseif entry.function_file then + line="in function <"..col.addon.file2(entry.function_file~=entry.source_file and entry.function_file or "")..":"..col.line2(entry.function_startline)..">" + + elseif entry.function_angle then + line="in function <"..col.funcname(entry.function_angle)..">" + + elseif entry.function_same then + line="inline" + + elseif entry.function_main then + line="in main chunk" + + elseif entry.function_unknown then + line="?" + + else + line=entry.function_raw + end + + out=out..(#out>0 and "\n" or "") + if source then + out = out .. source .. (linenum and (":"..linenum) or "") .. (line and ": "..line or "") + else + out = out .. entry.raw + end + + end + + return out +end + +local function colorMessage(msg) + -- strip [string "..."]: + msg = msg:gsub("^%[string \"(.-)\"%]:","%1:") + + -- color path:line, if present + local path,linenum,message = msg:match("^([^:]+/[^:]+):([%d]+): (.+)") + if path then msg = colorPath(path)..":"..col.line(linenum)..": "..message end + + return msg +end + +local errorFormatMessage = _cr("ffffffff","%d").."x %s" + +local function formatMessage(counter,message) + return errorFormatMessage:format(counter or -1, colorMessage(tostring(message))) +end + +local function colorLocals(locals) + locals = tostring(locals) or "" -- Yes, it gets called with nonstring from somewhere /mikk + locals = locals:gsub("|(%a)", "||%1"):gsub("|$", "||") -- Pipes + --locals = locals:gsub("> %@(.-):(%d+)", "> @|cffeda55f%1|r:|cff00ff00%2|r") -- Files/Line Numbers of locals + locals = locals:gsub("(%s-)([%a_%(][%a_%d%*%)]+) = ", "%1"..col.val.tablekey("%2").." = ") -- Table keys + locals = locals:gsub("= (%-?[%d%p]+)\n", "= "..col.val.num("%1").."\n") -- locals: number + locals = locals:gsub("= nil\n", "= "..col.val['nil']("nil").."\n") -- locals: nil + locals = locals:gsub("= true\n", "= "..col.val['true']("true").."\n") -- locals: true + locals = locals:gsub("= false\n", "= "..col.val['false']("false").."\n") -- locals: false + locals = locals:gsub("= <(.-)>", "= "..col.val.func("<%1>")) -- Things wrapped in <> + locals = locals:gsub("defined @(.-):(%d+)",function(path,line) return "defined @"..colorPath(path)..":"..col.line(line) end) + --locals = locals:gsub("@[%.I][%.n][%.t][%.e][%.r]face/", "") + --locals = locals:gsub("%.?%.?%.?/?AddOns/", "") + return locals +end + +local function preformatError(message,stack,locals) + local msg,pre_stack = message:match("(.*)\n%-%- STACKTRACE: %-%-\n(.*)") + if msg then + message = msg + stack = pre_stack .. "---\n" .. (tostring(stack) or "") + end + return message,stack,locals +end + +addon.Plugins:RegisterFormatter({ + name="compact", + label="Compact", + description="Stack lines compacted and with color coding of source: internal or addon code.", + formatStack=colorStack, + formatMessage=formatMessage, + formatLocals=colorLocals, + preformatError=preformatError, +}) + +addon.Plugins:RegisterFormatter({ + name="raw", + label="Raw", + description="Do not apply any formatting.", + --formatStack=function(...) return ... end, + --formatMessage=function(count,msg) return count.."x "..msg, + --formatLocals=identity, +}) diff --git a/plugins.lua b/plugins.lua new file mode 100644 index 0000000..0a53511 --- /dev/null +++ b/plugins.lua @@ -0,0 +1,18 @@ +local addonName, addon = ... + +addon.Plugins = {} +addon.Plugins.formatters = {} + +function addon.Plugins:RegisterFormatter(package) + if package.name then + package.formatStack = package.formatStack or function(stack) return stack end + package.formatMessage = package.formatMessage or function(count,message) return ("%dx %s"):format(count or -1,message) end + package.formatLocals = package.formatLocals or function(locals) return locals end + package.preformatError = package.preformatError or function(...) return ... end + addon.Plugins.formatters[package.name]=package + end +end + +function addon.Plugins:GetFormatter(name) + return addon.Plugins.formatters[name or addon.db.pluginFormatter] or addon.Plugins.formatters["default"] +end