--# Main
-- Codea Community installer

function setup()
	http.request("http://codea.io/cc/alpha/installer.php",
		function(d) loadstring(d)() end,
		function(e) print(e) end)
		print("please wait while installing Codea Community")
end
			
--Touch and hold this icon, then hit copy to get the whole code.
--# Main
--------------------------------------------------------------
--Project: pimp my print
--Version: 0.2
--Author: toffer


--The MIT License (MIT)
--
--Copyright (c) 2014 toffer
--
--Permission is hereby granted, free of charge, to any person obtaining a copy
--of this software and associated documentation files (the "Software"), to deal
--in the Software without restriction, including without limitation the rights
--to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
--copies of the Software, and to permit persons to whom the Software is
--furnished to do so, subject to the following conditions:
--
--The above copyright notice and this permission notice shall be included in all
--copies or substantial portions of the Software.
--
--THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
--IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
--FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
--AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
--LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
--OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
--SOFTWARE.


--[[    Usage
        Import the lib as a dependency
        Insert a breakpoint with --@
        To step line by line across a function
        or a lopp put it next to the function/loop declaration.
        To avoid breakpoints in nested function/loop put --! in front of the
        declaration/line. (see int setup function of this tab)
        !! the monitor params must be enabled
        Tap once to resume code execution ]]

--  example

function setup()

    -- step on each line of the function
    local function dostuff() --@
        local x,y = 1,4
        for i=1,2 do
            x = x + i
            y = y + x
        end
        -- avoid step this loop
        for i=1,20 do --!
            x = x + i
            y = y + x
        end
        x = y
    end
    dostuff()
    
    transforms = { rotation = 0, x = WIDTH/2, y = HEIGHT/2 }
    tween(5, transforms, { rotation = 360 }, { loop = tween.loop.forever })
    
    wastouched = false
    
end

function draw()
    background(56, 43, 34, 45)
    pushMatrix()
        translate(transforms.x,transforms.y + math.sin(ElapsedTime) * 100)
        rotate(transforms.rotation)
        if wastouched then
            fill(255, 0, 6, 255)
            rect(0,0,100,100) --@ another breakpoint
        else
            fill(196, 153, 36, 255)
            rect(0,0,100,100)
        end
    popMatrix()
    wastouched = false
end

function touched(touch)
    if touch.state == BEGAN then
        wastouched = true --@
    end
    if touch.state == 1 then
        transforms.x = touch.x
        transforms.y = touch.y
    end
end
--# pimp
-- pimp my print

local __G,debug = _G,debug
local threads,time,noop = {},0,function() end
local env,log,halt,monitor
local parsetokens

-- Register hooks
-- Base one, output source exerpt and local vars
local function breakpoint(token,pimp,...)
    local info = pimp:getinfo()
    local chunk,from,to,lines = pimp:prettysource(info,6,6)
    clearOutput()
    print(string.format("/ %s @%d,%d /",info.tab,from,to,info.name))
    print(chunk)
    parameter.watch("Locals")
    Locals = table.concat(pimp:prettyvars(info),"\n")
    return true
end

pimphook = pimphook or {}

pimphook["@"] = breakpoint

local pimp = pimp or {}

-- @internal Print hook
-- trigger hooks callback if a token is found from the first arg
-- if not, print as usual
local function hook(token,...)
    local hook = pimphook[token]
    if hook and hook(token,pimp,...) then pimp:yield() else log(token,...) end
end

-- Yield the current running thread.
-- Should not be directly called, rather use a positve return from a hook callback to
-- ensure all other operations (print,inspect...) appened before
function pimp:yield(...)
    halt = true
    coroutine.yield()
end

-- Pause the 'main' loop
-- If it was running, enter in inspect mode targeting the global env
function pimp:pause()
    halt = not halt and true or false
    if halt then pimp:inspect(env) end
end

function pimp:prettyvars(info)
    local l = 1
    while debug.getinfo(l,"f").func ~= hook do
        l = l + 1
    end
    local scope = l+1
    local i,locals,key,val = 0,{}
    while true do
        i = i + 1
        key,val = debug.getlocal(scope,i)
        if not key then
            break end
        
        if not string.match(key,"%(") then -- avoid for loops
            if type(val) == "table" then
                key = {key}
                for k,v in pairs(val) do
                    table.insert(key," "..k.." : "..tostring(v))
                end
                table.insert(locals,table.concat(key,"\n"))
            else
                table.insert(locals,key.." : "..tostring(val))
            end
        end
    end
    return locals
end

