-- Copyright 2011-18 Paul Kulchenko, ZeroBrane LLC --------------------------------------------------------- local ide = ide local q = EscapeMagic local unpack = table.unpack or unpack local dc = wx.wxMemoryDC() local function getFontHeight(font) dc:SetFont(font) local _, h = dc:GetTextExtent("AZ") dc:SetFont(wx.wxNullFont) return h end local pending local function pendingInput() if ide.osname ~= 'Unix' then ide:GetApp():SafeYieldFor(wx.NULL, wx.wxEVT_CATEGORY_USER_INPUT + wx.wxEVT_CATEGORY_UI) end return pending end local showProgress local function showCommandBar(params) local onDone, onUpdate, onItem, onSelection, defaultText, selectedText = params.onDone, params.onUpdate, params.onItem, params.onSelection, params.defaultText, params.selectedText local row_width = ide.config.commandbar.width or 0 if row_width < 1 then row_width = math.max(450, math.floor(row_width * ide:GetMainFrame():GetClientSize():GetWidth())) end local maxlines = ide.config.commandbar.maxlines local lines = {} local linenow = 0 local sash = ide:GetUIManager():GetArtProvider():GetMetric(wxaui.wxAUI_DOCKART_SASH_SIZE) local border = sash + 2 local nb = ide:GetEditorNotebook() local pos = nb:GetScreenPosition() if pos then local minx, miny for p = 0, nb:GetPageCount()-1 do local sp = nb:GetPage(p):GetScreenPosition() local x, y = sp:GetX(), sp:GetY() -- just in case, compare with the position of the notebook itself; -- this is needed because the tabs that haven't been refreshed yet -- may report 0 as their screen position on Linux, which is incorrect. if y > pos:GetY() and (not miny or y < miny) then miny = y end if x > pos:GetX() and (not minx or x < minx) then minx = x end end local anchorx = pos:GetX()+nb:GetClientSize():GetWidth()-row_width-16 local cp = nb:GetCurrentPage() if cp and cp:GetScreenPosition():GetX() ~= minx then anchorx = pos:GetX()+border end pos:SetX(anchorx) pos:SetY((miny or pos:GetY())+2) else pos = wx.wxDefaultPosition end local tempctrl = ide:IsValidCtrl(ide:GetProjectTree()) and ide:GetProjectTree() or wx.wxTreeCtrl() local tfont = tempctrl:GetFont() local ffont = (ide:GetEditor() or ide:CreateBareEditor()):GetFont() ffont:SetPointSize(ffont:GetPointSize()+2) local sfont = wx.wxFont(tfont) tfont:SetPointSize(tfont:GetPointSize()+2) local hoffset = 4 local voffset = 2 local line_height = getFontHeight(ffont) local row_height = line_height + getFontHeight(sfont) + voffset * 3 -- before, after, and between local frame = wx.wxFrame(ide:GetMainFrame(), wx.wxID_ANY, "Command Bar", pos, wx.wxDefaultSize, wx.wxFRAME_NO_TASKBAR + wx.wxFRAME_FLOAT_ON_PARENT + wx.wxNO_BORDER) local panel = wx.wxPanel(frame or ide:GetMainFrame(), wx.wxID_ANY, wx.wxDefaultPosition, wx.wxDefaultSize, wx.wxFULL_REPAINT_ON_RESIZE) local search = wx.wxTextCtrl(panel, wx.wxID_ANY, "\1", wx.wxDefaultPosition, -- make the text control proportional to the font size wx.wxSize(row_width, getFontHeight(tfont) + voffset), wx.wxTE_PROCESS_ENTER + wx.wxTE_PROCESS_TAB + wx.wxNO_BORDER) local results = wx.wxScrolledWindow(panel, wx.wxID_ANY, wx.wxDefaultPosition, wx.wxSize(0, 0)) local style, styledef = ide.config.styles, StylesGetDefault() local textcolor = wx.wxColour(unpack(style.text.fg or styledef.text.fg)) local backcolor = wx.wxColour(unpack(style.text.bg or styledef.text.bg)) local selcolor = wx.wxColour(unpack(style.caretlinebg.bg or styledef.caretlinebg.bg)) local pancolor = ide:GetUIManager():GetArtProvider():GetColour(wxaui.wxAUI_DOCKART_SASH_COLOUR) local borcolor = ide:GetUIManager():GetArtProvider():GetColour(wxaui.wxAUI_DOCKART_BORDER_COLOUR) search:SetBackgroundColour(backcolor) search:SetForegroundColour(textcolor) search:SetFont(tfont) local nbrush = wx.wxBrush(backcolor, wx.wxSOLID) local sbrush = wx.wxBrush(selcolor, wx.wxSOLID) local bbrush = wx.wxBrush(pancolor, wx.wxSOLID) local lpen = wx.wxPen(borcolor, 1, wx.wxDOT) local bpen = wx.wxPen(borcolor, 1, wx.wxSOLID) local npen = wx.wxPen(backcolor, 1, wx.wxSOLID) local topSizer = wx.wxFlexGridSizer(2, 1, -border*2, 0) topSizer:SetFlexibleDirection(wx.wxVERTICAL) topSizer:AddGrowableRow(1, 1) topSizer:Add(search, wx.wxSizerFlags(0):Expand():Border(wx.wxALL, border)) topSizer:Add(results, wx.wxSizerFlags(1):Expand():Border(wx.wxALL, border)) panel:SetSizer(topSizer) topSizer:Fit(frame) -- fit the frame/panel around the controls local minheight = frame:GetClientSize():GetHeight() -- make a one-time callback; -- needed because KILL_FOCUS handler can be called after closing window local function onExit(index) -- delay destroying the frame until all the related processing is done ide:DoWhenIdle(function() if ide:IsValidCtrl(frame) then frame:Destroy() end end) onExit = function() end onDone(index and lines[index], index, search:GetValue()) end local linesnow local function onPaint(event) if not ide:IsValidCtrl(frame) then return end -- adjust the scrollbar before working with the canvas local _, starty = results:GetViewStart() -- recalculate the scrollbars if the number of lines shown has changed if #lines ~= linesnow then -- adjust the starting line when the current line is the last one if linenow > starty+maxlines then starty = starty + 1 end results:SetScrollbars(1, row_height, 1, #lines, 0, starty*row_height, false) linesnow = #lines end local dc = wx.wxMemoryDC(results) results:PrepareDC(dc) local size = results:GetVirtualSize() local w,h = size:GetWidth(),size:GetHeight() local bitmap = wx.wxBitmap(w,h) local scale = ide:GetContentScaleFactor() -- scale the bitmap before drawing if ide:IsValidProperty(bitmap, "CreateScaled") and scale > 1 then bitmap:CreateScaled(w, h, bitmap:GetDepth(), scale) end dc:SelectObject(bitmap) -- clear the background dc:SetBackground(nbrush) dc:Clear() dc:SetTextForeground(textcolor) dc:SetBrush(sbrush) for r = 1, #lines do if r == linenow then dc:SetPen(wx.wxTRANSPARENT_PEN) dc:DrawRectangle(0, row_height*(r-1), row_width, row_height+1) end dc:SetPen(lpen) dc:DrawLine(hoffset, row_height*(r-1), row_width-hoffset*2, row_height*(r-1)) local fline, sline = onItem(lines[r]) if fline then dc:SetFont(ffont) dc:DrawText(fline, hoffset, row_height*(r-1)+voffset) end if sline then dc:SetFont(sfont) dc:DrawText(sline, hoffset, row_height*(r-1)+line_height+voffset*2) end end dc:SetPen(wx.wxNullPen) dc:SetBrush(wx.wxNullBrush) dc:SelectObject(wx.wxNullBitmap) dc:delete() dc = wx.wxPaintDC(results) dc:DrawBitmap(bitmap, 0, 0, true) dc:delete() end local progress = 0 showProgress = function(newprogress) progress = newprogress if not ide:IsValidCtrl(panel) then return end panel:Refresh() panel:Update() end local function onPanelPaint(event) if not ide:IsValidCtrl(frame) then return end local dc = wx.wxBufferedPaintDC(panel) dc:SetBrush(bbrush) dc:SetPen(bpen) local psize = panel:GetClientSize() dc:DrawRectangle(0, 0, psize:GetWidth(), psize:GetHeight()) dc:DrawRectangle(sash+1, sash+1, psize:GetWidth()-2*(sash+1), psize:GetHeight()-2*(sash+1)) if progress > 0 then dc:SetBrush(nbrush) dc:SetPen(npen) dc:DrawRectangle(sash+2, 1, math.floor((row_width-4)*progress), sash) end dc:SetPen(wx.wxNullPen) dc:SetBrush(wx.wxNullBrush) dc:delete() end local linewas -- line that was reported when updated local function onTextUpdated() if ide:IsValidProperty(ide:GetApp(), "GetMainLoop") then pending = ide:GetApp():GetMainLoop():IsYielding() end if pending then return end local text = search:GetValue() lines = onUpdate(text) linenow = #text > 0 and #lines > 0 and 1 or 0 linewas = nil -- the control can disappear during onUpdate as it can be closed, so check for that if not ide:IsValidCtrl(frame) then return end local size = frame:GetClientSize() local height = minheight + row_height*math.min(maxlines,#lines) if height ~= size:GetHeight() then results:SetScrollbars(1, 1, 1, 1, 0, 0, false) size:SetHeight(height) frame:SetClientSize(size) end results:Refresh() end local function onKeyDown(event) if ide:IsValidProperty(ide:GetApp(), "GetMainLoop") and ide:GetApp():GetMainLoop():IsYielding() then event:Skip() return end local linesnow = #lines local keycode = event:GetKeyCode() if keycode == wx.WXK_RETURN then onExit(linenow) return elseif event:GetModifiers() ~= wx.wxMOD_NONE then event:Skip() return elseif keycode == wx.WXK_UP then if linesnow > 0 then linenow = linenow - 1 if linenow <= 0 then linenow = linesnow end end elseif keycode == wx.WXK_DOWN then if linesnow > 0 then linenow = linenow % linesnow + 1 end elseif keycode == wx.WXK_PAGEDOWN then if linesnow > 0 then linenow = linenow + maxlines if linenow > linesnow then linenow = linesnow end end elseif keycode == wx.WXK_PAGEUP then if linesnow > 0 then linenow = linenow - maxlines if linenow <= 0 then linenow = 1 end end elseif keycode == wx.WXK_ESCAPE then onExit(false) return else event:Skip() return end local _, starty = results:GetViewStart() if linenow < starty+1 then results:Scroll(-1, linenow-1) elseif linenow > starty+maxlines then results:Scroll(-1, linenow-maxlines) end results:Refresh() end local function onMouseLeftDown(event) local pos = event:GetPosition() local _, y = results:CalcUnscrolledPosition(pos.x, pos.y) onExit(math.floor(y / row_height)+1) end local function onIdle(event) if pending then return onTextUpdated() end if linewas == linenow then return end linewas = linenow if linenow == 0 then return end -- save the selection/insertion point as it's reset on Linux (wxwidgets 2.9.5) local ip = search:GetInsertionPoint() local f, t = search:GetSelection() -- this may set focus to a different object/tab, -- so disable the focus event and then set the focus back search:SetEvtHandlerEnabled(false) onSelection(lines[linenow], search:GetValue()) search:SetFocus() search:SetEvtHandlerEnabled(true) if ide.osname == 'Unix' then search:SetInsertionPoint(ip) search:SetSelection(f, t) end end panel:Connect(wx.wxEVT_PAINT, onPanelPaint) panel:Connect(wx.wxEVT_ERASE_BACKGROUND, function() end) panel:Connect(wx.wxEVT_IDLE, onIdle) results:Connect(wx.wxEVT_PAINT, onPaint) results:Connect(wx.wxEVT_LEFT_DOWN, onMouseLeftDown) results:Connect(wx.wxEVT_ERASE_BACKGROUND, function() end) search:Connect(wx.wxEVT_KEY_DOWN, onKeyDown) search:Connect(wx.wxEVT_COMMAND_TEXT_UPDATED, onTextUpdated) search:Connect(wx.wxEVT_COMMAND_TEXT_ENTER, function() onExit(linenow) end) -- this could be done with calling `onExit`, but on OSX KILL_FOCUS is called before -- mouse LEFT_DOWN, which closes the panel before the results are taken; -- to avoid this, `onExit` call is delayed and handled in IDLE event search:Connect(wx.wxEVT_KILL_FOCUS, function() onExit() end) frame:Show(true) frame:Update() frame:Refresh() search:SetValue((defaultText or "")..(selectedText or "")) search:SetSelection(#(defaultText or ""), -1) search:SetFocus() end local sep = "[/\\%-_ ]+" local weights = {onegram = 0.1, digram = 0.4, trigram = 0.5} local cache = {} local missing = 3 -- penalty for missing symbols (1 missing == N matching) local casemismatch = 0.9 -- score for case mismatch (%% of full match) local function score(p, v) local function ngrams(str, num, low, needcache) local key = str..(low and '\1' or '\2')..num if cache[key] then return unpack(cache[key]) end local t, l, p = {}, {}, 0 for i = 1, #str-num+1 do local pair = str:sub(i, i+num-1) p = p + (t[pair] and 0 or 1) if low and pair:find('%u') then l[pair:lower()] = casemismatch end t[pair] = 1 end if needcache then cache[key] = {t, p, l} end return t, p, l end local function overlap(pattern, value, num) local ph, ps = ngrams(pattern, num, false, true) local vh, vs, vl = ngrams(value, num, true) if ps + vs == 0 then return 0 end local is = 0 -- intersection of two sets of ngrams for k in pairs(ph) do is = is + (vh[k] or vl[k:lower()] or 0) end return is / (ps + vs) - (num == 1 and missing * (ps - is) / (ps + vs) or 0) end local key = p..'\3'..v if not cache[key] then -- ignore all whitespaces in the pattern for one-gram comparison local score = weights.onegram * overlap(p:gsub("%s+",""), v, 1) if score > 0 then -- don't bother with those that can't even score 1grams p = ' '..(p:gsub(sep, ' ')) v = ' '..(v:gsub(sep, ' ')) score = score + weights.digram * overlap(p, v, 2) score = score + weights.trigram * overlap(' '..p, ' '..v, 3) end cache[key] = 2 * 100 * score end return cache[key] end local function commandBarScoreItems(t, pattern, limit) local r, plen = {}, #(pattern:gsub("%s+","")) local maxp = 0 local num = 0 local total = #t local prefilter = ide.config.commandbar and tonumber(ide.config.commandbar.prefilter) -- anchor for 1-2 symbol patterns to speed up search local needanchor = prefilter and prefilter * 4 <= #t and plen <= 2 local pref = pattern:sub(1,4):lower() local filter = prefilter and prefilter <= #t -- expand `abc` into `a.*b.*c`, but limit the prefix to avoid penalty for `s.*s.*s.*....` -- if there are too many records to filter, then only search for substrings and (prefilter * 10 <= #t and q(pref):gsub("%s+",".") or pref:gsub("%s",""):gsub(".", function(s) return q(s)..".*" end):gsub("%.%*$","")) or nil local lastpercent = 0 for n, v in ipairs(t) do -- there was additional input while scoring, so abort to check for it local timeToCheck = n % ((prefilter or 250) * 10) == 0 if timeToCheck and pendingInput() then r = {}; break end local progress = n/total local percent = math.floor(progress * 100 + 0.5) if timeToCheck and percent ~= lastpercent then lastpercent = percent if showProgress then showProgress(progress) end end if #v >= plen then local match = filter and v:lower():find(filter) -- check if the current name needs to be prefiltered or anchored (for better performance); -- if it needs to be anchored, then anchor it at the beginning of the string or the word if not filter or (match and (not needanchor or match == 1 or v:find("^[%p%s]", match-1))) then local p = math.floor(score(pattern, v)) maxp = math.max(p, maxp) if p > 1 and p > maxp / 4 then num = num + 1 r[num] = {v, p} end end end end table.sort(r, function(a, b) return a[2] > b[2] end) -- limit the list to be displayed -- `r[limit+1] = nil` is not desired as the resulting table may be sorted incorrectly if tonumber(limit) and limit < #r then local tmp = r r = {} for i = 1, limit do r[i] = tmp[i] end end if showProgress then showProgress(0) end return r end local markername = "commandbar.background" local mac = ide.osname == 'Macintosh' local win = ide.osname == 'Windows' local special = {SYMBOL = '@', LINE = ':', METHOD = ';'} local tabsep = "\0" local function name2index(name) local p = name:find(tabsep) return p and tonumber(name:sub(p + #tabsep)) or nil end local files function ShowCommandBar(default, selected) local styles = ide.config.styles -- re-register the marker as the colors might have changed local marker = ide:AddMarker(markername, wxstc.wxSTC_MARK_BACKGROUND, styles.text.fg, styles.caretlinebg.bg) local nb = ide:GetEditorNotebook() local selection = nb:GetSelection() local maxitems = ide.config.commandbar.maxitems local preview, origline, functions, methods local function markLine(ed, toline) ed:MarkerDefine(ide:GetMarker(markername)) ed:MarkerDeleteAll(marker) ed:MarkerAdd(toline-1, marker) -- store the original line if not stored yet origline = origline or (ed:GetCurrentLine()+1) ed:EnsureVisibleEnforcePolicy(toline-1) end showCommandBar({ defaultText = default or "", selectedText = selected or "", onDone = function(t, enter, text) if not mac then nb:Freeze() end -- delete all current line markers if any; restore line position local ed = ide:GetEditor() if ed and origline then ed:MarkerDeleteAll(marker) -- only restore original line if Escape was used (enter == false) if enter == false then ed:EnsureVisibleEnforcePolicy(origline-1) end end if enter then local fline, sline, docindex = unpack(t or {}) -- jump to symbol; docindex has the position of the symbol if text and text:find(special.SYMBOL) then if sline and docindex then local index = name2index(sline) local editor = index and nb:GetPage(index):DynamicCast("wxStyledTextCtrl") if not editor then local doc = ide:FindDocument(sline) -- reload the file (including the preview to refresh its symbols in the outline) editor = LoadFile(sline, (not doc or doc:GetEditor() == preview) and preview or nil) end if editor then if preview and preview ~= editor then ide:GetDocument(preview):Close() end editor:GotoPos(docindex-1) editor:EnsureVisibleEnforcePolicy(editor:LineFromPosition(docindex-1)) ide:DoWhenIdle(function() ide:GetDocument(editor):SetActive() end) end end -- insert selected method elseif text and text:find('^%s*'..special.METHOD) then if ed then -- clean up text and insert at the current location local method = sline local isfunc = methods.desc[method][1]:find(q(method).."%s*%(") local text = method .. (isfunc and "()" or "") local pos = ed:GetCurrentPos() ed:InsertTextDyn(pos, text) ed:EnsureVisibleEnforcePolicy(ed:LineFromPosition(pos)) ed:GotoPos(pos + #method + (isfunc and 1 or 0)) if isfunc then -- show the tooltip local frame = ide:GetMainFrame() frame:SetFocus() frame:AddPendingEvent( wx.wxCommandEvent(wx.wxEVT_COMMAND_MENU_SELECTED, ID.SHOWTOOLTIP)) end end -- set line position in the (current) editor if requested elseif text and text:find(special.LINE..'(%d*)%s*$') then local toline = tonumber(text:match(special.LINE..'(%d+)')) if toline and ed then ed:GotoLine(toline-1) ed:EnsureVisibleEnforcePolicy(toline-1) ide:DoWhenIdle(function() ide:GetDocument(ed):SetActive() end) end elseif docindex then -- switch to existing document local doc = ide:GetDocumentList()[docindex] if preview and preview ~= doc:GetEditor() then ide:GetDocument(preview):Close() end -- delay switching to allow the panel to be destroyed, as it may pull the focus away ide:DoWhenIdle(function() doc:SetActive() end) -- load a new file (into preview if set) elseif sline or text then -- 1. use "text" if Ctrl/Cmd-Enter is used -- 2. otherwise use currently selected file -- 3. otherwise use "text" local file = (wx.wxGetKeyState(wx.WXK_CONTROL) and text) or sline or text local fullPath = MergeFullPath(ide:GetProject(), file) local doc = ide:FindDocument(fullPath) -- if the document is already opened (not in the preview) -- or can't be opened as a file or folder, then close the preview if doc and doc:GetEditor() ~= preview or not LoadFile(fullPath, preview or nil) and not ide:SetProject(fullPath) then if preview then ide:GetDocument(preview):Close() end end end else -- close preview if preview then ide:GetDocument(preview):Close() end -- restore original selection if canceled if nb:GetSelection() ~= selection then nb:SetSelection(selection) end end preview = nil if not mac then nb:Thaw() end -- reset file cache if it's not needed if not ide.config.commandbar.filecache then files = nil end end, onUpdate = function(text) local lines = {} local projdir = ide:GetProject() -- delete all current line markers if any -- restore the original position if search text is updated local ed = ide:GetEditor() if ed and origline then ed:MarkerDeleteAll(marker) end -- reset cached functions if no symbol search if text and not text:find(special.SYMBOL) then functions = nil if ed and origline then ed:EnsureVisibleEnforcePolicy(origline-1) end end -- reset cached methods if no method search if text and not text:find(special.METHOD) then methods = nil end if text and text:find(special.SYMBOL) then local file, symbol = text:match('^(.*)'..special.SYMBOL..'(.*)') if not functions then local nums, paths = {}, {} functions = {pos = {}, src = {}} local function populateSymbols(path, symbols) for _, func in ipairs(symbols) do table.insert(functions, func.name) nums[func.name] = (nums[func.name] or 0) + 1 local num = nums[func.name] functions.src[func.name..num] = path functions.pos[func.name..num] = func.pos end end local currentonly = #file > 0 and ed local outline = ide:GetOutline() for _, doc in pairs(currentonly and {ide:GetDocument(ed)} or ide:GetDocuments()) do local path, editor = doc:GetFilePath(), doc:GetEditor() if path then paths[path] = true end local index = doc:GetTabIndex() populateSymbols(path or doc:GetFileName()..tabsep..index, outline:GetEditorSymbols(editor)) end -- now add all other files in the project if not currentonly and ide.config.commandbar.showallsymbols then local n = 0 outline:RefreshSymbols(projdir, function(path) local symbols = outline:GetFileSymbols(path) if not paths[path] and symbols then populateSymbols(path, symbols) end if not symbols then n = n + 1 end end) if n > 0 then ide:SetStatusFor(TR("Queued %d files to index."):format(n)) end end end local nums = {} if #symbol > 0 then local topscore for _, item in ipairs(commandBarScoreItems(functions, symbol, maxitems)) do local func, score = unpack(item) topscore = topscore or score nums[func] = (nums[func] or 0) + 1 local num = nums[func] if score > topscore / 4 and score > 1 then table.insert(lines, {("%2d %s"):format(score, func), functions.src[func..num], functions.pos[func..num]}) end end else for n, name in ipairs(functions) do if n > maxitems then break end nums[name] = (nums[name] or 0) + 1 local num = nums[name] lines[n] = {name, functions.src[name..num], functions.pos[name..num]} end end elseif ed and text and text:find('^%s*'..special.METHOD) then if not methods then methods = {desc = {}} local num = 1 if ed.api and ed.api.tip and ed.api.tip.shortfinfoclass then for libname, lib in pairs(ed.api.tip.shortfinfoclass) do for method, val in pairs(lib) do local signature, desc = val:match('(.-)\n(.*)') local m = (libname > "" and libname..'.' or "")..method desc = desc and desc:gsub("\n", " ") or val methods[num] = m methods.desc[m] = {signature or m, desc} num = num + 1 end end end end local method = text:match(special.METHOD..'(.*)') if #method > 0 then local topscore for _, item in ipairs(commandBarScoreItems(methods, method, maxitems)) do local method, score = unpack(item) topscore = topscore or score if score > topscore / 4 and score > 1 then table.insert(lines, { score, method }) end end end elseif text and text:find(special.LINE..'(%d*)%s*$') then local toline = tonumber(text:match(special.LINE..'(%d+)')) if toline and ed then markLine(ed, toline) end elseif text and #text > 0 and projdir and #projdir > 0 then -- populate the list of files files = files or ide:GetFileList(projdir, true, "*", {sort = false, path = false, folder = false, skipbinary = true}) local topscore for _, item in ipairs(commandBarScoreItems(files, text, maxitems)) do local file, score = unpack(item) topscore = topscore or score if score > topscore / 4 and score > 1 then table.insert(lines, { ("%2d %s"):format(score, wx.wxFileName(file):GetFullName()), file, }) end end else for index, doc in pairs(ide:GetDocumentList()) do lines[index] = {doc:GetFileName(), doc:GetFilePath(), index} end end return lines end, onItem = function(t) if methods then local score, method = unpack(t) return ("%2d %s"):format(score, methods.desc[method][1]), methods.desc[method][2] else return unpack(t) end end, onSelection = function(t, text) local _, file, docindex = unpack(t) local pos if text and text:find(special.SYMBOL) then pos, docindex = docindex, name2index(file) elseif text and text:find(special.METHOD) then return end if file then file = MergeFullPath(ide:GetProject(), file) end -- disabling event handlers for the notebook and the editor -- to minimize changes in the UI when editors are switched -- or files in the preview are updated. nb:SetEvtHandlerEnabled(false) local doc = file and ide:FindDocument(file) if docindex or doc then local doc = docindex and ide:GetDocumentList()[docindex] or doc local index, nb = doc:GetTabIndex() local ed = nb:GetPage(index) ed:SetEvtHandlerEnabled(false) if nb:GetSelection() ~= index then nb:SetSelection(index) end ed:SetEvtHandlerEnabled(true) elseif file then -- skip binary files with unknown extensions if #ide:GetKnownExtensions(GetFileExt(file)) > 0 -- file may not be read if there is an error, so provide a default for that case or not IsBinary(FileRead(file, 2048) or "") then preview = preview or NewFile() preview:SetEvtHandlerEnabled(false) LoadFile(file, preview, true, true) preview:SetFocus() -- force refresh since the panel covers the editor on OSX/Linux -- this fixes the preview window not always redrawn on Linux if not win then preview:Update() preview:Refresh() end preview:SetEvtHandlerEnabled(true) elseif preview then ide:GetDocument(preview):Close() preview = nil end end nb:SetEvtHandlerEnabled(true) if text and text:find(special.SYMBOL) then local ed = ide:GetEditor() if ed then markLine(ed, ed:LineFromPosition(pos-1)+1) end end end, }) end ide.test.commandBarScoreItems = commandBarScoreItems local fsep = GetPathSeparator() local function relpath(path, filepath) local pathpatt = "^"..EscapeMagic(path:gsub("[\\/]$",""):gsub("[\\/]", fsep))..fsep.."?" return (filepath:gsub(pathpatt, "")) end local addremove = {} ide:AddPackage('core.commandbar', { onProjectLoad = function() cache = {} -- reset ngram cache when switching projects to conserve memory files = nil -- reset files cache when switching projects end, onFiletreeFileAdd = function(self, tree, item, filepath) if not files or tree:IsDirectory(item) then return end addremove[relpath(ide:GetProject(), filepath)] = true end, onFiletreeFileRemove = function(self, tree, item, filepath) if not files or tree:IsDirectory(item) then return end addremove[relpath(ide:GetProject(), filepath)] = false end, onFiletreeFileRefresh = function(self) if not files then return end -- to save time only keep the file cache up-to-date if it's used if ide.config.commandbar.filecache then for key, val in ipairs(files or {}) do local ar = addremove[val] -- removed file, purge it from cache if ar == false then table.remove(files, key) end -- remove from the add-remove list, so that only non-existing files are left if ar ~= nil then addremove[val] = nil end end -- go over non-existing files and add them to the cache for key in pairs(addremove) do table.insert(files, key) end end addremove = {} end, })