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
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
|
-- Copyright 2011-15 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 out = 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 ANY_MARKER_VALUE = 2^25-1 -- marker numbers 0 to 24 have no pre-defined function
out:SetFont(ide.font.oNormal)
out:StyleSetFont(wxstc.wxSTC_STYLE_DEFAULT, ide.font.oNormal)
out:SetBufferedDraw(not ide.config.hidpi and true or false)
out:StyleClearAll()
out:SetTabWidth(ide.config.editor.tabwidth or 2)
out:SetIndent(ide.config.editor.tabwidth or 2)
out:SetUseTabs(ide.config.editor.usetabs and true or false)
out:SetViewWhiteSpace(ide.config.editor.whitespace and true or false)
out:SetIndentationGuides(true)
out:SetWrapMode(wxstc.wxSTC_WRAP_WORD)
out:SetWrapStartIndent(0)
out:SetWrapVisualFlagsLocation(wxstc.wxSTC_WRAPVISUALFLAGLOC_END_BY_TEXT)
out:SetWrapVisualFlags(wxstc.wxSTC_WRAPVISUALFLAG_END)
out:MarkerDefine(StylesGetMarker("prompt"))
out:MarkerDefine(StylesGetMarker("error"))
out:MarkerDefine(StylesGetMarker("output"))
out:MarkerDefine(StylesGetMarker("message"))
out:SetReadOnly(false)
SetupKeywords(out,"lua",nil,ide.config.stylesoutshell,ide.font.oNormal,ide.font.oItalic)
local function getPromptLine()
local totalLines = out:GetLineCount()
return out:MarkerPrevious(totalLines+1, PROMPT_MARKER_VALUE)
end
local function getPromptText()
local prompt = getPromptLine()
return out:GetTextRangeDyn(out:PositionFromLine(prompt), out:GetLength())
end
local function setPromptText(text)
local length = out:GetLength()
out:SetSelectionStart(length - string.len(getPromptText()))
out:SetSelectionEnd(length)
out:ClearAny()
out:AddTextDyn(text)
-- refresh the output window to force recalculation of wrapped lines;
-- otherwise a wrapped part of the last line may not be visible.
out:Update(); out:Refresh()
out:GotoPos(out:GetLength())
end
local function positionInLine(line)
return out:GetCurrentPos() - out:PositionFromLine(line)
end
local function caretOnPromptLine(disallowLeftmost, line)
local promptLine = getPromptLine()
local currentLine = line or out: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 = out:GetLineCount()
repeat -- check until we find at least some marker
nextMarker = nextMarker+1
until out:MarkerGet(nextMarker) > 0 or nextMarker > count-1
return chomp(out:GetTextRangeDyn(
out:PositionFromLine(line), out:PositionFromLine(nextMarker)))
end
function ConsoleSelectCommand(point)
local cpos = out:ScreenToClient(point or wx.wxGetMousePosition())
local position = out:PositionFromPoint(cpos)
if position == wxstc.wxSTC_INVALID_POSITION then return end
local promptline = out:MarkerPrevious(out:LineFromPosition(position), PROMPT_MARKER_VALUE)
if promptline == wxstc.wxSTC_INVALID_POSITION then return end
local nextline = out:MarkerNext(promptline+1, ANY_MARKER_VALUE)
local epos = nextline ~= wxstc.wxSTC_INVALID_POSITION and out:PositionFromLine(nextline) or out:GetLength()
out:SetSelection(out:PositionFromLine(promptline), epos)
return true
end
local currentHistory
local function getNextHistoryLine(forward, promptText)
local count = out:GetLineCount()
if currentHistory == nil then currentHistory = count end
if forward then
currentHistory = out:MarkerNext(currentHistory+1, PROMPT_MARKER_VALUE)
if currentHistory == -1 then
currentHistory = count
return ""
end
else
currentHistory = out:MarkerPrevious(currentHistory-1, PROMPT_MARKER_VALUE)
if currentHistory == -1 then
return ""
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 = out:GetLineCount()
if currentHistory == nil then currentHistory = count end
local current = currentHistory
while true do
currentHistory = out:MarkerPrevious(currentHistory-1, PROMPT_MARKER_VALUE)
if currentHistory == -1 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 shellPrint(marker, ...)
local cnt = select('#',...)
if cnt == 0 then return end -- return if nothing to print
local isPrompt = marker and (getPromptLine() > -1)
local text = ''
for i=1,cnt do
local x = select(i,...)
text = text .. tostring(x)..(i < cnt and "\t" or "")
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
-- add "\n" if it is missing
text = text:gsub("\n+$", "") .. "\n"
local lines = out:GetLineCount()
local promptLine = isPrompt and getPromptLine() or nil
local insertLineAt = isPrompt and getPromptLine() or out:GetLineCount()-1
local insertAt = isPrompt and out:PositionFromLine(getPromptLine()) or out:GetLength()
out:InsertTextDyn(insertAt, out.useraw and text or FixUTF8(text, function (s) return '\\'..string.byte(s) end))
local linesAdded = out:GetLineCount() - lines
if marker then
if promptLine then out:MarkerDelete(promptLine, PROMPT_MARKER) end
for line = insertLineAt, insertLineAt + linesAdded - 1 do
out:MarkerAdd(line, marker)
end
if promptLine then out:MarkerAdd(promptLine+linesAdded, PROMPT_MARKER) end
end
out:EmptyUndoBuffer() -- don't allow the user to undo shell text
out:GotoPos(out:GetLength())
out:EnsureVisibleEnforcePolicy(out:GetLineCount()-1)
end
DisplayShell = function (...)
shellPrint(OUTPUT_MARKER, ...)
end
DisplayShellErr = function (...)
shellPrint(ERROR_MARKER, ...)
end
DisplayShellMsg = function (...)
shellPrint(MESSAGE_MARKER, ...)
end
DisplayShellDirect = function (...)
shellPrint(nil, ...)
end
DisplayShellPrompt = function (...)
-- don't print anything; just mark the line with a prompt mark
out:MarkerAdd(out:GetLineCount()-1, PROMPT_MARKER)
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.print = DisplayShell
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 = FileTreeGetDir(), 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 ShellSupportRemote(client)
remotesend = client
local index = bottomnotebook:GetPageIndex(out)
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.").."\n"
..TR("Use Shift-Enter for multiline code.").." "
..TR("Use 'clear' to clear the shell output and the history.").."\n"
..TR("Prepend '=' to show complex values on multiple lines.").." "
..TR("Prepend '!' to force local execution."))
DisplayShellPrompt('')
end
out: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()
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 out:GetCurrentLine() > getPromptLine() then break end
-- if we are not on the caret line, move normally
if not caretOnPromptLine() then break end
local promptText = getPromptText()
setPromptText(getNextHistoryLine(false, promptText))
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 = out:GetLineCount()-1
if out:GetCurrentLine() < totalLines then break end
-- if we are not on the caret line, move normally
if not caretOnPromptLine() then break end
local promptText = getPromptText()
setPromptText(getNextHistoryLine(true, promptText))
return
elseif key == wx.WXK_TAB then
-- if we are above the prompt line, then don't move
local promptline = getPromptLine()
if out:GetCurrentLine() < promptline then return end
local promptText = getPromptText()
-- save the position in the prompt text to restore
local pos = out: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
out:GotoPos(pos)
end
return
elseif key == wx.WXK_ESCAPE then
setPromptText("")
return
elseif key == wx.WXK_BACK then
if not caretOnPromptLine(true) then return end
elseif key == wx.WXK_DELETE or key == wx.WXK_NUMPAD_DELETE then
if not caretOnPromptLine()
or out:LineFromPosition(out: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
or key == wx.WXK_LEFT or key == wx.WXK_NUMPAD_LEFT
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 out:LineFromPosition(out: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
out:Erase()
else
DisplayShellDirect('\n')
executeShellCode(promptText)
end
currentHistory = getPromptLine() -- reset history
return -- don't need to do anything else with return
else
-- move cursor to end if not already there
if not caretOnPromptLine() then
out:GotoPos(out:GetLength())
-- check if the selection starts before the prompt line and reset it
elseif out:LineFromPosition(out:GetSelectionStart()) < getPromptLine() then
out:GotoPos(out:GetLength())
out:SetSelection(out:GetSelectionEnd()+1,out:GetSelectionEnd())
end
end
break
end
event:Skip()
end)
local function inputEditable(line)
return caretOnPromptLine(false, line) and
not (out:LineFromPosition(out:GetSelectionStart()) < getPromptLine())
end
-- new 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
out: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() == out: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
out:MarkerDelete(promptLine, PROMPT_MARKER)
out:MarkerAdd(promptAt, PROMPT_MARKER)
promptAt = nil
end
end
end)
end
out:Connect(wxstc.wxEVT_STC_UPDATEUI,
function (event) 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 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.
out:Connect(wx.wxEVT_MOUSEWHEEL,
function (event)
if wx.wxGetKeyState(wx.WXK_CONTROL) then return end
event:Skip()
end)
end
displayShellIntro()
function out:Erase()
self:ClearAll()
displayShellIntro()
end
|