-- Return the tab name and a chunk of the source code where the playhead currently halted
-- An arrow mark the current line
function pimp:prettysource(info,linesbefore,linesafter)
    local cur = info.currentline
    local prev,next = cur - linesbefore,cur + linesafter
    local i,lines = 0,{}
    for line in string.gmatch(info.source,"(.-)\n") do
        i = i + 1
        if i > next then break end
        if i >= prev then
            line = string.gsub(i == cur and ">"..line or line,"%s%s%s%s"," ")
            -- line = string.gsub(line,"%s%s%s%s"," ")
            if #line > 35 then line = string.sub(line,1,32).."…" end
            table.insert(lines,line)
        end
    end
    return table.concat(lines,"\n"),prev,next,lines
    -- return string.gsub(table.concat(lines,"\n"),"print.-%)","")
end

-- Switch print monitoring on/off.
-- If on, all prints called with "special token" are submited to
-- their corresponding hooks callbacks.
function pimp:monitor(flag)
    if flag ~= monitor then
        monitor = flag
        print = monitor and hook or pimp.print
        saveProjectData("pimp_monitor",monitor)
    end
end

-- Print remaping when not monitoring
pimp.print = function(token,...)
    if not pimphook[token] then log(token,...) end
end

-- Return 'adjusted' debug info (as debug.info)
-- at the point where this method get called (assume this one get called from a hook)
-- @return table
--      source      : (string) source file (tab)
--      func        : (function) current function
--      name        : (string) current function name
--      currentline : (int) current line
--      tab         : (string) current tab name
--      level       : (int) debug level. ie:start point for local vars
function pimp:getinfo()
    local l = 1
    while debug.getinfo(l,"f").func ~= hook do
        l = l + 1
    end
    l = l + 1
    local info = debug.getinfo(l)
    info.level = l
    if not info.name then
        -- TODO: push fn refs in a table and check for others:keyboard..'
        local fn = info.func
        if fn == env.draw then info.name = "draw"
        elseif fn ==  env.touched then info.name = "touched"
        elseif fn == env.setup then info.name = "setup"
        end
    end
    local src = info.source
    info.source = string.gsub(string.sub(src,select(2,string.find(src,"%]%s")) + 1),"print.-%)","")
    info.tab = string.match(src,"%[%[(.-)%]")
    return info
end

local function stepin(fn,...)
    -- @internal threads handling
    -- @param fn function to carry
    -- @param arguments to pass along in

    if halt then
        return end

    local co = threads[fn]
    local status = co and coroutine.status(co) or "dead"
    if status == "dead" then
        co = coroutine.create(fn)
        threads[fn] = co
    end

    local succ,res = coroutine.resume(co,...)
    if not succ then
        error(res)
    end
end

local function instep(fn)
    -- Return flag thread ?
    local co = threads[fn]
    return co and coroutine.status(co) == "suspended" or false
end

-- Bootstrap
local osetup
local function setuphook()
    debug.sethook()

    -- init stuff
    log = print

    local mp = readProjectData("pimp_monitor")
    if mp == nil then mp = true end
    parameter.boolean("monitor",mp,
        function(flag)
            pimp:monitor(flag)
        end)

    env = setmetatable({},{ __index = _G })
    env._G = env

    -- if not pimpnotoken then
    -- TODO: dependencies
    local read = readProjectTab
    for _,tab in ipairs(listProjectTabs()) do
        if tab ~= "pimp" then
            local chunk = "--[["..tab.."]] "..pimp:parsetokens(read(tab))
            setfenv(loadstring(chunk),env)()
        end
    end
    
    --[[
    -- Build hooks table
    for k,v in pairs(pimphook) do
        pimphook[k] = v.yield end
      ]]
    
    osetup = env.setup; draw = env.draw; touched = env.touched or noop

    setfenv(osetup,env)
    setfenv(draw,env)
    setfenv(touched,env)
    setfenv(tween.update,env)

    -- end
    
    local _draw = draw
    local backmode
    local function dosetup()
        stepin(osetup)
        if not halt then
            backmode = backingMode() == 0 and
                function()
                    local bm = backingMode()
                    if monitor then
                        if bm == 0 then
                            backingMode(1)
                        end
                        elseif bm == 1 and not halt then
                            backingMode(0)
                        end
                end or noop
            dosetup = noop
        end return true
    end

    local _touched = touched
    local touchstack,touches = {count = -1}
    draw = function()
        ElapsedTime = time
        if dosetup() then
            return end

        if not halt then
            time = time + DeltaTime
            -- consume touch stack
            if #touchstack > 0 then
                for _,v in ipairs(touchstack) do
                    local touch = not instep(_touched) and table.remove(touchstack,1)
                                                    or nil
                    if type(touch) == "number" then -- draw reached
                        break end

                    stepin(_touched,touch)
                    if halt then break end
                end
            end

            backmode() -- ? do  we need that
            stepin(_draw)
        else
            if touchstack.count > -1 then
                local n = #touchstack
                if n > 1 then
                    table.insert(touchstack,1)
                end
            end
        end
    end

    touched = function(touch)
        local state = touch.state
        -- store touches that could be pushed into eval stack during
        -- halt phase
        if halt then
            if touchstack.count > -1 then
                table.insert(touchstack,touch)
                touchstack.count = touchstack.count + 1 - state
            else
                if state == 0 then
                    halt = nil
                end
            end
        else
            if #touchstack == 0 then
                stepin(_touched,touch)
                if halt then
                    touchstack.count = 0
                end
            end
        end
    end

    local _update = tween.update
    tween.update = function(dt)
        stepin(_update,dt)
    end

    time = ElapsedTime
