-- Copyright 2011-18 Paul Kulchenko, ZeroBrane LLC -- authors: Lomtik Software (J. Winwood & John Labenski) -- Luxinia Dev (Eike Decker & Christoph Kubisch) --------------------------------------------------------- local editorID = 100 -- window id to create editor pages with, incremented for new editors local notebook = ide.frame.notebook local edcfg = ide.config.editor local styles = ide.config.styles local unpack = table.unpack or unpack local q = EscapeMagic local CURRENT_LINE_MARKER = StylesGetMarker("currentline") local CURRENT_LINE_MARKER_VALUE = 2^CURRENT_LINE_MARKER local margin = { LINENUMBER = 0, MARKER = 1, FOLD = 2 } local linenumlen = 4 + 0.5 local foldtypes = { [0] = { wxstc.wxSTC_MARKNUM_FOLDEROPEN, wxstc.wxSTC_MARKNUM_FOLDER, wxstc.wxSTC_MARKNUM_FOLDERSUB, wxstc.wxSTC_MARKNUM_FOLDERTAIL, wxstc.wxSTC_MARKNUM_FOLDEREND, wxstc.wxSTC_MARKNUM_FOLDEROPENMID, wxstc.wxSTC_MARKNUM_FOLDERMIDTAIL, }, box = { wxstc.wxSTC_MARK_BOXMINUS, wxstc.wxSTC_MARK_BOXPLUS, wxstc.wxSTC_MARK_VLINE, wxstc.wxSTC_MARK_LCORNER, wxstc.wxSTC_MARK_BOXPLUSCONNECTED, wxstc.wxSTC_MARK_BOXMINUSCONNECTED, wxstc.wxSTC_MARK_TCORNER, }, circle = { wxstc.wxSTC_MARK_CIRCLEMINUS, wxstc.wxSTC_MARK_CIRCLEPLUS, wxstc.wxSTC_MARK_VLINE, wxstc.wxSTC_MARK_LCORNERCURVE, wxstc.wxSTC_MARK_CIRCLEPLUSCONNECTED, wxstc.wxSTC_MARK_CIRCLEMINUSCONNECTED, wxstc.wxSTC_MARK_TCORNERCURVE, }, plus = { wxstc.wxSTC_MARK_MINUS, wxstc.wxSTC_MARK_PLUS }, arrow = { wxstc.wxSTC_MARK_ARROWDOWN, wxstc.wxSTC_MARK_ARROW }, } -- ---------------------------------------------------------------------------- -- Update the statusbar text of the frame using the given editor. -- Only update if the text has changed. local statusTextTable = { "OVR?", "R/O?", "Cursor Pos" } local function updateStatusText(editor) local frame = ide:GetMainFrame() if not ide:IsValidCtrl(frame) then return end local texts = { "", "", "" } if frame and editor then local pos = editor:GetCurrentPos() local selected = #editor:GetSelectedText() local selections = ide.wxver >= "2.9.5" and editor:GetSelections() or 1 texts = { (editor:GetOvertype() and TR("OVR") or TR("INS")), (editor:GetReadOnly() and TR("R/O") or TR("R/W")), table.concat({ TR("Ln: %d"):format(editor:LineFromPosition(pos) + 1), TR("Col: %d"):format(editor:GetColumn(pos) + 1), selected > 0 and TR("Sel: %d/%d"):format(selected, selections) or "", }, ' ')} end if frame then for n in ipairs(texts) do if (texts[n] ~= statusTextTable[n]) then ide:SetStatus(texts[n], n) statusTextTable[n] = texts[n] end end end end local function updateBraceMatch(editor) local pos = editor:GetCurrentPos() local posp = pos > 0 and pos-1 local char = editor:GetCharAt(pos) local charp = posp and editor:GetCharAt(posp) local match = { [string.byte("<")] = true, [string.byte(">")] = true, [string.byte("(")] = true, [string.byte(")")] = true, [string.byte("{")] = true, [string.byte("}")] = true, [string.byte("[")] = true, [string.byte("]")] = true, } pos = (match[char] and pos) or (charp and match[charp] and posp) if (pos) then -- don't match brackets in markup comments local style = bit.band(editor:GetStyleAt(pos), ide.STYLEMASK) if (MarkupIsSpecial and MarkupIsSpecial(style) or editor.spec.iscomment[style]) then return end local pos2 = editor:BraceMatch(pos) if (pos2 == wxstc.wxSTC_INVALID_POSITION) then editor:BraceBadLight(pos) else editor:BraceHighlight(pos,pos2) end editor.matchon = true elseif(editor.matchon) then editor:BraceBadLight(wxstc.wxSTC_INVALID_POSITION) editor:BraceHighlight(wxstc.wxSTC_INVALID_POSITION,-1) editor.matchon = false end end -- Check if file is altered, show dialog to reload it local function isFileAlteredOnDisk(editor) if not editor then return end local doc = ide:GetDocument(editor) if doc then local filePath = doc:GetFilePath() local fileName = doc:GetFileName() local oldModTime = doc:GetFileModifiedTime() if filePath and (string.len(filePath) > 0) and oldModTime and oldModTime:IsValid() then local modTime = GetFileModTime(filePath) if modTime == nil then doc:SetFileModifiedTime(nil) wx.wxMessageBox( TR("File '%s' no longer exists."):format(fileName), ide:GetProperty("editormessage"), wx.wxOK + wx.wxCENTRE, ide.frame) elseif not editor:GetReadOnly() and modTime:IsValid() and oldModTime:IsEarlierThan(modTime) then local ret = edcfg.autoreload and (not doc:IsModified()) and wx.wxYES or wx.wxMessageBox( TR("File '%s' has been modified on disk."):format(fileName) .."\n"..TR("Do you want to reload it?"), ide:GetProperty("editormessage"), wx.wxYES_NO + wx.wxCENTRE, ide.frame) if ret ~= wx.wxYES or ReLoadFile(filePath, editor, true) then doc:SetFileModifiedTime(GetFileModTime(filePath)) end end end end end local function navigateToPosition(editor, fromPosition, toPosition, length) table.insert(editor.jumpstack, fromPosition) editor:GotoPosEnforcePolicy(toPosition) if length then editor:SetAnchor(toPosition + length) end end local function navigateBack(editor) if #editor.jumpstack == 0 then return end local pos = table.remove(editor.jumpstack) editor:GotoPosEnforcePolicy(pos) return true end function EditorAutoComplete(editor) if not (editor and editor.spec) then return end local pos = editor:GetCurrentPos() -- don't do auto-complete in comments or strings. -- the current position and the previous one have default style (0), -- so we need to check two positions back. local style = pos >= 2 and bit.band(editor:GetStyleAt(pos-2),ide.STYLEMASK) or 0 if editor.spec.iscomment[style] or editor.spec.isstring[style] or (MarkupIsAny and MarkupIsAny(style)) -- markup in comments then return end -- retrieve the current line and get a string to the current cursor position in the line local line = editor:GetCurrentLine() local linetx = editor:GetLineDyn(line) local linestart = editor:PositionFromLine(line) local localpos = pos-linestart local lt = linetx:sub(1,localpos) lt = lt:gsub("%s*(["..editor.spec.sep.."])%s*", "%1") -- strip closed brace scopes lt = lt:gsub("%b()","") lt = lt:gsub("%b{}","") lt = lt:gsub("%b[]",".0") -- remove everything that can't be auto-completed lt = lt:match("[%w_"..q(editor.spec.sep).."]*$") -- if there is nothing to auto-complete for, then don't show the list if lt:find("^["..q(editor.spec.sep).."]*$") then return end -- know now which string is to be completed local userList = CreateAutoCompList(editor, lt, pos) -- don't show if what's typed so far matches one of the options local right = linetx:sub(localpos+1,#linetx):match("^([%a_]+[%w_]*)") local left = lt:match("[%w_]*$") -- extract one word on the left (without separators) local compmatch = { left = "( ?)%f[%w_]"..left.."%f[^%w_]( ?)", leftright = "( ?)%f[%w_]"..left..(right or "").."%f[^%w_]( ?)", } -- if the multiple selection is active, then remove the match from the `userList`, -- as it's going to match a (possibly earlier) copy of the same value local selections = ide.wxver >= "2.9.5" and editor:GetSelections() or 1 if userList and selections > 1 then for _, m in pairs(compmatch) do -- replace with a space only when there are spaces on both sides userList = userList:gsub(m, function(s1, s2) return #(s1..s2) == 2 and " " or "" end) end end if userList and #userList > 0 -- don't show autocomplete if there is a full match on the list of autocomplete options and not (userList:find(compmatch.left) or userList:find(compmatch.leftright)) then editor:UserListShow(1, userList) elseif editor:AutoCompActive() then editor:AutoCompCancel() end end local ident = "([a-zA-Z_][a-zA-Z_0-9%.%:]*)" local function getValAtPosition(editor, pos) local line = editor:LineFromPosition(pos) local linetx = editor:GetLineDyn(line) local linestart = editor:PositionFromLine(line) local localpos = pos-linestart local selected = editor:GetSelectionStart() ~= editor:GetSelectionEnd() and pos >= editor:GetSelectionStart() and pos <= editor:GetSelectionEnd() -- check if we have a selected text or an identifier. -- for an identifier, check fragments on the left and on the right. -- this is to match 'io' in 'i^o.print' and 'io.print' in 'io.pr^int'. -- remove square brackets to make tbl[index].x show proper values. local start = linetx:sub(1,localpos) :gsub("%b[]", function(s) return ("."):rep(#s) end) :find(ident.."$") local right, funccall = linetx:sub(localpos+1,#linetx):match("^([a-zA-Z_0-9]*)%s*(['\"{%(]?)") local var = selected -- GetSelectedText() returns concatenated text when multiple instances -- are selected, so get the selected text based on start/end and editor:GetTextRangeDyn(editor:GetSelectionStart(), editor:GetSelectionEnd()) or (start and linetx:sub(start,localpos):gsub(":",".")..right or nil) -- since this function can be called in different contexts, we need -- to detect function call of different types: -- 1. foo.b^ar(... -- the cursor (pos) is on the function name -- 2. foo.bar(..^. -- the cursor (pos) is on the parameter list -- "var" has value for #1 and the following fragment checks for #2 -- check if the style is the right one; this is to ignore -- comments, strings, numbers (to avoid '1 = 1'), keywords, and such local goodpos = true if start and not selected then local style = bit.band(editor:GetStyleAt(linestart+start),ide.STYLEMASK) if (MarkupIsAny and MarkupIsAny(style)) -- markup in comments or editor.spec.iscomment[style] or editor.spec.isstring[style] or editor.spec.isnumber[style] or editor.spec.iskeyword[style] then goodpos = false end end local linetxtopos = linetx:sub(1,localpos) funccall = (#funccall > 0) and goodpos and var or (linetxtopos..")"):match(ident .. "%s*%b()$") or (linetxtopos.."}"):match(ident .. "%s*%b{}$") or (linetxtopos.."'"):match(ident .. "%s*'[^']*'$") or (linetxtopos..'"'):match(ident .. '%s*"[^"]*"$') or nil -- don't do anything for strings or comments or numbers if not goodpos then return nil, funccall end return var, funccall end local function formatUpToX(s) local x = math.max(20, ide.config.acandtip.width) local splitstr = "([ \t]*)(%S*)([ \t]*)(\n?)" local t = {""} for prefix, word, suffix, newline in s:gmatch(splitstr) do if #(t[#t]) + #prefix + #word > x and #t > 0 then table.insert(t, word..suffix) else t[#t] = t[#t]..prefix..word..suffix end if #newline > 0 then table.insert(t, "") end end return table.concat(t, "\n") end local function callTipFitAndShow(editor, pos, tip) local point = editor:PointFromPosition(pos) local sline = editor:LineFromPosition(pos) local height = editor:TextHeight(sline) local maxlines = math.max(1, math.floor( math.max(editor:GetSize():GetHeight()-point:GetY()-height, point:GetY())/height-1 )) -- cut the tip to not exceed the number of maxlines. -- move the position to the left if needed to fit. -- find the longest line in terms of width in pixels. local maxwidth = 0 local lines = {} for line in formatUpToX(tip):gmatch("[^\n]*\n?") do local width = editor:TextWidth(wxstc.wxSTC_STYLE_DEFAULT, line) if width > maxwidth then maxwidth = width end table.insert(lines, line) if #lines >= maxlines then lines[#lines] = lines[#lines]:gsub("%s*\n$","")..'...' break end end tip = table.concat(lines, '') local startpos = editor:PositionFromLine(sline) local afterwidth = editor:GetSize():GetWidth()-point:GetX() -- only adjust the position if the line is not wrapped, -- otherwise the popup may cover the cursor if maxwidth > afterwidth and editor:WrapCount(sline) == 1 then local charwidth = editor:TextWidth(wxstc.wxSTC_STYLE_DEFAULT, 'A') pos = math.max(startpos, pos - math.floor((maxwidth - afterwidth) / charwidth)) end editor:CallTipShow(pos, tip) end function EditorCallTip(editor, pos, x, y) -- don't show anything if the calltip/auto-complete is active; -- this may happen after typing function name, while the mouse is over -- a different function or when auto-complete is on for a parameter. if editor:CallTipActive() or editor:AutoCompActive() then return end -- don't activate if the window itself is not active (in the background) if not ide.frame:IsActive() then return end local var, funccall = editor:ValueFromPosition(pos) -- if this is a value type rather than a function/method call, then use -- full match to avoid calltip about coroutine.status for "status" vars local tip = GetTipInfo(editor, funccall or var, false, not funccall) local limit = ide.config.acandtip.maxlength local debugger = ide:GetDebugger() if debugger and debugger:IsConnected() then if var then debugger:EvalAsync(var, function(val, err) -- val is `nil` if there is any error val = val ~= nil and (var.." = "..val) or err -- convert invalid UTF8 code, as could only appear in strings val = FixUTF8(val, function(s) return '\\'..string.byte(s) end) if #val > limit then val = val:sub(1, limit-3).."..." end -- check if the mouse position is specified and the mouse has moved, -- then don't show the tooltip as it's already too late for it. if x and y then local mpos = wx.wxGetMousePosition() if mpos.x ~= x or mpos.y ~= y then return end end if PackageEventHandle("onEditorCallTip", editor, val, funccall or var, true) ~= false then callTipFitAndShow(editor, pos, val) end end, debugger:GetDataOptions({maxlevel = false})) end elseif tip then local oncalltip = PackageEventHandle("onEditorCallTip", editor, tip, funccall or var, false) -- only shorten if shown on mouse-over. Use shortcut to get full info. local showtooltip = ide.frame.menuBar:FindItem(ID.SHOWTOOLTIP) local suffix = "...\n" ..TR("Use '%s' to see full description."):format(showtooltip:GetItemLabelText()) if x and y and #tip > limit then tip = tip:sub(1, limit-#suffix):gsub("%W*%w*$","")..suffix end if oncalltip ~= false then callTipFitAndShow(editor, pos, tip) end end end function ClosePage(selection, notebook) local editor = (notebook and selection and notebook:GetPage(selection):DynamicCast("wxStyledTextCtrl") or ide:GetEditor() ) if not editor then return false end if PackageEventHandle("onEditorPreClose", editor) == false then return false end if SaveModifiedDialog(editor, true) ~= wx.wxID_CANCEL then DynamicWordsRemoveAll(editor) local debugger = ide:GetDebugger() -- check if the window with the scratchpad running is being closed if debugger and debugger.scratchpad and debugger.scratchpad.editors and debugger.scratchpad.editors[editor] then debugger:ScratchpadOff() end -- check if the debugger is running and is using the current window; -- abort the debugger if the current marker is in the window being closed if debugger and debugger:IsConnected() and (editor:MarkerNext(0, CURRENT_LINE_MARKER_VALUE) >= 0) then debugger:Stop() end -- update the editor status if the active document is being closed -- if another editor/document gets focus, it will update the status if ide:GetDocument(editor):IsActive() then updateStatusText() end -- the event needs to be triggered before the document/editor is removed, -- so there is a small chance that the notebook page will not be removed, -- despite the event already triggered PackageEventHandle("onEditorClose", editor) if not ide:RemoveDocument(editor) then return false end editor:Destroy() ide:SetTitle() -- disable full screen if the last tab in the main notebook is closed if ide:GetEditorNotebook():GetPageCount() == 0 then ide:ShowFullScreen(false) end return true end return false end -- Indicator handling for functions and local/global variables local indicator = { FNCALL = ide:GetIndicator("core.fncall"), LOCAL = ide:GetIndicator("core.varlocal"), GLOBAL = ide:GetIndicator("core.varglobal"), SELF = ide:GetIndicator("core.varself"), MASKING = ide:GetIndicator("core.varmasking"), MASKED = ide:GetIndicator("core.varmasked"), } function IndicateFunctionsOnly(editor, lines, linee) local sindic = styles.indicator if not (edcfg.showfncall and editor.spec and editor.spec.isfncall) or not (sindic and sindic.fncall and sindic.fncall.st ~= wxstc.wxSTC_INDIC_HIDDEN) then return end local lines = lines or 0 local linee = linee or editor:GetLineCount()-1 if (lines < 0) then return end local isfncall = editor.spec.isfncall local isinvalid = {} for i,v in pairs(editor.spec.iscomment) do isinvalid[i] = v end for i,v in pairs(editor.spec.iskeyword) do isinvalid[i] = v end for i,v in pairs(editor.spec.isstring) do isinvalid[i] = v end editor:SetIndicatorCurrent(indicator.FNCALL) for line=lines,linee do local tx = editor:GetLineDyn(line) local ls = editor:PositionFromLine(line) editor:IndicatorClearRange(ls, #tx) local from = 1 local off = -1 while from do tx = from==1 and tx or string.sub(tx,from) local f,t,w = isfncall(tx) if (f) then local p = ls+f+off local s = bit.band(editor:GetStyleAt(p),ide.STYLEMASK) if not isinvalid[s] then editor:IndicatorFillRange(p, #w) end off = off + t end from = t and (t+1) end end end local delayed = {} function IndicateIfNeeded() local editor = ide:GetEditor() -- do the current one first if editor and delayed[editor] then return editor:IndicateSymbols() or next(delayed) ~= nil end local ed = next(delayed) local needmore = false if ide:IsValidCtrl(ed) then needmore = ed:IndicateSymbols() elseif ed then delayed[ed] = nil end return needmore or next(delayed) ~= nil end -- find all instances of a symbol at pos -- return table with [0] as the definition position (if local) local function indicateFindInstances(editor, name, pos) local tokens = editor:GetTokenList() local instances = {{[-1] = 1}} local this for _, token in ipairs(tokens) do local op = token[1] if op == 'EndScope' then -- EndScope has "new" level, so need +1 if this and token.fpos > pos and this == token.at+1 then break end if #instances > 1 and instances[#instances][-1] == token.at+1 then table.remove(instances) end elseif token.name == name then if op == 'Id' then table.insert(instances[#instances], token.fpos) elseif op:find("^Var") then if this and this == token.at then break end -- if new Var is defined at the same level, replace the current frame; -- if not, add a new one; skip implicit definition of "self" variable. instances[#instances + (token.at > instances[#instances][-1] and 1 or 0)] = {[0] = (not token.self and token.fpos or nil), [-1] = token.at} end if token.fpos <= pos and pos <= token.fpos+#name then this = instances[#instances][-1] end end end instances[#instances][-1] = nil -- remove the current level -- only return the list if "this" instance has been found; -- this is to avoid reporting (improper) instances when checking for -- comments, strings, table fields, etc. return this and instances[#instances] or {} end local function indicateSymbols(editor, lines) if not ide.config.autoanalyzer then return end local d = delayed[editor] delayed[editor] = nil -- assume this can be finished for now -- this function can be called for an editor tab that is already closed -- when there are still some pending events for it, so handle it. if not ide:IsValidCtrl(editor) then return end -- if marksymbols is not set in the spec, nothing else to do if not (editor.spec and editor.spec.marksymbols) then return end local indic = styles.indicator or {} local pos, vars = d and d[1] or 1, d and d[2] or nil local start = lines and editor:PositionFromLine(lines)+1 or nil if d and start and pos >= start then -- ignore delayed processing as the change is earlier in the text pos, vars = 1, nil end local tokens = editor:GetTokenList() if start then -- if the range is specified local curindic = editor:GetIndicatorCurrent() editor:SetIndicatorCurrent(indicator.MASKED) for n = #tokens, 1, -1 do local token = tokens[n] -- find the last token before the range if not token.nobreak and token.name and token.fpos+#token.name < start then pos, vars = token.fpos+#token.name, token.context break end -- unmask all variables from the rest of the list if token[1] == 'Masked' then editor:IndicatorClearRange(token.fpos-1, #token.name) end -- trim the list as it will be re-generated table.remove(tokens, n) end -- Clear masked indicators from the current position to the end as these -- will be re-calculated and re-applied based on masking variables. -- This step is needed as some positions could have shifted after updates. editor:IndicatorClearRange(pos-1, editor:GetLength()-pos+1) editor:SetIndicatorCurrent(curindic) -- need to cleanup vars as they may include variables from later -- fragments (because the cut-point was arbitrary). Also need -- to clean variables in other scopes, hence getmetatable use. local vars = vars while vars do for name, var in pairs(vars) do -- remove all variables that are created later than the current pos -- skip all non-variable elements from the vars table if type(name) == 'string' then while type(var) == 'table' and var.fpos and (var.fpos > pos) do var = var.masked -- restored a masked var vars[name] = var end end end vars = getmetatable(vars) and getmetatable(vars).__index end else if pos == 1 then -- if not continuing, then trim the list tokens = editor:ResetTokenList() end end local cleared = {} for _, indic in ipairs {indicator.FNCALL, indicator.LOCAL, indicator.GLOBAL, indicator.MASKING, indicator.SELF} do cleared[indic] = pos end local function IndicateOne(indic, pos, length) editor:SetIndicatorCurrent(indic) editor:IndicatorClearRange(cleared[indic]-1, pos-cleared[indic]) editor:IndicatorFillRange(pos-1, length) cleared[indic] = pos+length end local s = ide:GetTime() local canwork = start and 0.010 or 0.100 -- use shorter interval when typing local f = editor.spec.marksymbols(editor:GetTextDyn(), pos, vars) while true do local op, name, lineinfo, vars, at, nobreak = f() if not op then break end local var = vars and vars[name] local token = {op, name=name, fpos=lineinfo, at=at, context=vars, self = (op == 'VarSelf') or nil, nobreak=nobreak} if op == 'Function' then vars['function'] = (vars['function'] or 0) + 1 end if op == 'FunctionCall' then if indic.fncall and edcfg.showfncall then IndicateOne(indicator.FNCALL, lineinfo, #name) end elseif op ~= 'VarNext' and op ~= 'VarInside' and op ~= 'Statement' and op ~= 'String' then table.insert(tokens, token) end -- indicate local/global variables if op == 'Id' and (var and indic.varlocal or not var and indic.varglobal) then IndicateOne(var and (var.self and indicator.SELF or indicator.LOCAL) or indicator.GLOBAL, lineinfo, #name) end -- indicate masked values at the same level if op == 'Var' and var and (var.masked and at == var.masked.at) then local fpos = var.masked.fpos -- indicate masked if it's not implicit self if indic.varmasked and not var.masked.self then editor:SetIndicatorCurrent(indicator.MASKED) editor:IndicatorFillRange(fpos-1, #name) table.insert(tokens, {"Masked", name=name, fpos=fpos, nobreak=nobreak}) end if indic.varmasking then IndicateOne(indicator.MASKING, lineinfo, #name) end end -- in some rare cases `nobreak` may be a number indicating a desired -- position from which to start in case of a break if lineinfo and nobreak ~= true and (op == 'Statement' or op == 'String') and ide:GetTime()-s > canwork then delayed[editor] = {tonumber(nobreak) or lineinfo, vars} break end end -- clear indicators till the end of processed fragment pos = delayed[editor] and delayed[editor][1] or editor:GetLength()+1 -- don't clear "masked" indicators as those can be set out of order (so -- last updated fragment is not always the last in terms of its position); -- these indicators should be up-to-date to the end of the code fragment. local funconly = ide.config.editor.showfncall and editor.spec.isfncall for _, indic in ipairs {indicator.FNCALL, indicator.LOCAL, indicator.GLOBAL, indicator.MASKING} do -- don't clear "funccall" indicators as those can be set based on -- IndicateFunctionsOnly processing, which is dealt with separately if indic ~= indicator.FNCALL or not funconly then IndicateOne(indic, pos, 0) end end local needmore = delayed[editor] ~= nil if ide.config.outlineinactivity then if needmore then ide.timers.outline:Stop() else ide.timers.outline:Start(ide.config.outlineinactivity*1000, wx.wxTIMER_ONE_SHOT) end end return needmore -- request more events if still need to work end -- ---------------------------------------------------------------------------- -- Create an editor local editorfont function CreateEditor(bare) local editor = ide:CreateStyledTextCtrl(notebook, editorID, wx.wxDefaultPosition, wx.wxSize(0, 0), wx.wxBORDER_NONE) editorID = editorID + 1 -- increment so they're always unique editor.matchon = false editor.assignscache = false editor.bom = false editor.updated = 0 editor.jumpstack = {} editor.ctrlcache = {} editor.tokenlist = {} editor.onidle = {} editor.usedynamicwords = true -- populate cache with Ctrl- combinations for workaround on Linux -- http://wxwidgets.10942.n7.nabble.com/Menu-shortcuts-inconsistentcy-issue-td85065.html for id, shortcut in pairs(ide.config.keymap) do if shortcut:match('%f[%w]Ctrl[-+]') then local mask = (wx.wxMOD_CONTROL + (shortcut:match('%f[%w]Alt[-+]') and wx.wxMOD_ALT or 0) + (shortcut:match('%f[%w]Shift[-+]') and wx.wxMOD_SHIFT or 0) ) local key = shortcut:match('[-+](.)$') if key then editor.ctrlcache[key:byte()..mask] = id end end end -- populate editor keymap with configured combinations for _, map in pairs(edcfg.keymap or {}) do local key, mod, cmd, osname = unpack(map) if not osname or osname == ide.osname then if cmd then editor:CmdKeyAssign(key, mod, cmd) else editor:CmdKeyClear(key, mod) end end end editor:StyleClearAll() editorfont = editorfont or ide:CreateFont(edcfg.fontsize or 10, wx.wxFONTFAMILY_MODERN, wx.wxFONTSTYLE_NORMAL, wx.wxFONTWEIGHT_NORMAL, false, edcfg.fontname or "", edcfg.fontencoding or wx.wxFONTENCODING_DEFAULT) editor:SetFont(editorfont) editor:StyleSetFont(wxstc.wxSTC_STYLE_DEFAULT, editor:GetFont()) editor:SetTabWidth(tonumber(edcfg.tabwidth) or 2) editor:SetIndent(tonumber(edcfg.tabwidth) or 2) editor:SetUseTabs(edcfg.usetabs and true or false) editor:SetIndentationGuides(tonumber(edcfg.indentguide) or (edcfg.indentguide and true or false)) editor:SetViewWhiteSpace(tonumber(edcfg.whitespace) or (edcfg.whitespace and true or false)) editor:SetEndAtLastLine(edcfg.endatlastline and true or false) if (edcfg.usewrap) then editor:SetWrapMode(edcfg.wrapmode) editor:SetWrapStartIndent(0) if ide.wxver >= "2.9.5" then if edcfg.wrapflags then editor:SetWrapVisualFlags(tonumber(edcfg.wrapflags) or wxstc.wxSTC_WRAPVISUALFLAG_NONE) end if edcfg.wrapstartindent then editor:SetWrapStartIndent(tonumber(edcfg.wrapstartindent) or 0) end if edcfg.wrapindentmode then editor:SetWrapIndentMode(tonumber(edcfg.wrapindentmode) or wxstc.wxSTC_WRAPINDENT_FIXED) end if edcfg.wrapflagslocation then editor:SetWrapVisualFlagsLocation(tonumber(edcfg.wrapflagslocation) or wxstc.wxSTC_WRAPVISUALFLAGLOC_DEFAULT) end end else editor:SetScrollWidth(100) -- set default width editor:SetScrollWidthTracking(1) -- enable width auto-adjustment end if edcfg.defaulteol == wxstc.wxSTC_EOL_CRLF or edcfg.defaulteol == wxstc.wxSTC_EOL_LF then editor:SetEOLMode(edcfg.defaulteol) -- else: keep wxStyledTextCtrl default behavior (CRLF on Windows, LF on Unix) end editor:SetCaretLineVisible(edcfg.caretline and true or false) editor:SetVisiblePolicy(wxstc.wxSTC_VISIBLE_STRICT, 3) local charwidth = editor:TextWidth(wxstc.wxSTC_STYLE_DEFAULT, "8") editor:SetMarginType(margin.LINENUMBER, wxstc.wxSTC_MARGIN_NUMBER) editor:SetMarginMask(margin.LINENUMBER, 0) editor:SetMarginWidth(margin.LINENUMBER, edcfg.linenumber and math.floor(linenumlen * charwidth) or 0) editor:SetMarginType(margin.MARKER, wxstc.wxSTC_MARGIN_SYMBOL) editor:SetMarginMask(margin.MARKER, 0xffffffff - wxstc.wxSTC_MASK_FOLDERS) editor:SetMarginSensitive(margin.MARKER, true) editor:SetMarginWidth(margin.MARKER, charwidth*1.8) editor:MarkerDefine(StylesGetMarker("currentline")) editor:MarkerDefine(StylesGetMarker("breakpoint")) editor:MarkerDefine(StylesGetMarker("bookmark")) if edcfg.fold then editor:SetMarginType(margin.FOLD, wxstc.wxSTC_MARGIN_SYMBOL) editor:SetMarginMask(margin.FOLD, wxstc.wxSTC_MASK_FOLDERS) editor:SetMarginSensitive(margin.FOLD, true) editor:SetMarginWidth(margin.FOLD, charwidth*1.8) end editor:SetFoldFlags(tonumber(edcfg.foldflags) or wxstc.wxSTC_FOLDFLAG_LINEAFTER_CONTRACTED) editor:SetBackSpaceUnIndents(edcfg.backspaceunindent and 1 or 0) if ide.wxver >= "3.0" and edcfg.showligatures then editor:SetTechnology(wxstc.wxSTC_TECHNOLOGY_DIRECTWRITE) -- Windows only end if ide.wxver >= "2.9.5" then -- allow multiple selection and multi-cursor editing if supported editor:SetMultipleSelection(1) editor:SetAdditionalCaretsBlink(1) editor:SetAdditionalSelectionTyping(1) -- allow extra ascent/descent editor:SetExtraAscent(tonumber(edcfg.extraascent) or 0) editor:SetExtraDescent(tonumber(edcfg.extradescent) or 0) -- set whitespace size editor:SetWhitespaceSize(tonumber(edcfg.whitespacesize) or 1) -- set virtual space options editor:SetVirtualSpaceOptions(tonumber(edcfg.virtualspace) or 0) end do local fg, bg = wx.wxWHITE, wx.wxColour(128, 128, 128) local foldtype = foldtypes[edcfg.foldtype] or foldtypes.box local foldmarkers = foldtypes[0] for m = 1, #foldmarkers do editor:MarkerDefine(foldmarkers[m], foldtype[m] or wxstc.wxSTC_MARK_EMPTY, fg, bg) end bg:delete() end if edcfg.calltipdelay and edcfg.calltipdelay > 0 then editor:SetMouseDwellTime(edcfg.calltipdelay) end if edcfg.edgemode ~= wxstc.wxSTC_EDGE_NONE or edcfg.edge then editor:SetEdgeMode(edcfg.edgemode ~= wxstc.wxSTC_EDGE_NONE and edcfg.edgemode or wxstc.wxSTC_EDGE_LINE) editor:SetEdgeColumn(tonumber(edcfg.edge) or 80) end editor:AutoCompSetIgnoreCase(ide.config.acandtip.ignorecase) if (ide.config.acandtip.strategy > 0) then editor:AutoCompSetAutoHide(0) editor:AutoCompStops([[ \n\t=-+():.,;*/!"'$%&~'#°^@?´`<>][|}{]]) end if ide.config.acandtip.fillups then editor:AutoCompSetFillUps(ide.config.acandtip.fillups) end if ide:IsValidProperty(editor, "SetMultiPaste") then editor:SetMultiPaste(wxstc.wxSTC_MULTIPASTE_EACH) end function editor:UseDynamicWords(val) if val == nil then return self.usedynamicwords end self.usedynamicwords = val end function editor:GetTokenList() return self.tokenlist end function editor:ResetTokenList() self.tokenlist = {}; return self.tokenlist end function editor:IndicateSymbols(...) return indicateSymbols(self, ...) end function editor:ValueFromPosition(pos) return getValAtPosition(self, pos) end function editor:MarkerGotoNext(marker) local value = 2^marker local line = editor:MarkerNext(editor:GetCurrentLine()+1, value) if line == wx.wxNOT_FOUND then line = editor:MarkerNext(0, value) end if line == wx.wxNOT_FOUND then return end editor:GotoLine(line) editor:EnsureVisibleEnforcePolicy(line) return line end function editor:MarkerGotoPrev(marker) local value = 2^marker local line = editor:MarkerPrevious(editor:GetCurrentLine()-1, value) if line == wx.wxNOT_FOUND then line = editor:MarkerPrevious(editor:GetLineCount(), value) end if line == wx.wxNOT_FOUND then return end editor:GotoLine(line) editor:EnsureVisibleEnforcePolicy(line) return line end function editor:MarkerToggle(marker, line, value) if type(marker) == "string" then marker = StylesGetMarker(marker) end assert(marker ~= nil, "Marker update requires known marker type") line = line or editor:GetCurrentLine() local isset = bit.band(editor:MarkerGet(line), 2^marker) > 0 if value ~= nil and isset == value then return end if PackageEventHandle("onEditorMarkerUpdate", editor, marker, line+1, not isset) == false then return end if isset then editor:MarkerDelete(line, marker) else editor:MarkerAdd(line, marker) end end function editor:BookmarkToggle(...) return self:MarkerToggle("bookmark", ...) end function editor:BreakpointToggle(...) return self:MarkerToggle("breakpoint", ...) end function editor:DoWhenIdle(func) table.insert(self.onidle, func) end -- GotoPos should work by itself, but it doesn't (wx 2.9.5). -- This is likely because the editor window hasn't been refreshed yet, -- so its LinesOnScreen method returns 0/-1, which skews the calculations. -- To avoid this, the caret line is made visible at the first opportunity. do local redolater function editor:GotoPosDelayed(pos) local badtime = self:LinesOnScreen() <= 0 -- -1 on OSX, 0 on Windows if pos then if badtime then redolater = pos -- without this GotoPos the content is not scrolled correctly on -- Windows, but with this it's not scrolled correctly on OSX. if ide.osname ~= 'Macintosh' then self:GotoPos(pos) end else redolater = nil self:GotoPosEnforcePolicy(pos) end elseif not badtime and redolater then -- reset the left margin first to make sure that the position -- is set "from the left" to get the best content displayed. self:SetXOffset(0) self:GotoPosEnforcePolicy(redolater) redolater = nil end end end if bare then return editor end -- bare editor doesn't have any event handlers local mclickpos editor:Connect(wx.wxEVT_LEFT_DOWN, function(event) event:Skip() mclickpos = event:GetPosition() end) editor:Connect(wx.wxEVT_LEFT_UP, function(event) event:Skip() if not mclickpos or wx.wxGetKeyState(wx.WXK_SHIFT) or wx.wxGetKeyState(wx.WXK_CONTROL) or wx.wxGetKeyState(wx.WXK_ALT) then return end local point = event:GetPosition() if mclickpos:GetX() ~= point:GetX() or mclickpos:GetY() ~= point:GetY() then return end local pos = editor:PositionFromPoint(point) local line = editor:LineFromPosition(pos) local header = bit.band(editor:GetFoldLevel(line), wxstc.wxSTC_FOLDLEVELHEADERFLAG) == wxstc.wxSTC_FOLDLEVELHEADERFLAG local from = header and line or editor:GetFoldParent(line) -- check if the click over the LINENUMBER margin if editor:MarginFromPoint(point:GetX()) == margin.LINENUMBER then -- if the next line is not visible, select the entire block to include folder lines if not editor:GetLineVisible(line+1) then local to = editor:GetLastChild(from, -1) editor:SetSelection(editor:PositionFromLine(from), editor:PositionFromLine(to+1)) editor:ToggleFold(line) end end end) editor.ev = {} editor:Connect(wxstc.wxEVT_STC_MARGINCLICK, function (event) local line = editor:LineFromPosition(event:GetPosition()) local header = bit.band(editor:GetFoldLevel(line), wxstc.wxSTC_FOLDLEVELHEADERFLAG) == wxstc.wxSTC_FOLDLEVELHEADERFLAG local from = header and line or editor:GetFoldParent(line) local marginno = event:GetMargin() if marginno == margin.MARKER then editor:BreakpointToggle(line) elseif marginno == margin.FOLD then local shift, ctrl = wx.wxGetKeyState(wx.WXK_SHIFT), wx.wxGetKeyState(wx.WXK_CONTROL) if shift and ctrl then editor:FoldSome(line) elseif ctrl then -- select the scope that was clicked on if from > -1 then -- only select if there is a block to select local to = editor:GetLastChild(from, -1) editor:SetSelection(editor:PositionFromLine(from), editor:PositionFromLine(to+1)) end elseif header or shift then editor:ToggleFold(line) end end end) editor:Connect(wxstc.wxEVT_STC_MODIFIED, function (event) if (editor.assignscache and editor:GetCurrentLine() ~= editor.assignscache.line) then editor.assignscache = false end local evtype = event:GetModificationType() if bit.band(evtype, wxstc.wxSTC_MOD_CHANGEMARKER) == 0 then -- this event is being called on OSX too frequently, so skip these notifications editor.updated = ide:GetTime() end local pos = event:GetPosition() local firstLine = editor:LineFromPosition(pos) local inserted = bit.band(evtype, wxstc.wxSTC_MOD_INSERTTEXT) ~= 0 local deleted = bit.band(evtype, wxstc.wxSTC_MOD_DELETETEXT) ~= 0 if (inserted or deleted) then SetAutoRecoveryMark() local linesChanged = inserted and event:GetLinesAdded() or 0 -- collate events if they are for the same line local events = #editor.ev if events == 0 or editor.ev[events][1] ~= firstLine then editor.ev[events+1] = {firstLine, linesChanged} elseif events > 0 and editor.ev[events][1] == firstLine then editor.ev[events][2] = math.max(editor.ev[events][2], linesChanged) end if editor.usedynamicwords then DynamicWordsAdd(editor, nil, firstLine, linesChanged) end end local beforeInserted = bit.band(evtype,wxstc.wxSTC_MOD_BEFOREINSERT) ~= 0 local beforeDeleted = bit.band(evtype,wxstc.wxSTC_MOD_BEFOREDELETE) ~= 0 if (beforeInserted or beforeDeleted) then -- unfold the current line being changed if folded, but only if one selection local lastLine = editor:LineFromPosition(pos+event:GetLength()) local selections = ide.wxver >= "2.9.5" and editor:GetSelections() or 1 if (not editor:GetFoldExpanded(firstLine) or not editor:GetLineVisible(firstLine) or not editor:GetLineVisible(lastLine)) and selections == 1 then for line = firstLine, lastLine do if not editor:GetLineVisible(line) then editor:ToggleFold(editor:GetFoldParent(line)) end end end end -- hide calltip/auto-complete after undo/redo/delete local undodelete = (wxstc.wxSTC_MOD_DELETETEXT + wxstc.wxSTC_PERFORMED_UNDO + wxstc.wxSTC_PERFORMED_REDO) if bit.band(evtype, undodelete) ~= 0 then editor:DoWhenIdle(function(editor) if editor:CallTipActive() then editor:CallTipCancel() end if editor:AutoCompActive() then editor:AutoCompCancel() end end) end if ide.config.acandtip.nodynwords then return end -- only required to track changes if beforeDeleted then local text = editor:GetTextRangeDyn(pos, pos+event:GetLength()) local _, numlines = text:gsub("\r?\n","%1") if editor.usedynamicwords then DynamicWordsRem(editor,nil,firstLine, numlines) end end if beforeInserted then if editor.usedynamicwords then DynamicWordsRem(editor,nil,firstLine, 0) end end end) editor:Connect(wxstc.wxEVT_STC_CHARADDED, function (event) local LF = string.byte("\n") -- `CHARADDED` gets `\n` code on all platforms local ch = event:GetKey() local pos = editor:GetCurrentPos() local line = editor:GetCurrentLine() local linetx = editor:GetLineDyn(line) local linestart = editor:PositionFromLine(line) local localpos = pos-linestart local linetxtopos = linetx:sub(1,localpos) if PackageEventHandle("onEditorCharAdded", editor, event) == false then -- this event has already been handled elseif (ch == LF) then -- auto-indent if (line > 0) then local indent = editor:GetLineIndentation(line - 1) local linedone = editor:GetLineDyn(line - 1) -- if the indentation is 0 and the current line is not empty, -- but the previous line is empty, then take indentation from the -- current line (instead of the previous one). This may happen when -- CR is hit at the beginning of a line (rather than at the end). if indent == 0 and not linetx:match("^[\010\013]*$") and linedone:match("^[\010\013]*$") then indent = editor:GetLineIndentation(line) end local ut = editor:GetUseTabs() local tw = ut and editor:GetTabWidth() or editor:GetIndent() local style = bit.band(editor:GetStyleAt(editor:PositionFromLine(line-1)), ide.STYLEMASK) if edcfg.smartindent -- don't apply smartindent to multi-line comments or strings and not (editor.spec.iscomment[style] or editor.spec.isstring[style] or (MarkupIsAny and MarkupIsAny(style))) and editor.spec.isdecindent and editor.spec.isincindent then local closed, blockend = editor.spec.isdecindent(linedone) local opened = editor.spec.isincindent(linedone) -- if the current block is already indented, skip reverse indenting if (line > 1) and (closed > 0 or blockend > 0) and editor:GetLineIndentation(line-2) > indent then -- adjust opened first; this is needed when use ENTER after }) if blockend == 0 then opened = opened + closed end closed, blockend = 0, 0 end editor:SetLineIndentation(line-1, indent - tw * closed) indent = indent + tw * (opened - blockend) if indent < 0 then indent = 0 end end editor:SetLineIndentation(line, indent) indent = ut and (indent / tw) or indent editor:GotoPos(editor:GetCurrentPos()+indent) end elseif ch == ("("):byte() or ch == (","):byte() then if ch == (","):byte() then -- comma requires special handling: either it's in a list of parameters -- and follows an opening bracket, or it does nothing if linetxtopos:gsub("%b()",""):find("%(") then linetxtopos = linetxtopos:gsub("%b()",""):gsub("%(.+,$", "(") else linetxtopos = nil end end local var, funccall = editor:ValueFromPosition(pos) local tip = GetTipInfo(editor, funccall or var, ide.config.acandtip.shorttip) if tip then if editor:CallTipActive() then editor:CallTipCancel() end if PackageEventHandle("onEditorCallTip", editor, tip) ~= false then editor:DoWhenIdle(function(editor) callTipFitAndShow(editor, pos, tip) end) end end elseif ide.config.autocomplete then -- code completion prompt local trigger = linetxtopos:match("["..editor.spec.sep.."%w_]+$") if trigger and (#trigger > 1 or trigger:match("["..editor.spec.sep.."]")) then editor:DoWhenIdle(function(editor) EditorAutoComplete(editor) end) end end end) editor:Connect(wxstc.wxEVT_STC_DWELLSTART, function (event) -- on Linux DWELLSTART event seems to be generated even for those -- editor windows that are not active. What's worse, when generated -- the event seems to report "old" position when retrieved using -- event:GetX and event:GetY, so instead we use wxGetMousePosition. local linux = ide.osname == 'Unix' if linux and editor ~= ide:GetEditor() then return end -- check if this editor has focus; it may not when Stack/Watch window -- is on top, but DWELL events are still triggered in this case. -- Don't want to show calltip as it is still shown when the focus -- is switched to a different application. local focus = editor:FindFocus() if focus and focus:GetId() ~= editor:GetId() then return end -- event:GetX() and event:GetY() positions don't correspond to -- the correct positions calculated using ScreenToClient (at least -- on Windows and Linux), so use what's calculated. local mpos = wx.wxGetMousePosition() local cpos = editor:ScreenToClient(mpos) local position = editor:PositionFromPointClose(cpos.x, cpos.y) if position ~= wxstc.wxSTC_INVALID_POSITION then EditorCallTip(editor, position, mpos.x, mpos.y) end event:Skip() end) editor:Connect(wxstc.wxEVT_STC_DWELLEND, function (event) if editor:CallTipActive() then editor:CallTipCancel() end event:Skip() end) editor:Connect(wx.wxEVT_KILL_FOCUS, function (event) -- on OSX clicking on scrollbar in the popup is causing the editor to lose focus, -- which causes canceling of auto-complete, which later cause crash because -- the window is destroyed in wxwidgets after already being closed. Skip on OSX. if ide.osname ~= 'Macintosh' and editor:AutoCompActive() then editor:AutoCompCancel() end PackageEventHandle("onEditorFocusLost", editor) event:Skip() end) local eol = { [wxstc.wxSTC_EOL_CRLF] = "\r\n", [wxstc.wxSTC_EOL_LF] = "\n", [wxstc.wxSTC_EOL_CR] = "\r", } local function addOneLine(editor, adj) local pos = editor:GetLineEndPosition(editor:LineFromPosition(editor:GetCurrentPos())+(adj or 0)) local added = eol[editor:GetEOLMode()] or "\n" editor:InsertTextDyn(pos, added) editor:SetCurrentPos(pos+#added) local ev = wxstc.wxStyledTextEvent(wxstc.wxEVT_STC_CHARADDED) ev:SetKey(string.byte("\n")) editor:AddPendingEvent(ev) end editor:Connect(wxstc.wxEVT_STC_USERLISTSELECTION, function (event) if PackageEventHandle("onEditorUserlistSelection", editor, event) == false then return end -- if used Shift-Enter, then skip auto complete and just do Enter. -- `lastkey` comparison can be replaced with checking `listCompletionMethod`, -- but it's not exposed in wxSTC (as of wxwidgets 3.1.1) if wx.wxGetKeyState(wx.WXK_SHIFT) and editor.lastkey == ("\r"):byte() then return addOneLine(editor) end if ide.wxver >= "2.9.5" and editor:GetSelections() > 1 then local text = event:GetText() -- capture all positions as the selection may change local positions = {} for s = 0, editor:GetSelections()-1 do table.insert(positions, editor:GetSelectionNCaret(s)) end -- process all selections from last to first table.sort(positions) local mainpos = editor:GetSelectionNCaret(editor:GetMainSelection()) editor:BeginUndoAction() for s = #positions, 1, -1 do local pos = positions[s] local startpos = editor:WordStartPosition(pos, true) editor:SetSelection(startpos, pos) editor:ReplaceSelection(text) -- if this is the main position, save new cursor position to restore if pos == mainpos then mainpos = editor:GetCurrentPos() elseif pos < mainpos then -- adjust main position as earlier changes may affect it mainpos = mainpos + #text - (pos - startpos) end end editor:EndUndoAction() editor:GotoPos(mainpos) else local pos = editor:GetCurrentPos() local startpos = editor:WordStartPosition(pos, true) local endpos = editor:WordEndPosition(pos, true) editor:SetSelection(startpos, ide.config.acandtip.droprest and endpos or pos) editor:ReplaceSelection(event:GetText()) end end) local function updateModified() local update = function() local doc = ide:GetDocument(editor) if doc then doc:SetTabText() end end -- delay update on Unix/Linux as it seems to hang the application on ArchLinux; -- execute immediately on other platforms if ide.osname == "Unix" then editor:DoWhenIdle(update) else update() end end editor:Connect(wxstc.wxEVT_STC_SAVEPOINTREACHED, updateModified) editor:Connect(wxstc.wxEVT_STC_SAVEPOINTLEFT, updateModified) -- "updateStatusText" should be called in UPDATEUI event, but it creates -- several performance problems on Windows (using wx2.9.5+) when -- brackets or backspace is used (very slow screen repaint with 0.5s delay). -- Moving it to PAINTED event creates problems on OSX (using wx2.9.5+), -- where refresh of R/W and R/O status in the status bar is delayed. -- To avoid this macOS issue and a observed crash on Linux (wx3.1.2+) the -- execution of the update is delayed till after the event is completed. -- See https://github.com/pkulchenko/wxlua/issues/5 for the related discussion. -- PAINTED is better than UPDATEUI, as it handles INS vs OVR and R/O vs R/W status. -- The underlying issue has been addressed in https://trac.wxwidgets.org/ticket/18451 editor:Connect(wxstc.wxEVT_STC_PAINTED, function (event) PackageEventHandle("onEditorPainted", editor, event) -- STC_PAINTED is called on multiple editors when they point to -- the same document; only update status for the active one local doc = ide:GetDocument(editor) if doc and doc:IsActive() then editor:DoWhenIdle(function() updateStatusText(editor) end) end if ide.osname == 'Windows' then if edcfg.usewrap ~= true and editor:AutoCompActive() then -- showing auto-complete list leaves artifacts on the screen, -- which can only be fixed by a forced refresh. -- shows with wxSTC 3.21 and both wxwidgets 2.9.5 and 3.1 editor:Update() editor:Refresh() end end -- adjust line number margin, but only if it's already shown local linecount = #tostring(editor:GetLineCount()) + 0.5 local mwidth = editor:GetMarginWidth(margin.LINENUMBER) if mwidth > 0 then local width = math.max(linecount, linenumlen) * editor:TextWidth(wxstc.wxSTC_STYLE_DEFAULT, "8") if mwidth ~= width then editor:SetMarginWidth(margin.LINENUMBER, math.floor(width)) end end end) editor.processedUpdateContent = 0 editor:Connect(wxstc.wxEVT_STC_UPDATEUI, function (event) -- some of UPDATEUI events may be triggered as the result of editor updates -- from subsequent events (like PAINTED, which happens in documentmap plugin). -- the reason for the `processed` check is that it is not possible -- to completely skip all of these updates as this causes the issue -- of markup styling becoming visible after text deletion by Backspace. -- to avoid this, we allow the first update after any updates caused -- by real changes; the rest of UPDATEUI events are skipped. -- (use direct comparison, as need to skip events that just update content) if event:GetUpdated() == wxstc.wxSTC_UPDATE_CONTENT and not next(editor.ev) then if editor.processedUpdateContent > 1 then return end else editor.processedUpdateContent = 0 end editor.processedUpdateContent = editor.processedUpdateContent + 1 PackageEventHandle("onEditorUpdateUI", editor, event) editor:GotoPosDelayed() updateBraceMatch(editor) local minupdated for _,iv in ipairs(editor.ev) do local line = iv[1] if not minupdated or line < minupdated then minupdated = line end IndicateFunctionsOnly(editor,line,line+iv[2]) end if minupdated then local ok, res = pcall(indicateSymbols, editor, minupdated) if not ok then ide:Print("Internal error: ",res,minupdated) end end editor.ev = {} -- skip checking/updating markup when cursor position or selection changes, -- as its drawing is not going to be affected by those operations if event:GetUpdated() ~= wxstc.wxSTC_UPDATE_SELECTION then local firstvisible = editor:GetFirstVisibleLine() local firstline = editor:DocLineFromVisible(firstvisible) local lastline = editor:DocLineFromVisible(firstvisible + editor:LinesOnScreen()) -- cap last line at the number of lines in the document MarkupStyle(editor, minupdated or firstline, math.min(editor:GetLineCount(),lastline)) end end) editor:Connect(wx.wxEVT_IDLE, function (event) while #editor.onidle > 0 do table.remove(editor.onidle, 1)(editor) end end) editor:Connect(wx.wxEVT_LEFT_DOWN, function (event) if MarkupHotspotClick then local position = editor:PositionFromPointClose(event:GetX(),event:GetY()) if position ~= wxstc.wxSTC_INVALID_POSITION then if MarkupHotspotClick(position, editor) then return end end end if event:ControlDown() and event:AltDown() -- ide.wxver >= "2.9.5"; fix after GetModifiers is added to wxMouseEvent in wxlua and not event:ShiftDown() and not event:MetaDown() then local point = event:GetPosition() local pos = editor:PositionFromPointClose(point.x, point.y) local value = pos ~= wxstc.wxSTC_INVALID_POSITION and editor:ValueFromPosition(pos) or nil local instances = value and indicateFindInstances(editor, value, pos+1) if instances and instances[0] then navigateToPosition(editor, pos, instances[0]-1, #value) return end end event:Skip() end) if edcfg.nomousezoom then -- disable zoom using mouse wheel as it triggers zooming when scrolling -- on OSX with kinetic scroll and then pressing CMD. editor:Connect(wx.wxEVT_MOUSEWHEEL, function (event) if wx.wxGetKeyState(wx.WXK_CONTROL) then return end event:Skip() end) end local inhandler = false editor:Connect(wx.wxEVT_SET_FOCUS, function (event) event:Skip() if inhandler or ide:IsExiting() then return end inhandler = true PackageEventHandle("onEditorFocusSet", editor) isFileAlteredOnDisk(editor) inhandler = false end) editor:Connect(wx.wxEVT_KEY_DOWN, function (event) local keycode = event:GetKeyCode() local mod = event:GetModifiers() if PackageEventHandle("onEditorKeyDown", editor, event) == false then -- this event has already been handled return elseif keycode == wx.WXK_ESCAPE then if editor:CallTipActive() or editor:AutoCompActive() then event:Skip() elseif ide.findReplace:IsShown() then ide.findReplace:Hide() elseif ide:GetMainFrame():IsFullScreen() then ide:ShowFullScreen(false) end -- Ctrl-Home and Ctrl-End don't work on OSX with 2.9.5+; fix it elseif ide.osname == 'Macintosh' and ide.wxver >= "2.9.5" and (mod == wx.wxMOD_RAW_CONTROL or mod == (wx.wxMOD_RAW_CONTROL + wx.wxMOD_SHIFT)) and (keycode == wx.WXK_HOME or keycode == wx.WXK_END) then local pos = keycode == wx.WXK_HOME and 0 or editor:GetLength() if event:ShiftDown() -- mark selection and scroll to caret then editor:SetCurrentPos(pos) editor:EnsureCaretVisible() else editor:GotoPos(pos) end elseif (keycode == wx.WXK_DELETE or keycode == wx.WXK_BACK) and (mod == wx.wxMOD_NONE) then -- Delete and Backspace behave the same way for selected text if #(editor:GetSelectedText()) > 0 then editor:ClearAny() else local pos = editor:GetCurrentPos() if keycode == wx.WXK_BACK then pos = pos - 1 if pos < 0 then return end end -- check if the modification is to one of "invisible" characters. -- if not, proceed with "normal" processing as there are other -- events that may depend on Backspace, for example, re-calculating -- auto-complete suggestions. local style = bit.band(editor:GetStyleAt(pos), ide.STYLEMASK) if not MarkupIsSpecial or not MarkupIsSpecial(style) then event:Skip() return end editor:SetTargetStart(pos) editor:SetTargetEnd(pos+1) editor:ReplaceTarget("") end elseif mod == wx.wxMOD_ALT and keycode == wx.WXK_LEFT then -- if no "jump back" is needed, then do normal processing as this -- combination can be mapped to some action if not navigateBack(editor) then event:Skip() end elseif (keycode == wx.WXK_RETURN or keycode == wx.WXK_NUMPAD_ENTER) and (mod == wx.wxMOD_CONTROL or mod == (wx.wxMOD_CONTROL + wx.wxMOD_SHIFT)) then addOneLine(editor, mod == (wx.wxMOD_CONTROL + wx.wxMOD_SHIFT) and -1 or 0) elseif ide.osname == "Unix" and ide.wxver >= "2.9.5" and editor.ctrlcache[keycode..mod] then ide.frame:AddPendingEvent(wx.wxCommandEvent( wx.wxEVT_COMMAND_MENU_SELECTED, editor.ctrlcache[keycode..mod])) else if ide.osname == 'Macintosh' and mod == wx.wxMOD_META then return -- ignore a key press if Command key is also pressed end event:Skip() end editor.lastkey = keycode end) local function selectAllInstances(instances, name, curpos) local this local idx = 0 for _, pos in pairs(instances) do pos = pos - 1 -- positions are 0-based in Scintilla if idx == 0 then -- clear selections first as there seems to be a bug (Scintilla 3.2.3) -- that doesn't reset selection after right mouse click. editor:ClearSelections() editor:SetSelection(pos, pos+#name) else editor:AddSelection(pos+#name, pos) end -- check if this is the current selection if curpos >= pos and curpos <= pos+#name then this = idx end idx = idx + 1 end if this then editor:SetMainSelection(this) end -- set the current name as the search value to make subsequence searches look for it ide.findReplace:SetFind(name) end editor:Connect(wxstc.wxEVT_STC_DOUBLECLICK, function(event) -- only activate selection of instances on Ctrl/Cmd-DoubleClick if event:GetModifiers() == wx.wxMOD_CONTROL then local pos = event:GetPosition() local value = pos ~= wxstc.wxSTC_INVALID_POSITION and editor:ValueFromPosition(pos) or nil local instances = value and indicateFindInstances(editor, value, pos+1) if instances and (instances[0] or #instances > 0) then selectAllInstances(instances, value, pos) return end end event:Skip() end) editor:Connect(wxstc.wxEVT_STC_ZOOM, function(event) -- if Shift+Zoom is used, then zoom all editors, not just the current one if wx.wxGetKeyState(wx.WXK_SHIFT) then local zoom = editor:GetZoom() for _, doc in pairs(ide:GetDocuments()) do -- check the editor zoom level to avoid recursion if doc:GetEditor():GetZoom() ~= zoom then doc:GetEditor():SetZoom(zoom) end end end event:Skip() end) if ide.osname == "Windows" then editor:DragAcceptFiles(true) editor:Connect(wx.wxEVT_DROP_FILES, function(event) local files = event:GetFiles() if not files or #files == 0 then return end -- activate all files/directories one by one for _, filename in ipairs(files) do ide:ActivateFile(filename) end end) elseif ide.osname == "Unix" then editor:Connect(wxstc.wxEVT_STC_DO_DROP, function(event) local dropped = event:GetText() -- this event may get a list of files separated by \n (and the list ends in \n as well), -- so check if what's dropped looks like this list if dropped:find("^file://.+\n$") then for filename in dropped:gmatch("file://(.-)\n") do ide:ActivateFile(filename) end event:SetDragResult(wx.wxDragCancel) -- cancel the drag to not paste the text end end) end local pos local function getPositionValues() local p = pos or editor:GetCurrentPos() local value = p ~= wxstc.wxSTC_INVALID_POSITION and editor:ValueFromPosition(p) or nil local instances = value and indicateFindInstances(editor, value, p+1) return p, value, instances end editor:Connect(wx.wxEVT_CONTEXT_MENU, function (event) local point = editor:ScreenToClient(event:GetPosition()) -- capture the position of the click to use in handlers later pos = editor:PositionFromPoint(point) if pos == 0 and (point:GetX() < 0 or point:GetY() < 0) then pos = nil end local _, _, instances = getPositionValues() local occurrences = (not instances or #instances == 0) and "" or (" (%d)"):format(#instances+(instances[0] and 1 or 0)) local line = instances and instances[0] and editor:LineFromPosition(instances[0]-1)+1 local def = line and " ("..TR("on line %d"):format(line)..")" or "" local menu = ide:MakeMenu { { ID.UNDO, TR("&Undo")..KSC(ID.UNDO) }, { ID.REDO, TR("&Redo")..KSC(ID.REDO) }, { }, { ID.CUT, TR("Cu&t")..KSC(ID.CUT) }, { ID.COPY, TR("&Copy")..KSC(ID.COPY) }, { ID.PASTE, TR("&Paste")..KSC(ID.PASTE) }, { ID.SELECTALL, TR("Select &All")..KSC(ID.SELECTALL) }, { }, { ID.GOTODEFINITION, TR("Go To Definition")..def..KSC(ID.GOTODEFINITION) }, { ID.RENAMEALLINSTANCES, TR("Rename All Instances")..occurrences..KSC(ID.RENAMEALLINSTANCES) }, { ID.REPLACEALLSELECTIONS, TR("Replace All Selections")..KSC(ID.REPLACEALLSELECTIONS) }, { }, { ID.QUICKADDWATCH, TR("Add Watch Expression")..KSC(ID.QUICKADDWATCH) }, { ID.QUICKEVAL, TR("Evaluate In Console")..KSC(ID.QUICKEVAL) }, { ID.ADDTOSCRATCHPAD, TR("Add To Scratchpad")..KSC(ID.ADDTOSCRATCHPAD) }, { ID.RUNTO, TR("Run To Cursor")..KSC(ID.RUNTO) }, } -- disable calltips that could open over the menu local dwelltime = editor:GetMouseDwellTime() editor:SetMouseDwellTime(0) -- disable dwelling -- cancel calltip if it's already shown as it interferes with popup menu if editor:CallTipActive() then editor:CallTipCancel() end PackageEventHandle("onMenuEditor", menu, editor, event) -- popup statuses are not refreshed on Linux, so do it manually if ide.osname == "Unix" then UpdateMenuUI(menu, editor) end editor:PopupMenu(menu) editor:SetMouseDwellTime(dwelltime) -- restore dwelling pos = nil -- reset the position end) editor:Connect(ID.RUNTO, wx.wxEVT_COMMAND_MENU_SELECTED, function() local pos = getPositionValues() if pos and pos ~= wxstc.wxSTC_INVALID_POSITION then ide:GetDebugger():RunTo(editor, editor:LineFromPosition(pos)+1) end end) editor:Connect(ID.GOTODEFINITION, wx.wxEVT_UPDATE_UI, function(event) local _, _, instances = getPositionValues() event:Enable(instances and instances[0]) end) editor:Connect(ID.GOTODEFINITION, wx.wxEVT_COMMAND_MENU_SELECTED, function(event) local _, value, instances = getPositionValues() if value and instances[0] then navigateToPosition(editor, editor:GetCurrentPos(), instances[0]-1, #value) end end) editor:Connect(ID.RENAMEALLINSTANCES, wx.wxEVT_UPDATE_UI, function(event) local _, _, instances = getPositionValues() event:Enable(instances and (instances[0] or #instances > 0) or editor:GetSelectionStart() ~= editor:GetSelectionEnd()) end) editor:Connect(ID.RENAMEALLINSTANCES, wx.wxEVT_COMMAND_MENU_SELECTED, function(event) local pos, value, instances = getPositionValues() if value and pos then if not (instances and (instances[0] or #instances > 0)) then -- if multiple instances (of a variable) are not detected, -- then simply find all instances of (selected) `value` instances = {} local length, pos = editor:GetLength(), 0 while true do editor:SetTargetStart(pos) editor:SetTargetEnd(length) pos = editor:SearchInTarget(value) if pos == wx.wxNOT_FOUND then break end table.insert(instances, pos+1) pos = pos + #value end end selectAllInstances(instances, value, pos) end end) editor:Connect(ID.REPLACEALLSELECTIONS, wx.wxEVT_UPDATE_UI, function(event) event:Enable((ide.wxver >= "2.9.5" and editor:GetSelections() or 1) > 1) end) editor:Connect(ID.REPLACEALLSELECTIONS, wx.wxEVT_COMMAND_MENU_SELECTED, function(event) local main = editor:GetMainSelection() local text = wx.wxGetTextFromUser( TR("Enter replacement text"), TR("Replace All Selections"), editor:GetTextRangeDyn(editor:GetSelectionNStart(main), editor:GetSelectionNEnd(main)) ) if not text or text == "" then return end editor:BeginUndoAction() for s = 0, editor:GetSelections()-1 do local selst, selend = editor:GetSelectionNStart(s), editor:GetSelectionNEnd(s) editor:SetTargetStart(selst) editor:SetTargetEnd(selend) editor:ReplaceTarget(text) editor:SetSelectionNStart(s, selst) editor:SetSelectionNEnd(s, selst+#text) end editor:EndUndoAction() editor:SetMainSelection(main) end) editor:Connect(ID.QUICKADDWATCH, wx.wxEVT_UPDATE_UI, function(event) local _, value = getPositionValues() event:Enable(value ~= nil) end) editor:Connect(ID.QUICKADDWATCH, wx.wxEVT_COMMAND_MENU_SELECTED, function(event) local _, value = getPositionValues() ide:AddWatch(value) end) editor:Connect(ID.QUICKEVAL, wx.wxEVT_UPDATE_UI, function(event) local _, value = getPositionValues() event:Enable(value ~= nil) end) editor:Connect(ID.QUICKEVAL, wx.wxEVT_COMMAND_MENU_SELECTED, function(event) local _, value = getPositionValues() ShellExecuteCode(value) end) editor:Connect(ID.ADDTOSCRATCHPAD, wx.wxEVT_UPDATE_UI, function(event) local debugger = ide:GetDebugger() event:Enable(debugger.scratchpad and debugger.scratchpad.editors and not debugger.scratchpad.editors[editor]) end) editor:Connect(ID.ADDTOSCRATCHPAD, wx.wxEVT_COMMAND_MENU_SELECTED, function(event) ide:GetDebugger():ScratchpadOn(editor) end) return editor end -- ---------------------------------------------------------------------------- -- Add an editor to the notebook function AddEditor(editor, name) assert(notebook:GetPageIndex(editor) == wx.wxNOT_FOUND, "Editor being added is not in the notebook: failed") -- set the document properties local document = ide:CreateDocument(editor, name) -- add page only after document is created as there may be handlers -- that expect the document (for example, onEditorFocusSet) if not notebook:AddPage(editor, name, true) then ide:RemoveDocument(editor) return else document:SetTabText(name) return document end end local lexlpegmap = { text = {"identifier"}, lexerdef = {"nothing"}, comment = {"comment"}, stringtxt = {"string","longstring"}, preprocessor= {"preprocessor","embedded"}, operator = {"operator"}, number = {"number"}, keywords0 = {"keyword"}, keywords1 = {"constant","variable"}, keywords2 = {"function","regex"}, keywords3 = {"library","class","type"}, } local function cleanup(paths) for _, path in ipairs(paths) do if not FileRemove(path) then wx.wxRmdir(path) end end end local function setLexLPegLexer(editor, spec) local lexername = spec.lexer local lexer = lexername:gsub("^lexlpeg%.","") local ppath = package.path local lpath = ide:GetRootPath("lualibs/lexers") package.path = MergeFullPath(lpath, "?.lua") -- update `package.path` to reference `lexers/` local ok, lex = pcall(require, "lexer") package.path = ppath -- restore the original `package.path` if not ok then return nil, "Can't load LexLPeg lexer components: "..lex end -- if the requested lexer is a dynamically registered one, then need to create a file for it, -- as LexLPeg lexers are loaded in a separate Lua state, which this process has no contol over. local dynlexer, dynfile = ide:GetLexer(lexername), nil local tmppath = MergeFullPath(wx.wxStandardPaths.Get():GetTempDir(), "lexer-"..wx.wxGetLocalTimeMillis():ToString()) if dynlexer then local ok, err = CreateFullPath(tmppath) if not ok then return nil, err end -- update lex.LEXERPATH to search there lex.LEXERPATH = MergeFullPath(tmppath, "?.lua") dynfile = MergeFullPath(tmppath, lexer..".lua") -- save the file to the temp folder ok, err = FileWrite(dynfile, dynlexer) if not ok then cleanup({tmppath}); return nil, err end end local ok, err = pcall(lex.load, lexer) if dynlexer then cleanup({dynfile, tmppath}) end if not ok then return nil, (err:gsub(".+lexer%.lua:%d+:%s*","")) end local lexmod = err local lexpath = package.searchpath("lexlpeg", ide.osclibs) if not lexpath then return nil, "Can't find LexLPeg lexer." end do local err = wx.wxSysErrorCode() local _ = wx.wxLogNull() -- disable error reporting; will report as needed local loaded = pcall(function() editor:LoadLexerLibrary(lexpath) end) if not loaded then return nil, "Can't load LexLPeg library." end -- the error code may be non-zero, but still needs to be different from the previous one -- as it may report non-zero values on Windows (for example, 1447) when no error is generated local newerr = wx.wxSysErrorCode() if newerr > 0 and newerr ~= err then return nil, wx.wxSysErrorMsg() end end if dynlexer then local ok, err = CreateFullPath(tmppath) if not ok then return nil, err end -- copy lexer.lua to the temp folder ok, err = FileCopy(MergeFullPath(lpath, "lexer.lua"), MergeFullPath(tmppath, "lexer.lua")) if not ok then return nil, err end -- save the file to the temp folder ok, err = FileWrite(dynfile, dynlexer) if not ok then FileRemove(MergeFullPath(tmppath, "lexer.lua")); return nil, err end -- update lpath to point to the temp folder lpath = tmppath end -- temporarily set the enviornment variable to load the new lua state with proper paths -- do here as the Lua state in LexLPeg parser is initialized furing `SetLexerLanguage` call local ok, cpath = wx.wxGetEnv("LUA_CPATH") if ok then wx.wxSetEnv("LUA_CPATH", ide.osclibs) end editor:SetLexerLanguage("lpeg") editor:SetProperty("lexer.lpeg.home", lpath) editor:PrivateLexerCall(wxstc.wxSTC_SETLEXERLANGUAGE, lexer) --[[ SetLexerLanguage for LexLPeg ]] if ok then wx.wxSetEnv("LUA_CPATH", cpath) end if dynlexer then cleanup({dynfile, MergeFullPath(tmppath, "lexer.lua"), tmppath}) end local styleconvert = {} for name, map in pairs(lexlpegmap) do styleconvert[name] = {} for _, stylename in ipairs(map) do if lexmod._TOKENSTYLES[stylename] then table.insert(styleconvert[name], lexmod._TOKENSTYLES[stylename]) end end end spec.lexerstyleconvert = styleconvert -- assign line comment value based on the values in the lexer comment table if spec.linecomment == nil then local comments = (lexmod._FOLDPOINTS and lexmod._FOLDPOINTS.comment) or (lexmod._foldsymbols and lexmod._foldsymbols.comment) or {} for k, v in pairs(comments) do if type(v) == 'function' then spec.linecomment = k break end end end return true end function SetupKeywords(editor, ext, forcespec, styles, font, fontitalic) local lexerstyleconvert = nil local spec = forcespec or ide:FindSpec(ext, editor:GetLine(0)) -- found a spec setup lexers and keywords if spec and ide:IsValidProperty(editor, 'spec') and editor.spec == spec and not forcespec then return end if spec then if type(spec.lexer) == "string" then local ok, err = setLexLPegLexer(editor, spec) if not ok then spec.lexerstyleconvert = {} ide:Print(("Can't load LexLPeg '%s' lexer: %s"):format(spec.lexer, err)) editor:SetLexer(wxstc.wxSTC_LEX_NULL) end UpdateSpecs(spec) else editor:SetLexer(spec.lexer or wxstc.wxSTC_LEX_NULL) end lexerstyleconvert = spec.lexerstyleconvert if (spec.keywords) then for i,words in ipairs(spec.keywords) do editor:SetKeyWords(i-1,words) end end editor.api = GetApi(spec.apitype or "none") editor.spec = spec else editor:SetLexer(wxstc.wxSTC_LEX_NULL) editor:SetKeyWords(0, "") editor.api = GetApi("none") editor.spec = ide.specs.none end -- need to set folding property after lexer is set, otherwise -- the folds are not shown (wxwidgets 2.9.5) editor:SetProperty("fold", edcfg.fold and "1" or "0") if edcfg.fold then editor:SetProperty("fold.compact", edcfg.foldcompact and "1" or "0") editor:SetProperty("fold.comment", "1") editor:SetProperty("fold.line.comments", "1") end -- quickfix to prevent weird looks, otherwise need to update styling mechanism for cpp -- cpp "greyed out" styles are `styleid + 64` editor:SetProperty("lexer.cpp.track.preprocessor", "0") editor:SetProperty("lexer.cpp.update.preprocessor", "0") StylesApplyToEditor(styles or ide.config.styles, editor, font, fontitalic, lexerstyleconvert) end