Permalink
Fetching contributors…
Cannot retrieve contributors at this time
604 lines (522 sloc) 21.4 KB
-- Copyright 2011-16 Paul Kulchenko, ZeroBrane LLC
-- authors: Luxinia Dev (Eike Decker & Christoph Kubisch)
---------------------------------------------------------
local ide = ide
local unpack = table.unpack or unpack
local bottomnotebook = ide.frame.bottomnotebook
local console = bottomnotebook.shellbox
local remotesend
local PROMPT_MARKER = StylesGetMarker("prompt")
local PROMPT_MARKER_VALUE = 2^PROMPT_MARKER
local ERROR_MARKER = StylesGetMarker("error")
local OUTPUT_MARKER = StylesGetMarker("output")
local MESSAGE_MARKER = StylesGetMarker("message")
local config = ide.config.console
console:SetFont(wx.wxFont(config.fontsize or 10, wx.wxFONTFAMILY_MODERN, wx.wxFONTSTYLE_NORMAL,
wx.wxFONTWEIGHT_NORMAL, false, config.fontname or "",
config.fontencoding or wx.wxFONTENCODING_DEFAULT)
)
console:StyleSetFont(wxstc.wxSTC_STYLE_DEFAULT, console:GetFont())
console:SetBufferedDraw(not ide.config.hidpi and true or false)
console:StyleClearAll()
console:SetTabWidth(ide.config.editor.tabwidth or 2)
console:SetIndent(ide.config.editor.tabwidth or 2)
console:SetUseTabs(ide.config.editor.usetabs and true or false)
console:SetViewWhiteSpace(ide.config.editor.whitespace and true or false)
console:SetIndentationGuides(true)
console:SetWrapMode(wxstc.wxSTC_WRAP_WORD)
console:SetWrapStartIndent(0)
console:SetWrapVisualFlagsLocation(wxstc.wxSTC_WRAPVISUALFLAGLOC_END_BY_TEXT)
console:SetWrapVisualFlags(wxstc.wxSTC_WRAPVISUALFLAG_END)
console:MarkerDefine(StylesGetMarker("prompt"))
console:MarkerDefine(StylesGetMarker("error"))
console:MarkerDefine(StylesGetMarker("output"))
console:MarkerDefine(StylesGetMarker("message"))
console:SetReadOnly(false)
SetupKeywords(console,"lua",nil,ide.config.stylesoutshell)
local function getPromptLine()
local totalLines = console:GetLineCount()
return console:MarkerPrevious(totalLines+1, PROMPT_MARKER_VALUE)
end
local function getPromptText()
local prompt = getPromptLine()
return console:GetTextRangeDyn(console:PositionFromLine(prompt), console:GetLength())
end
local function setPromptText(text)
local length = console:GetLength()
console:SetSelectionStart(length - string.len(getPromptText()))
console:SetSelectionEnd(length)
console:ClearAny()
console:AddTextDyn(text)
-- refresh the output window to force recalculation of wrapped lines;
-- otherwise a wrapped part of the last line may not be visible.
console:Update(); console:Refresh()
console:GotoPos(console:GetLength())
end
local function positionInLine(line)
return console:GetCurrentPos() - console:PositionFromLine(line)
end
local function caretOnPromptLine(disallowLeftmost, line)
local promptLine = getPromptLine()
local currentLine = line or console:GetCurrentLine()
local boundary = disallowLeftmost and 0 or -1
return (currentLine > promptLine
or currentLine == promptLine and positionInLine(promptLine) > boundary)
end
local function chomp(line) return (line:gsub("%s+$", "")) end
local function getInput(line)
local nextMarker = line
local count = console:GetLineCount()
repeat -- check until we find at least some marker
nextMarker = nextMarker+1
until console:MarkerGet(nextMarker) > 0 or nextMarker > count-1
return chomp(console:GetTextRangeDyn(
console:PositionFromLine(line), console:PositionFromLine(nextMarker)))
end
function ConsoleSelectCommand(point)
local cpos = console:ScreenToClient(point or wx.wxGetMousePosition())
local position = console:PositionFromPoint(cpos)
if position == wxstc.wxSTC_INVALID_POSITION then return end
local promptline = console:MarkerPrevious(console:LineFromPosition(position), PROMPT_MARKER_VALUE)
if promptline == wxstc.wxSTC_INVALID_POSITION then return end
local nextline = console:MarkerNext(promptline+1, ide.ANYMARKERMASK)
local epos = nextline ~= wxstc.wxSTC_INVALID_POSITION and console:PositionFromLine(nextline) or console:GetLength()
console:SetSelection(console:PositionFromLine(promptline), epos)
return true
end
local currentHistory
local lastCommand = ""
local function getNextHistoryLine(forward, promptText)
local count = console:GetLineCount()
if currentHistory == nil then currentHistory = count end
if forward then
currentHistory = console:MarkerNext(currentHistory+1, PROMPT_MARKER_VALUE)
if currentHistory == wx.wxNOT_FOUND then
currentHistory = count
return ""
end
else
currentHistory = console:MarkerPrevious(currentHistory-1, PROMPT_MARKER_VALUE)
if currentHistory == wx.wxNOT_FOUND then
return lastCommand
end
end
-- need to skip the current prompt line
-- or skip repeated commands
if currentHistory == getPromptLine()
or getInput(currentHistory) == promptText then
return getNextHistoryLine(forward, promptText)
end
return getInput(currentHistory)
end
local function getNextHistoryMatch(promptText)
local count = console:GetLineCount()
if currentHistory == nil then currentHistory = count end
local current = currentHistory
while true do
currentHistory = console:MarkerPrevious(currentHistory-1, PROMPT_MARKER_VALUE)
if currentHistory == wx.wxNOT_FOUND then -- restart search from the last item
currentHistory = count
elseif currentHistory ~= getPromptLine() then -- skip current prompt
local input = getInput(currentHistory)
if input:find(promptText, 1, true) == 1 then return input end
end
-- couldn't find anything and made a loop; get out
if currentHistory == current then return end
end
assert(false, "getNextHistoryMatch coudn't find a proper match")
end
local function concat(sep, ...)
local text = ""
for i=1, select('#',...) do
text = text .. (i > 1 and sep or "") .. tostring(select(i,...))
end
-- split the text into smaller chunks as one large line
-- is difficult to handle for the editor
local prev, maxlength = 0, ide.config.debugger.maxdatalength
if #text > maxlength and not text:find("\n.") then
text = text:gsub("()(%s+)", function(p, s)
if p-prev >= maxlength then
prev = p
return "\n"
else
return s
end
end)
end
return text
end
local partial = false
local function shellPrint(marker, text, newline)
if not text or text == "" then return end -- return if nothing to print
if newline then text = text:gsub("\n+$", "").."\n" end
local isPrompt = marker and (getPromptLine() ~= wx.wxNOT_FOUND)
local lines = console:GetLineCount()
local promptLine = isPrompt and getPromptLine() or nil
local insertLineAt = isPrompt and not partial and getPromptLine() or console:GetLineCount()-1
local insertAt = isPrompt and not partial and console:PositionFromLine(getPromptLine()) or console:GetLength()
console:InsertTextDyn(insertAt, console.useraw and text or FixUTF8(text, function (s) return '\\'..string.byte(s) end))
local linesAdded = console:GetLineCount() - lines
partial = text:find("\n$") == nil
if marker then
if promptLine then console:MarkerDelete(promptLine, PROMPT_MARKER) end
for line = insertLineAt, insertLineAt + linesAdded - 1 do
console:MarkerAdd(line, marker)
end
if promptLine then console:MarkerAdd(promptLine+linesAdded, PROMPT_MARKER) end
end
console:EmptyUndoBuffer() -- don't allow the user to undo shell text
console:GotoPos(console:GetLength())
console:EnsureVisibleEnforcePolicy(console:GetLineCount()-1)
end
displayShellDirect = function (...) shellPrint(nil, concat("\t", ...), true) end
DisplayShell = function (...) shellPrint(OUTPUT_MARKER, concat("\t", ...), true) end
DisplayShellErr = function (...) shellPrint(ERROR_MARKER, concat("\t", ...), true) end
DisplayShellMsg = function (...) shellPrint(MESSAGE_MARKER, concat("\t", ...), true) end
-- don't print anything; just mark the line with a prompt mark
DisplayShellPrompt = function (...) console:MarkerAdd(console:GetLineCount()-1, PROMPT_MARKER) end
function console:Print(...) return DisplayShell(...) end
function console:Write(...) return shellPrint(OUTPUT_MARKER, concat("", ...), false) end
function console:Error(...) return DisplayShellErr(...) end
local function filterTraceError(err, addedret)
local err = err:match("(.-:%d+:.-)\n[^\n]*\n[^\n]*\n[^\n]*src/editor/shellbox.lua:.*in function 'executeShellCode'")
or err
err = err:gsub("stack traceback:.-\n[^\n]+\n?","")
if addedret then err = err:gsub('^%[string "return ', '[string "') end
err = err:match("(.*)\n[^\n]*%(tail call%): %?$") or err
return err
end
local function createenv()
local env = {}
setmetatable(env,{__index = _G})
local function luafilename(level)
level = level and level + 1 or 2
local src
while (true) do
src = debug.getinfo(level)
if (src == nil) then return nil,level end
if (string.byte(src.source) == string.byte("@")) then
return string.sub(src.source,2),level
end
level = level + 1
end
end
local function luafilepath(level)
local src,level = luafilename(level)
if (src == nil) then return src,level end
src = string.gsub(src,"[\\/][^\\//]*$","")
return src,level
end
local function relativeFilename(file)
assert(type(file)=='string',"String as filename expected")
local name = file
local level = 3
while (name) do
if (wx.wxFileName(name):FileExists()) then return name end
name,level = luafilepath(level)
if (name == nil) then break end
name = name .. "/" .. file
end
return file
end
local function relativeFilepath(file)
local name = luafilepath(3)
return (file and name) and name.."/"..file or file or name
end
local _loadfile = loadfile
local function loadfile(file)
assert(type(file)=='string',"String as filename expected")
local name = relativeFilename(file)
return _loadfile(name)
end
local function dofile(file, ...)
assert(type(file) == 'string',"String as filename expected")
local fn,err = loadfile(file)
local args = {...}
if not fn then
DisplayShellErr(err)
else
setfenv(fn,env)
return fn(unpack(args))
end
end
local os = {
exit = function()
ide.frame:AddPendingEvent(wx.wxCommandEvent(wx.wxEVT_COMMAND_MENU_SELECTED, ID_EXIT))
end,
}
env.os = setmetatable(os, {__index = _G.os})
env.io = setmetatable({write = function(...) console:Write(...) end}, {__index = _G.io})
env.print = function(...) console:Print(...) end
env.dofile = dofile
env.loadfile = loadfile
env.RELFILE = relativeFilename
env.RELPATH = relativeFilepath
return env
end
local env = createenv()
function ShellSetAlias(alias, table)
local value = env[alias]
env[alias] = table
return value
end
local function packResults(status, ...) return status, {...} end
local function executeShellCode(tx)
if tx == nil or tx == '' then return end
local forcelocalprefix = '^!'
local forcelocal = tx:find(forcelocalprefix)
tx = tx:gsub(forcelocalprefix, '')
DisplayShellPrompt('')
-- try to compile as statement
local _, err = loadstring(tx)
local isstatement = not err
if remotesend and not forcelocal then remotesend(tx, isstatement); return end
local addedret, forceexpression = true, tx:match("^%s*=%s*")
tx = tx:gsub("^%s*=%s*","")
local fn
fn, err = loadstring("return "..tx)
if not forceexpression and err then
fn, err = loadstring(tx)
addedret = false
end
if fn == nil and err then
DisplayShellErr(filterTraceError(err, addedret))
elseif fn then
setfenv(fn,env)
-- set the project dir as the current dir to allow "require" calls
-- to work from shell
local projectDir, cwd = ide:GetProject(), nil
if projectDir and #projectDir > 0 then
cwd = wx.wxFileName.GetCwd()
wx.wxFileName.SetCwd(projectDir)
end
local ok, res = packResults(xpcall(fn,
function(err)
DisplayShellErr(filterTraceError(debug.traceback(err), addedret))
end))
-- restore the current dir
if cwd then wx.wxFileName.SetCwd(cwd) end
if ok and (addedret or #res > 0) then
if addedret then
local mobdebug = require "mobdebug"
for i,v in pairs(res) do -- stringify each of the returned values
res[i] = (forceexpression and i > 1 and '\n' or '') ..
mobdebug.line(v, {nocode = true, comment = 1,
-- if '=' is used, then use multi-line serialized output
indent = forceexpression and ' ' or nil})
end
-- add nil only if we are forced (using =) or if this is not a statement
-- this is needed to print 'nil' when asked for 'foo',
-- and don't print it when asked for 'print(1)'
if #res == 0 and (forceexpression or not isstatement) then
res = {'nil'}
end
end
DisplayShell(unpack(res))
end
end
end
function console:GetRemote() return remotesend end
function console:SetRemote(client)
remotesend = client
local index = bottomnotebook:GetPageIndex(console)
if index then
bottomnotebook:SetPageText(index,
client and TR("Remote console") or TR("Local console"))
end
end
function ShellExecuteFile(wfilename)
if (not wfilename) then return end
local cmd = 'dofile([['..wfilename:GetFullPath()..']])'
ShellExecuteCode(cmd)
end
ShellExecuteInline = executeShellCode
function ShellExecuteCode(code)
local index = bottomnotebook:GetPageIndex(bottomnotebook.shellbox)
if ide.config.activateoutput and bottomnotebook:GetSelection() ~= index then
bottomnotebook:SetSelection(index)
end
displayShellDirect(code)
executeShellCode(code)
end
local function displayShellIntro()
DisplayShellMsg(TR("Welcome to the interactive Lua interpreter.").." "
..TR("Enter Lua code and press Enter to run it.").." "
..TR("Use Shift-Enter for multiline code.").."\n"
..TR("Use 'clear' to clear the shell output and the history.").." "
..TR("Use 'reset' to clear the environment.").."\n"
..TR("Prepend '=' to show complex values on multiple lines.").." "
..TR("Prepend '!' to force local execution."))
DisplayShellPrompt('')
end
console:Connect(wx.wxEVT_KEY_DOWN,
function (event)
-- this loop is only needed to allow to get to the end of function easily
-- "return" aborts the processing and ignores the key
-- "break" aborts the processing and processes the key normally
while true do
local key = event:GetKeyCode()
local modifiers = event:GetModifiers()
if key == wx.WXK_UP or key == wx.WXK_NUMPAD_UP then
-- if we are below the prompt line, then allow to go up
-- through multiline entry
if console:GetCurrentLine() > getPromptLine() then break end
-- if we are not on the caret line, or are on wrapped caret line, move normally
if not caretOnPromptLine()
or console:GetLineWrapped(console:GetCurrentPos(), -1) then break end
-- only change prompt if no modifiers are used (to allow for selection movement)
if modifiers == wx.wxMOD_NONE then
local promptText = getPromptText()
setPromptText(getNextHistoryLine(false, promptText))
-- move to the beginning of the updated prompt
console:GotoPos(console:PositionFromLine(getPromptLine()))
end
return
elseif key == wx.WXK_DOWN or key == wx.WXK_NUMPAD_DOWN then
-- if we are above the last line, then allow to go down
-- through multiline entry
local totalLines = console:GetLineCount()-1
if console:GetCurrentLine() < totalLines then break end
-- if we are not on the caret line, or are on wrapped caret line, move normally
if not caretOnPromptLine()
or console:GetLineWrapped(console:GetCurrentPos(), 1) then break end
-- only change prompt if no modifiers are used (to allow for selection movement)
if modifiers == wx.wxMOD_NONE then
local promptText = getPromptText()
setPromptText(getNextHistoryLine(true, promptText))
-- staying at the end of the updated prompt
end
return
elseif key == wx.WXK_TAB then
-- if we are above the prompt line, then don't move
local promptline = getPromptLine()
if console:GetCurrentLine() < promptline then return end
local promptText = getPromptText()
-- save the position in the prompt text to restore
local pos = console:GetCurrentPos()
local text = promptText:sub(1, positionInLine(promptline))
if #text == 0 then return end
-- find the next match and set the prompt text
local match = getNextHistoryMatch(text)
if match then
setPromptText(match)
-- restore the position to make it easier to find the next match
console:GotoPos(pos)
end
return
elseif key == wx.WXK_ESCAPE then
setPromptText("")
return
elseif key == wx.WXK_BACK or key == wx.WXK_LEFT or key == wx.WXK_NUMPAD_LEFT then
if (key == wx.WXK_BACK or console:LineFromPosition(console:GetCurrentPos()) >= getPromptLine())
and not caretOnPromptLine(true) then return end
elseif key == wx.WXK_DELETE or key == wx.WXK_NUMPAD_DELETE then
if not caretOnPromptLine()
or console:LineFromPosition(console:GetSelectionStart()) < getPromptLine() then
return
end
elseif key == wx.WXK_PAGEUP or key == wx.WXK_NUMPAD_PAGEUP
or key == wx.WXK_PAGEDOWN or key == wx.WXK_NUMPAD_PAGEDOWN
or key == wx.WXK_END or key == wx.WXK_NUMPAD_END
or key == wx.WXK_HOME or key == wx.WXK_NUMPAD_HOME
-- `key == wx.WXK_LEFT or key == wx.WXK_NUMPAD_LEFT` are handled separately
or key == wx.WXK_RIGHT or key == wx.WXK_NUMPAD_RIGHT
or key == wx.WXK_SHIFT or key == wx.WXK_CONTROL
or key == wx.WXK_ALT then
break
elseif key == wx.WXK_RETURN or key == wx.WXK_NUMPAD_ENTER then
if not caretOnPromptLine()
or console:LineFromPosition(console:GetSelectionStart()) < getPromptLine() then
return
end
-- allow multiline entry for shift+enter
if caretOnPromptLine(true) and event:ShiftDown() then break end
local promptText = getPromptText()
if #promptText == 0 then return end -- nothing to execute, exit
if promptText == 'clear' then
console:Erase()
elseif promptText == 'reset' then
console:Reset()
setPromptText("")
else
displayShellDirect('\n')
executeShellCode(promptText)
end
currentHistory = getPromptLine() -- reset history
return -- don't need to do anything else with return
elseif modifiers == wx.wxMOD_NONE or console:GetSelectedText() == "" then
-- move cursor to end if not already there
if not caretOnPromptLine() then
console:GotoPos(console:GetLength())
console:SetReadOnly(false) -- allow the current character to appear at the new location
-- check if the selection starts before the prompt line and reset it
elseif console:LineFromPosition(console:GetSelectionStart()) < getPromptLine() then
console:GotoPos(console:GetLength())
console:SetSelection(console:GetSelectionEnd()+1,console:GetSelectionEnd())
end
end
break
end
event:Skip()
end)
local function inputEditable(line)
return caretOnPromptLine(false, line) and
not (console:LineFromPosition(console:GetSelectionStart()) < getPromptLine())
end
-- Scintilla 3.2.1+ changed the way markers move when the text is updated
-- ticket: http://sourceforge.net/p/scintilla/bugs/939/
-- discussion: https://groups.google.com/forum/?hl=en&fromgroups#!topic/scintilla-interest/4giFiKG4VXo
if ide.wxver >= "2.9.5" then
-- this is a workaround that stores a position of the last prompt marker
-- before insert and restores the same position after as the marker
-- could have moved if the text is added at the beginning of the line.
local promptAt
console:Connect(wxstc.wxEVT_STC_MODIFIED,
function (event)
local evtype = event:GetModificationType()
if bit.band(evtype, wxstc.wxSTC_MOD_BEFOREINSERT) ~= 0 then
local promptLine = getPromptLine()
if promptLine and event:GetPosition() == console:PositionFromLine(promptLine)
then promptAt = promptLine end
end
if bit.band(evtype, wxstc.wxSTC_MOD_INSERTTEXT) ~= 0 then
local promptLine = getPromptLine()
if promptLine and promptAt then
console:MarkerDelete(promptLine, PROMPT_MARKER)
console:MarkerAdd(promptAt, PROMPT_MARKER)
promptAt = nil
end
end
end)
end
console:Connect(wxstc.wxEVT_STC_UPDATEUI,
function (event) console:SetReadOnly(not inputEditable()) end)
-- only allow copy/move text by dropping to the input line
console:Connect(wxstc.wxEVT_STC_DO_DROP,
function (event)
if not inputEditable(console:LineFromPosition(event:GetPosition())) then
event:SetDragResult(wx.wxDragNone)
end
end)
if config.nomousezoom then
-- disable zoom using mouse wheel as it triggers zooming when scrolling
-- on OSX with kinetic scroll and then pressing CMD.
console:Connect(wx.wxEVT_MOUSEWHEEL,
function (event)
if wx.wxGetKeyState(wx.WXK_CONTROL) then return end
event:Skip()
end)
end
displayShellIntro()
function console:Erase()
-- save the last command to keep when the history is cleared
currentHistory = getPromptLine()
lastCommand = getNextHistoryLine(false, "")
-- allow writing as the editor may be read-only depending on current cursor position
self:SetReadOnly(false)
self:ClearAll()
displayShellIntro()
end
function console:Reset()
env = createenv() -- recreate the environment to "forget" all changes in it
end