end

if not debug.gethook() then
    debug.sethook(function(e)
        if setup and setup ~= setuphook then
            osetup = setup
            setup = setuphook
        end
    end,"r")
end

-- Tokenizer
local find,match,sub,insert = string.find,string.match,string.sub,table.insert
local getoken,nextline,parseblank,parsetable,parsebloc,parseline
local lines,line,cursor,chunk

getoken = function()
    local pref,token,opts = match(line,"^(.-)%-%-([@!])(.-)$")
    if pref then
        if token == "@" then
            local params = {'"@"'}
            if opts and pimphook then
                for key,hk in pairs(pimphook) do
                    local res = { match(opts,key) }
                    for _,v in ipairs(res) do
                        insert(params,'"'..v..'"')
                    end
                end
            end
            return pref,";print("..table.concat(params,",")..")"
        elseif token == "!" then
            return pref,""
        end
    end
end

nextline = function()
    local pos = cursor
    cursor,line = select(2,find(chunk,"(.-)\n",pos))
    if not cursor then -- last line
        cursor,line = select(2,find(chunk,"([^\n]+)$",pos))
    end
    cursor = cursor + 1
    local prefix,strhook = getoken()
    if prefix then
        line = prefix           
        return parseblank(strhook) or parsetable(strhook)
                or parsebloc(strhook) or parseline(strhook)
    end
end

parseblank = function(strhook)
    if not find(line,"%S") then -- blank line
        insert(lines,line)
        return true
    end
    if find(line,"%-%-[^@%[%[]") then
        local pref,com = match(line,"(.-)(%-%-.-)$")
        if pref and match(pref,"%w") then
            insert(lines,pref..strhook..com)
        else 
            insert(lines,line)
        end
        return true
    end
    if find(line,"[[",1,true) then
        while true do
            insert(lines,line)
            if find(line,"]]",1,true) then
                break
            end
            nextline()
        end
        return true
    end
end

parseline = function(strhook)
    insert(lines,line..strhook)
    return true
end

parsetable = function(strhook)
    if match(line,"{") then
        if match(line,"}") then -- one liner
            insert(lines,line..strhook)
            return true
        end
        insert(lines,line)
        local nest = 0
        while true do               
            if not nextline() then
                if not parseblank(strhook) and not parsebloc(strhook) then
                    if match(line,"{") then
                        nest = nest + 1
                    end
                    if match(line,"}") then
                        if nest > 0 then
                            insert(lines,line)
                            nest = nest - 1
                        else
                            insert(lines,line..strhook)
                            break
                        end
                    else    
                        insert(lines,line)
                    end
                end                 
            end
        end
        return true
    end
end

parsebloc = function(strhook)
    -- TODO repeat + better catch
    if match(line,"if.-then")
    or match(line,"for.-do")
    or match(line,"while.-do")
    or match(line,"function") then
        -- on liner
        if match(line,"end") then
            local repl = match(line,"return") and "return" or "end"
            line = string.gsub(line,repl,strhook.." "..repl)
            insert(lines,line)
            return true
        end

        -- insert first line
        insert(lines,line)
        local res
        while true do
            if not nextline() then
                if match(line,"end") then
                    if not parsebloc(strhook) then
                        -- TODO: fancy format like:
                        -- for i=1,10 do
                        --  x = x + 1 end
                        insert(lines,line)
                        break
                    end
                elseif match(line,"else") then
                    insert(lines,line)
                else
                    if parseblank(strhook) or parsetable(strhook)
                    or parsebloc(strhook) or parseline(strhook) then end
                end
            end
        end
        return true
    end
end

local function nextoken()
    local nxt = select(2,find(chunk,"--@",cursor,true))
    if nxt then         
        nxt = select(2,find(sub(chunk,cursor,nxt),".*\n"))
        if nxt then
            insert(lines,sub(chunk,cursor,cursor + nxt - 2))
            cursor = cursor + nxt
        end
        nextline()
        return true
    end

    local left = sub(chunk,cursor)
    if #left > 0 then
        insert(lines,left)
    end
end

function pimp:parsetokens(src)
    chunk,lines,cursor = src,{},1
    local hastoken = false
    while nextoken() do
        hastoken = true
    end
    
    if hastoken then
        src = table.concat(lines,"\n")
    end
    chunk,lines = nil,nil
    return src,hastoken
end
				

Not a Codea Community user? Then get it now!

Touch and hold the Codea Community logo and hit copy, go back to Codea, paste into a new project and run it !

pimp my print version 0.2