summaryrefslogtreecommitdiff
path: root/love2dToAPK/tools/tools/zbstudio-old-win/src/editor/output.lua
blob: 523ac8cee5ce5857285af412a0fc83c4908b41d7 (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
-- Copyright 2011-15 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 errorlog = bottomnotebook.errorlog

-------
-- setup errorlog
local MESSAGE_MARKER = StylesGetMarker("message")
local PROMPT_MARKER = StylesGetMarker("prompt")
local PROMPT_MARKER_VALUE = 2^PROMPT_MARKER

errorlog:Show(true)
errorlog:SetFont(ide.font.oNormal)
errorlog:StyleSetFont(wxstc.wxSTC_STYLE_DEFAULT, ide.font.oNormal)
errorlog:SetBufferedDraw(not ide.config.hidpi and true or false)
errorlog:StyleClearAll()
errorlog:SetMarginWidth(1, 16) -- marker margin
errorlog:SetMarginType(1, wxstc.wxSTC_MARGIN_SYMBOL)
errorlog:MarkerDefine(StylesGetMarker("message"))
errorlog:MarkerDefine(StylesGetMarker("prompt"))
errorlog:SetReadOnly(true)
if (ide.config.outputshell.usewrap) then
  errorlog:SetWrapMode(wxstc.wxSTC_WRAP_WORD)
  errorlog:SetWrapStartIndent(0)
  errorlog:SetWrapVisualFlags(wxstc.wxSTC_WRAPVISUALFLAG_END)
  errorlog:SetWrapVisualFlagsLocation(wxstc.wxSTC_WRAPVISUALFLAGLOC_END_BY_TEXT)
end

StylesApplyToEditor(ide.config.stylesoutshell,errorlog,ide.font.oNormal,ide.font.oItalic)

function ClearOutput(force)
  if not (force or ide:GetMenuBar():IsChecked(ID_CLEAROUTPUT)) then return end
  errorlog:SetReadOnly(false)
  errorlog:ClearAll()
  errorlog:SetReadOnly(true)
end

local inputBound = 0 -- to track where partial output ends for input editing purposes
local function getInputLine()
  return errorlog:MarkerPrevious(errorlog:GetLineCount()+1, PROMPT_MARKER_VALUE)
end
local function getInputText(bound)
  return errorlog:GetTextRangeDyn(
    errorlog:PositionFromLine(getInputLine())+(bound or 0), errorlog:GetLength())
end
local function updateInputMarker()
  local lastline = errorlog:GetLineCount()-1
  errorlog:MarkerDeleteAll(PROMPT_MARKER)
  errorlog: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<cnt and "\t" or "")
  end

  local promptLine = getInputLine()
  local insertedAt = promptLine == -1 and errorlog:GetLength() or errorlog:PositionFromLine(promptLine) + inputBound
  local current = errorlog:GetReadOnly()
  errorlog:SetReadOnly(false)
  errorlog:InsertTextDyn(insertedAt, errorlog.useraw and message or FixUTF8(message, "\022"))
  errorlog:EmptyUndoBuffer()
  errorlog:SetReadOnly(current)
  errorlog:GotoPos(errorlog:GetLength())
  if promptLine ~= -1 then updateInputMarker() end
end
function DisplayOutput(...)
  errorlog:MarkerAdd(errorlog:GetLineCount()-1, MESSAGE_MARKER)
  DisplayOutputNoMarker(...)
end
function DisplayOutputLn(...)
  DisplayOutput(...)
  DisplayOutputNoMarker("\n")
end

local streamins = {}
local streamerrs = {}
local streamouts = {}
local customprocs = {}
local textout = '' -- this is a buffer for any text sent to external scripts

function DetachChildProcess()
  for _, custom in pairs(customprocs) do
    -- since processes are detached, their END_PROCESS event is not going
    -- to be called; call endcallback() manually if registered.
    if custom.endcallback then custom.endcallback() end
    if custom.proc then custom.proc:Detach() end
  end
end

function CommandLineRunning(uid)
  for pid, custom in pairs(customprocs) do
    if (custom.uid == uid and custom.proc and custom.proc.Exists(tonumber(pid))) then
      return pid, custom.proc
    end
  end

  return
