-- Copyright 2011-16 Paul Kulchenko, ZeroBrane LLC -- authors: Lomtik Software (J. Winwood & John Labenski) -- Luxinia Dev (Eike Decker & Christoph Kubisch) --------------------------------------------------------- local ide = ide local frame = ide.frame local bottomnotebook = frame.bottomnotebook local out = bottomnotebook.errorlog local unpack = table.unpack or unpack local MESSAGE_MARKER = StylesGetMarker("message") local ERROR_MARKER = StylesGetMarker("error") local PROMPT_MARKER = StylesGetMarker("prompt") local PROMPT_MARKER_VALUE = 2^PROMPT_MARKER local config = ide.config.output out:SetFont(ide:CreateFont(config.fontsize or 10, wx.wxFONTFAMILY_MODERN, wx.wxFONTSTYLE_NORMAL, wx.wxFONTWEIGHT_NORMAL, false, config.fontname or "", config.fontencoding or wx.wxFONTENCODING_DEFAULT) ) out:StyleSetFont(wxstc.wxSTC_STYLE_DEFAULT, out:GetFont()) out:StyleClearAll() out:SetMarginWidth(1, 16) -- marker margin out:SetMarginType(1, wxstc.wxSTC_MARGIN_SYMBOL) out:MarkerDefine(StylesGetMarker("message")) out:MarkerDefine(StylesGetMarker("error")) out:MarkerDefine(StylesGetMarker("prompt")) out:SetReadOnly(true) if config.usewrap then out:SetWrapMode(wxstc.wxSTC_WRAP_WORD) out:SetWrapStartIndent(0) out:SetWrapVisualFlags(wxstc.wxSTC_WRAPVISUALFLAG_END) out:SetWrapVisualFlagsLocation(wxstc.wxSTC_WRAPVISUALFLAGLOC_END_BY_TEXT) end function OutputAddStyles(styles) if ide.config.output.showansi and wxstc.wxSTC_LEX_ERRORLIST and type(ide.config.output.ansimap) == type({}) then out:SetLexer(wxstc.wxSTC_LEX_ERRORLIST) out:SetProperty("lexer.errorlist.escape.sequences","1") -- assign ansimap styles -- if this styles table is the same as the default one, then make a copy -- to avoid modifying all editor styles with "ansi" ones, -- as they will conflict with lexer-specific styles if ide.config.styles == styles then local stylecopy = StylesGetDefault() for k,v in pairs(styles) do stylecopy[k] = v end styles = stylecopy ide.config.stylesoutshell = styles end for k,v in pairs(ide.config.output.ansimap) do styles["ansi"..k] = v end end end OutputAddStyles(ide.config.stylesoutshell) StylesApplyToEditor(ide.config.stylesoutshell,out) function ClearOutput(force) if not (force or ide:GetMenuBar():IsChecked(ID.CLEAROUTPUTENABLE)) then return end out:SetReadOnly(false) out:ClearAll() out:SetReadOnly(true) end function out:Erase() ClearOutput(true) end local inputBound = 0 -- to track where partial output ends for input editing purposes local function getInputLine() return out:MarkerPrevious(out:GetLineCount()+1, PROMPT_MARKER_VALUE) end local function getInputText(bound) return out:GetTextRangeDyn( out:PositionFromLine(getInputLine())+(bound or 0), out:GetLength()) end local function updateInputMarker() local lastline = out:GetLineCount()-1 out:MarkerDeleteAll(PROMPT_MARKER) out:MarkerAdd(lastline, PROMPT_MARKER) inputBound = #getInputText() end function OutputEnableInput() updateInputMarker() end function DisplayOutputNoMarker(...) local message = "" local cnt = select('#',...) for i=1,cnt do local v = select(i,...) message = message..tostring(v)..(i 0 and pidAssign or nil end if pid and winapi then -- pid provided and winapi loaded local now = ide:GetTime() if pidAssign and pidAssign > 0 then checkstart, checknext, checkperiod = now, now, 0.02 end if now - checkstart > 1 and checkperiod < 0.5 then checkperiod = checkperiod * 2 end if now >= checknext then checknext = now + checkperiod else return end local wins = winapi.find_all_windows(function(w) return w:get_process():get_pid() == pid end) local any = ide.interpreter and ide.interpreter.unhideanywindow local show, hide, ignore = 1, 2, 0 for _,win in pairs(wins) do -- win:get_class_name() can return nil if the window is already gone -- between getting the list and this check. local action = ide.config.unhidewindow[win:get_class_name()] or (any and show or ignore) if action == show and not win:is_visible() or action == hide and win:is_visible() then -- use show_async call (ShowWindowAsync) to avoid blocking the IDE -- if the app is busy or is being debugged win:show_async(action == show and winapi.SW_SHOW or winapi.SW_HIDE) pid = nil -- indicate that unhiding is done end end end end local function nameTab(tab, name) local index = bottomnotebook:GetPageIndex(tab) if index ~= wx.wxNOT_FOUND then bottomnotebook:SetPageText(index, name) end end function OutputSetCallbacks(pid, proc, callback, endcallback) local streamin = proc and proc:GetInputStream() local streamerr = proc and proc:GetErrorStream() if streamin then streamins[pid] = {stream=streamin, callback=callback, proc=proc, check=proc and proc.IsInputAvailable} end if streamerr then streamerrs[pid] = {stream=streamerr, callback=callback, proc=proc, check=proc and proc.IsErrorAvailable} end customprocs[pid] = {proc=proc, endcallback=endcallback} end function CommandLineRun(cmd,wdir,tooutput,nohide,stringcallback,uid,endcallback) if (not cmd) then return end -- expand ~ at the beginning of the command if ide.oshome and cmd:find('~') then cmd = cmd:gsub([[^(['"]?)~]], '%1'..ide.oshome:gsub('[\\/]$',''), 1) end -- try to extract the name of the executable from the command -- the executable may not have the extension and may be in quotes local exename = string.gsub(cmd, "\\", "/") local _,_,fullname = string.find(exename,'^[\'"]([^\'"]+)[\'"]') exename = fullname and string.match(fullname,'/?([^/]+)$') or string.match(exename,'/?([^/]-)%s') or exename uid = uid or exename if (CommandLineRunning(uid)) then DisplayOutputLn(TR("Program can't start because conflicting process is running as '%s'.") :format(cmd)) return end DisplayOutputLn(TR("Program starting as '%s'."):format(cmd)) local proc = wx.wxProcess(out) if (tooutput) then proc:Redirect() end -- redirect the output if requested -- set working directory if specified local oldcwd if (wdir and #wdir > 0) then -- directory can be empty; ignore in this case oldcwd = wx.wxFileName.GetCwd() oldcwd = wx.wxFileName.SetCwd(wdir) and oldcwd end -- launch process local params = wx.wxEXEC_ASYNC + wx.wxEXEC_MAKE_GROUP_LEADER + (nohide and wx.wxEXEC_NOHIDE or 0) local pid = wx.wxExecute(cmd, params, proc) if oldcwd then wx.wxFileName.SetCwd(oldcwd) end -- For asynchronous execution, the return value is the process id and -- zero value indicates that the command could not be executed. -- The return value of -1 in this case indicates that we didn't launch -- a new process, but connected to the running one (e.g. DDE under Windows). if not pid or pid == -1 or pid == 0 then DisplayOutputLn(TR("Program unable to run as '%s'."):format(cmd)) return end DisplayOutputLn(TR("Program '%s' started in '%s' (pid: %d).") :format(uid, (wdir and wdir or wx.wxFileName.GetCwd()), pid)) OutputSetCallbacks(pid, proc, stringcallback, endcallback) customprocs[pid].uid=uid customprocs[pid].started = ide:GetTime() local streamout = proc and proc:GetOutputStream() if streamout then streamouts[pid] = {stream=streamout, callback=stringcallback, out=true} end unHideWindow(pid) nameTab(out, TR("Output (running)")) return pid end ide:GetCodePage() -- populate the codepage value if auto-detection is requested local readonce = 4096 local maxread = readonce * 10 -- maximum number of bytes to read before pausing local function getStreams(all) local function readStream(tab) for _,v in pairs(tab) do -- periodically stop reading to get a chance to process other events local processed = 0 while (v.check(v.proc) and (all or processed <= maxread)) do local str = v.stream:Read(readonce) -- the buffer has readonce bytes, so cut it to the actual size str = str:sub(1, v.stream:LastRead()) processed = processed + #str local codepage = ide:GetCodePage() if codepage and FixUTF8(str) == nil and winapi then -- this looks like invalid UTF-8 content, which may be in a different code page str = winapi.encode(codepage, winapi.CP_UTF8, str) end local pfn if (v.callback) then str,pfn = v.callback(str) end if not str then -- skip if nothing to display elseif (v.toshell) then ide:GetConsole():Print(str) else DisplayOutputNoMarker(str) if str and (getInputLine() ~= wx.wxNOT_FOUND or out:GetReadOnly()) then ide:GetOutput():Activate() updateInputMarker() end end pfn = pfn and pfn() end end end local function sendStream(tab) local str = textout if not str then return end textout = nil str = str .. "\n" for _,v in pairs(tab) do local pfn if v.callback then str,pfn = v.callback(str) end if str then v.stream:Write(str, #str) end updateInputMarker() pfn = pfn and pfn() end end readStream(streamins) readStream(streamerrs) sendStream(streamouts) end function out:ProcessStreams() if (#streamins or #streamerrs) then getStreams() end end out:Connect(wx.wxEVT_END_PROCESS, function(event) local pid = event:GetPid() if (pid ~= -1) then getStreams(true) streamins[pid] = nil streamerrs[pid] = nil streamouts[pid] = nil if not customprocs[pid] then return end if customprocs[pid].endcallback then local ok, err = pcall(customprocs[pid].endcallback, pid, event:GetExitCode()) if not ok then ide:GetOutput():Error(("Post processing execution failed: %s"):format(err)) end end -- if this was started with uid (`CommandLineRun`), then it needs additional processing if customprocs[pid].uid then -- delete markers and set focus to the editor if there is an input marker if out:MarkerPrevious(out:GetLineCount(), PROMPT_MARKER_VALUE) > wx.wxNOT_FOUND then out:MarkerDeleteAll(PROMPT_MARKER) local editor = ide:GetEditor() -- check if editor still exists; it may not if the window is closed if editor then editor:SetFocus() end end unHideWindow(0) ide:SetLaunchedProcess(nil) nameTab(out, TR("Output")) DisplayOutputLn(TR("Program completed in %.2f seconds (pid: %d).") :format(ide:GetTime() - customprocs[pid].started, pid)) end -- this protects against the object referenced in wxProcess being collected -- before the wxProcess itself is collected, which may cause a crash on exit if customprocs[pid].proc then customprocs[pid].proc:Detach() end customprocs[pid] = nil end end) out:Connect(wx.wxEVT_IDLE, function() out:ProcessStreams() if ide.osname == 'Windows' then unHideWindow() end end) local function activateByPartialName(fname, jumpline, jumplinepos) -- fname may include name of executable, as in "path/to/lua: file.lua"; -- strip it and try to find match again if needed. -- try the stripped name first as if it doesn't match, the longer -- name may have parts that may be interpreted as a network path and -- may take few seconds to check. local name local fixedname = fname:match(":%s+(.+)") if fixedname then name = GetFullPathIfExists(ide:GetProject(), fixedname) or FileTreeFindByPartialName(fixedname) end name = name or GetFullPathIfExists(ide:GetProject(), fname) or FileTreeFindByPartialName(fname) local editor = LoadFile(name or fname,nil,true) if not editor then local ed = ide:GetEditor() if ed and ide:GetDocument(ed):GetFileName() == (name or fname) then editor = ed end end if not editor then return false end jumpline = tonumber(jumpline) jumplinepos = tonumber(jumplinepos) editor:GotoPos(editor:PositionFromLine(math.max(0,jumpline-1)) + (jumplinepos and (math.max(0,jumplinepos-1)) or 0)) editor:EnsureVisibleEnforcePolicy(jumpline) editor:SetFocus() return true end out:Connect(wxstc.wxEVT_STC_DOUBLECLICK, function(event) local line = out:GetCurrentLine() local linetx = out:GetLineDyn(line) -- try to detect a filename and line in linetx for pattern, multiple in pairs(ide.config.output.lineactivate or {}) do local results = {} for fname, jumpline, jumplinepos in linetx:gmatch(pattern) do -- insert matches in reverse order (if any) table.insert(results, 1, {fname, jumpline, jumplinepos}) if type(multiple) == "function" then results[1] = {multiple(unpack(results[1]))} end if multiple ~= true then break end -- one match is enough if no multiple is requested end for _, result in ipairs(results) do if activateByPartialName(unpack(result)) then -- doubleclick can set selection, so reset it local pos = event:GetPosition() if pos == wx.wxNOT_FOUND then pos = out:GetLineEndPosition(event:GetLine()) end out:SetSelection(pos, pos) return end end end event:Skip() end) local function positionInLine(line) return out:GetCurrentPos() - out:PositionFromLine(line) end local function caretOnInputLine(disallowLeftmost) local inputLine = getInputLine() local boundary = inputBound + (disallowLeftmost and 0 or -1) return (out:GetCurrentLine() > inputLine or out:GetCurrentLine() == inputLine and positionInLine(inputLine) > boundary) end out:Connect(wx.wxEVT_KEY_DOWN, function (event) local key = event:GetKeyCode() if out:GetReadOnly() then -- no special processing if it's readonly elseif key == wx.WXK_UP or key == wx.WXK_NUMPAD_UP then if out:GetCurrentLine() <= getInputLine() then return end elseif key == wx.WXK_DOWN or key == wx.WXK_NUMPAD_DOWN then -- can go down elseif key == wx.WXK_LEFT or key == wx.WXK_NUMPAD_LEFT then if not caretOnInputLine(true) then return end elseif key == wx.WXK_BACK then if not caretOnInputLine(true) then return end elseif key == wx.WXK_DELETE or key == wx.WXK_NUMPAD_DELETE then if not caretOnInputLine() or out:LineFromPosition(out:GetSelectionStart()) < getInputLine() 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 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 -- fall through elseif key == wx.WXK_RETURN or key == wx.WXK_NUMPAD_ENTER then if not caretOnInputLine() or out:LineFromPosition(out:GetSelectionStart()) < getInputLine() then return end out:GotoPos(out:GetLength()) -- move to the end textout = (textout or '') .. getInputText(inputBound) -- remove selection if any, otherwise the text gets replaced out:SetSelection(out:GetSelectionEnd()+1,out:GetSelectionEnd()) -- don't need to do anything else with return else -- move cursor to end if not already there if not caretOnInputLine() then out:GotoPos(out:GetLength()) -- check if the selection starts before the input line and reset it elseif out:LineFromPosition(out:GetSelectionStart()) < getInputLine(-1) then out:GotoPos(out:GetLength()) out:SetSelection(out:GetSelectionEnd()+1,out:GetSelectionEnd()) end end event:Skip() end) local function inputEditable(line) local inputLine = getInputLine() local currentLine = line or out:GetCurrentLine() return inputLine ~= wx.wxNOT_FOUND and (currentLine > inputLine or currentLine == inputLine and positionInLine(inputLine) >= inputBound) and not (out:LineFromPosition(out:GetSelectionStart()) < getInputLine()) end out:Connect(wxstc.wxEVT_STC_UPDATEUI, function() out:SetReadOnly(not inputEditable()) end) -- only allow copy/move text by dropping to the input line out:Connect(wxstc.wxEVT_STC_DO_DROP, function (event) if not inputEditable(out: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. out:Connect(wx.wxEVT_MOUSEWHEEL, function (event) if wx.wxGetKeyState(wx.WXK_CONTROL) then return end event:Skip() end) end