end

function CommandLineToShell(uid,state)
  for pid,custom in pairs(customprocs) do
    if (pid == uid or custom.uid == uid) and custom.proc and custom.proc.Exists(tonumber(pid)) then
      if (streamins[pid]) then streamins[pid].toshell = state end
      if (streamerrs[pid]) then streamerrs[pid].toshell = state end
      return true
    end
  end
end

-- logic to "unhide" wxwidget window using winapi
pcall(require, 'winapi')
local checkstart, checknext, checkperiod
local pid = nil
local function unHideWindow(pidAssign)
  -- skip if not configured to do anything
  if not ide.config.unhidewindow then return end
  if pidAssign then
    pid = pidAssign > 0 and pidAssign or nil
  end
  if pid and winapi then
    local now = TimeGet()
    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.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 ~= -1 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(errorlog)
  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 = TimeGet()

  local streamout = proc and proc:GetOutputStream()
  if streamout then streamouts[pid] = {stream=streamout, callback=stringcallback, out=true} end

  unHideWindow(pid)
  nameTab(errorlog, TR("Output (running)"))

  return pid
end

local readonce = 4096
local maxread = readonce * 10 -- maximum number of bytes to read before pausing
local function getStreams()
  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 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 pfn
        if (v.callback) then
          str,pfn = v.callback(str)
        end
        if not str then
          -- skip if nothing to display
        elseif (v.toshell) then
          DisplayShell(str)
        else
          DisplayOutputNoMarker(str)
          if str and (getInputLine() > -1 or errorlog:GetReadOnly()) then
            ActivateOutput()
            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
      v.stream:Write(str, #str)
      updateInputMarker()
      pfn = pfn and pfn()
    end
  end

  readStream(streamins)
  readStream(streamerrs)
  sendStream(streamouts)
end

errorlog:Connect(wx.wxEVT_END_PROCESS, function(event)
    local pid = event:GetPid()
    if (pid ~= -1) then
      getStreams()
      streamins[pid] = nil
      streamerrs[pid] = nil
      streamouts[pid] = nil

      if not customprocs[pid] then return end
      if customprocs[pid].endcallback then customprocs[pid].endcallback() end
      -- if this wasn't started with CommandLineRun, skip the rest
      if not customprocs[pid].uid then return end

      -- delete markers and set focus to the editor if there is an input marker
      if errorlog:MarkerPrevious(errorlog:GetLineCount(), PROMPT_MARKER_VALUE) > -1 then
        errorlog:MarkerDeleteAll(PROMPT_MARKER)
        local editor = GetEditor()
        -- check if editor still exists; it may not if the window is closed
        if editor then editor:SetFocus() end
      end
      unHideWindow(0)
      DebuggerStop(true)
      nameTab(errorlog, TR("Output"))
      DisplayOutputLn(TR("Program completed in %.2f seconds (pid: %d).")
        :format(TimeGet() - customprocs[pid].started, pid))
      customprocs[pid] = nil
    end
  end)

errorlog:Connect(wx.wxEVT_IDLE, function()
    if (#streamins or #streamerrs) then getStreams() end
    if ide.osname == 'Windows' then unHideWindow() end
  end)

local jumptopatterns = {
  -- <filename>(line,linepos):
  "^%s*(.-)%((%d+),(%d+)%)%s*:",
  -- <filename>(line):
  "^%s*(.-)%((%d+).*%)%s*:",
  --[string "<filename>"]:line:
  '^.-%[string "([^"]+)"%]:(%d+)%s*:',
  -- <filename>:line:linepos
  "^%s*(.-):(%d+):(%d+):",
  -- <filename>:line:
  "^%s*(.-):(%d+)%s*:",
}

errorlog:Connect(wxstc.wxEVT_STC_DOUBLECLICK,
  function(event)
    local line = errorlog:GetCurrentLine()
    local linetx = errorlog:GetLineDyn(line)

    -- try to detect a filename and line in linetx
    local fname, jumpline, jumplinepos
    for _,pattern in ipairs(jumptopatterns) do
      fname,jumpline,jumplinepos = linetx:match(pattern)
      if (fname and jumpline) then break end
    end

    if not (fname and jumpline) then return end

    -- 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 interpreter as network path and
    -- may take few seconds to check.
    local name
    local fixedname = fname:match(":%s+(.+)")
    if fixedname then
      name = GetFullPathIfExists(FileTreeGetDir(), fixedname)
        or FileTreeFindByPartialName(fixedname)
    end
    name = name
      or GetFullPathIfExists(FileTreeGetDir(), fname)
      or FileTreeFindByPartialName(fname)

    local editor = LoadFile(name or fname,nil,true)
    if not editor then
      local ed = GetEditor()
      if ed and ide:GetDocument(ed):GetFileName() == (name or fname) then
        editor = ed
      end
    end
    if editor then
      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()
    end

    -- doubleclick can set selection, so reset it
    local pos = event:GetPosition()
    if pos == -1 then pos = errorlog:GetLineEndPosition(event:GetLine()) end
    errorlog:SetSelection(pos, pos)
  end)

local function positionInLine(line)
  return errorlog:GetCurrentPos() - errorlog:PositionFromLine(line)
end
local function caretOnInputLine(disallowLeftmost)
  local inputLine = getInputLine()
  local boundary = inputBound + (disallowLeftmost and 0 or -1)
  return (errorlog:GetCurrentLine() > inputLine
    or errorlog:GetCurrentLine() == inputLine
   and positionInLine(inputLine) > boundary)
end

errorlog: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
      -- no special processing if it's readonly
      if errorlog:GetReadOnly() then break end

      local key = event:GetKeyCode()
      if key == wx.WXK_UP or key == wx.WXK_NUMPAD_UP then
        if errorlog:GetCurrentLine() > getInputLine() then break
        else return end
      elseif key == wx.WXK_DOWN or key == wx.WXK_NUMPAD_DOWN then
        break -- 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 errorlog:LineFromPosition(errorlog: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
        break
      elseif key == wx.WXK_RETURN or key == wx.WXK_NUMPAD_ENTER then
        if not caretOnInputLine()
        or errorlog:LineFromPosition(errorlog:GetSelectionStart()) < getInputLine() then
          return
        end
        errorlog:GotoPos(errorlog:GetLength()) -- move to the end
        textout = (textout or '') .. getInputText(inputBound)
        -- remove selection if any, otherwise the text gets replaced
        errorlog:SetSelection(errorlog:GetSelectionEnd()+1,errorlog:GetSelectionEnd())
        break -- don't need to do anything else with return
      else
        -- move cursor to end if not already there
        if not caretOnInputLine() then
          errorlog:GotoPos(errorlog:GetLength())
        -- check if the selection starts before the input line and reset it
        elseif errorlog:LineFromPosition(errorlog:GetSelectionStart()) < getInputLine(-1) then
          errorlog:GotoPos(errorlog:GetLength())
          errorlog:SetSelection(errorlog:GetSelectionEnd()+1,errorlog:GetSelectionEnd())
        end
      end
      break
    end
    event:Skip()
  end)

local function inputEditable(line)
  local inputLine = getInputLine()
  local currentLine = line or errorlog:GetCurrentLine()
  return inputLine > -1 and
    (currentLine > inputLine or
     currentLine == inputLine and positionInLine(inputLine) >= inputBound) and
    not (errorlog:LineFromPosition(errorlog:GetSelectionStart()) < getInputLine())
end

errorlog:Connect(wxstc.wxEVT_STC_UPDATEUI,
  function () errorlog:SetReadOnly(not inputEditable()) end)

-- only allow copy/move text by dropping to the input line
errorlog:Connect(wxstc.wxEVT_STC_DO_DROP,
  function (event)
    if not inputEditable(errorlog:LineFromPosition(event:GetPosition())) then
      event:SetDragResult(wx.wxDragNone)
    end
  end)

if ide.config.outputshell.nomousezoom then
  -- disable zoom using mouse wheel as it triggers zooming when scrolling
  -- on OSX with kinetic scroll and then pressing CMD.
  errorlog:Connect(wx.wxEVT_MOUSEWHEEL,
    function (event)
      if wx.wxGetKeyState(wx.WXK_CONTROL) then return end
      event:Skip()
    end)
end