-- Copyright 2014-17 Paul Kulchenko, ZeroBrane LLC
|
|
local ide = ide
|
ide.outline = {
|
outlineCtrl = nil,
|
imglist = ide:CreateImageList("OUTLINE", "FILE-NORMAL", "VALUE-LCALL",
|
"VALUE-GCALL", "VALUE-ACALL", "VALUE-SCALL", "VALUE-MCALL"),
|
settings = {
|
symbols = {},
|
ignoredirs = {},
|
},
|
needsaving = false,
|
needrefresh = nil,
|
indexqueue = {[0] = {}},
|
indexpurged = false, -- flag that the index has been purged from old records; once per session
|
}
|
|
local outline = ide.outline
|
local image = { FILE = 0, LFUNCTION = 1, GFUNCTION = 2, AFUNCTION = 3,
|
SMETHOD = 4, METHOD = 5,
|
}
|
local q = EscapeMagic
|
local caches = {}
|
|
local function setData(ctrl, item, value)
|
if ide.wxver >= "2.9.5" then
|
local data = wx.wxLuaTreeItemData()
|
data:SetData(value)
|
ctrl:SetItemData(item, data)
|
end
|
end
|
|
local function resetOutlineTimer()
|
if ide.config.outlineinactivity then
|
ide.timers.outline:Start(ide.config.outlineinactivity*1000, wx.wxTIMER_ONE_SHOT)
|
end
|
end
|
|
local function resetIndexTimer(interval)
|
if ide.timers.symbolindex and ide.config.symbolindexinactivity and not ide.timers.symbolindex:IsRunning() then
|
ide.timers.symbolindex:Start(interval or ide.config.symbolindexinactivity*1000, wx.wxTIMER_ONE_SHOT)
|
end
|
end
|
|
local function outlineRefresh(editor, force)
|
if not editor then return end
|
local tokens = editor:GetTokenList()
|
local sep = editor.spec.sep
|
local varname = "([%w_][%w_"..q(sep:sub(1,1)).."]*)"
|
local funcs = {updated = ide:GetTime()}
|
local var = {}
|
local outcfg = ide.config.outline or {}
|
local scopes = {}
|
local funcnum = 0
|
local SCOPENUM, FUNCNUM = 1, 2
|
local text
|
for _, token in ipairs(tokens) do
|
local op = token[1]
|
if op == 'Var' or op == 'Id' then
|
var = {name = token.name, fpos = token.fpos, global = token.context[token.name] == nil}
|
elseif outcfg.showcurrentfunction and op == 'Scope' then
|
local fundepth = #scopes
|
if token.name == '(' then -- a function starts a new scope
|
funcnum = funcnum + 1 -- increment function count
|
local nested = fundepth == 0 or scopes[fundepth][SCOPENUM] > 0
|
scopes[fundepth + (nested and 1 or 0)] = {1, funcnum}
|
elseif fundepth > 0 then
|
scopes[fundepth][SCOPENUM] = scopes[fundepth][SCOPENUM] + 1
|
end
|
elseif outcfg.showcurrentfunction and op == 'EndScope' then
|
local fundepth = #scopes
|
if fundepth > 0 and scopes[fundepth][SCOPENUM] > 0 then
|
scopes[fundepth][SCOPENUM] = scopes[fundepth][SCOPENUM] - 1
|
if scopes[fundepth][SCOPENUM] == 0 then
|
local funcnum = scopes[fundepth][FUNCNUM]
|
if funcs[funcnum] then
|
funcs[funcnum].poe = token.fpos + (token.name and #token.name or 0)
|
end
|
table.remove(scopes)
|
end
|
end
|
elseif op == 'Function' then
|
local depth = token.context['function'] or 1
|
local name, pos = token.name, token.fpos
|
text = text or editor:GetTextDyn()
|
local _, _, rname, params = text:find('([^(]*)(%b())', pos)
|
if rname then rname = rname:gsub("%s+$","") end
|
-- if something else got captured, then don't show any parameters
|
if name and rname and name ~= rname then params = "" end
|
if not name then
|
local s = editor:PositionFromLine(editor:LineFromPosition(pos-1))
|
local rest
|
rest, pos, name = text:sub(s+1, pos-1):match('%s*(.-)()'..varname..'%s*=%s*function%s*$')
|
if rest then
|
pos = s + pos
|
-- guard against "foo, bar = function() end" as it would get "bar"
|
if #rest>0 and rest:find(',') then name = nil end
|
end
|
end
|
local ftype = image.LFUNCTION
|
if not name then
|
ftype = image.AFUNCTION
|
elseif outcfg.showmethodindicator and name:find('['..q(sep)..']') then
|
ftype = name:find(q(sep:sub(1,1))) and image.SMETHOD or image.METHOD
|
elseif var.name == name and var.fpos == pos
|
or var.name and name:find('^'..var.name..'['..q(sep)..']') then
|
ftype = var.global and image.GFUNCTION or image.LFUNCTION
|
end
|
name = name or outcfg.showanonymous
|
funcs[#funcs+1] = {
|
name = ((name or '~')..(params or "")):gsub("%s+", " "),
|
skip = (not name) and true or nil,
|
depth = depth,
|
image = ftype,
|
pos = name and pos or token.fpos,
|
}
|
end
|
end
|
|
if force == nil then return funcs end
|
|
local ctrl = outline.outlineCtrl
|
if not ctrl then return end -- outline can be completely removed/disabled
|
|
local cache = caches[editor] or {}
|
caches[editor] = cache
|
|
-- add file
|
local filename = ide:GetDocument(editor):GetTabText()
|
local fileitem = cache.fileitem
|
if not fileitem or not fileitem:IsOk() then
|
local root = ctrl:GetRootItem()
|
if not root or not root:IsOk() then return end
|
|
if outcfg.showonefile then
|
fileitem = root
|
else
|
fileitem = ctrl:AppendItem(root, filename, image.FILE)
|
setData(ctrl, fileitem, editor)
|
ctrl:SetItemBold(fileitem, true)
|
ctrl:SortChildren(root)
|
end
|
cache.fileitem = fileitem
|
end
|
|
do -- check if any changes in the cached function list
|
local prevfuncs = cache.funcs or {}
|
local nochange = #funcs == #prevfuncs
|
local resort = {} -- items that need to be re-sorted
|
if nochange then
|
for n, func in ipairs(funcs) do
|
func.item = prevfuncs[n].item -- carry over cached items
|
if func.depth ~= prevfuncs[n].depth then
|
nochange = false
|
elseif nochange and prevfuncs[n].item then
|
if func.name ~= prevfuncs[n].name then
|
ctrl:SetItemText(prevfuncs[n].item, func.name)
|
if outcfg.sort then resort[ctrl:GetItemParent(prevfuncs[n].item)] = true end
|
end
|
if func.image ~= prevfuncs[n].image then
|
ctrl:SetItemImage(prevfuncs[n].item, func.image)
|
end
|
end
|
end
|
end
|
cache.funcs = funcs -- set new cache as positions may change
|
if nochange and not force then -- return if no visible changes
|
if outcfg.sort then -- resort items for all parents that have been modified
|
for item in pairs(resort) do ctrl:SortChildren(item) end
|
end
|
return
|
end
|
end
|
|
-- refresh the tree
|
-- refreshing shouldn't change the focus of the current element,
|
-- but it appears that DeleteChildren (wxwidgets 2.9.5 on Windows)
|
-- moves the focus from the current element to wxTreeCtrl.
|
-- need to save the window having focus and restore after the refresh.
|
local win = ide:GetMainFrame():FindFocus()
|
|
ctrl:Freeze()
|
|
-- disabling event handlers is not strictly necessary, but it's expected
|
-- to fix a crash on Windows that had DeleteChildren in the trace (#442).
|
ctrl:SetEvtHandlerEnabled(false)
|
ctrl:DeleteChildren(fileitem)
|
ctrl:SetEvtHandlerEnabled(true)
|
|
local edpos = editor:GetCurrentPos()+1
|
local stack = {fileitem}
|
local resort = {} -- items that need to be re-sorted
|
for n, func in ipairs(funcs) do
|
local depth = outcfg.showflat and 1 or func.depth
|
local parent = stack[depth]
|
while not parent do depth = depth - 1; parent = stack[depth] end
|
if not func.skip then
|
local item = ctrl:AppendItem(parent, func.name, func.image)
|
if ide.config.outline.showcurrentfunction
|
and edpos >= func.pos and func.poe and edpos <= func.poe then
|
ctrl:SetItemBold(item, true)
|
end
|
if outcfg.sort then resort[parent] = true end
|
setData(ctrl, item, n)
|
func.item = item
|
stack[func.depth+1] = item
|
end
|
func.skip = nil
|
end
|
if outcfg.sort then -- resort items for all parents that have been modified
|
for item in pairs(resort) do ctrl:SortChildren(item) end
|
end
|
if outcfg.showcompact then ctrl:Expand(fileitem) else ctrl:ExpandAllChildren(fileitem) end
|
|
-- scroll to the fileitem, but only if it's not a root item (as it's hidden)
|
if fileitem:GetValue() ~= ctrl:GetRootItem():GetValue() then
|
ctrl:ScrollTo(fileitem)
|
ctrl:SetScrollPos(wx.wxHORIZONTAL, 0, true)
|
else -- otherwise, scroll to the top
|
ctrl:SetScrollPos(wx.wxVERTICAL, 0, true)
|
end
|
ctrl:Thaw()
|
|
if win and win ~= ide:GetMainFrame():FindFocus() then win:SetFocus() end
|
end
|
|
local failures = {}
|
local function indexFromQueue()
|
if #outline.indexqueue == 0 then return end
|
|
local editor = ide:GetEditor()
|
local inactivity = ide.config.symbolindexinactivity
|
if editor and inactivity and editor:GetModifiedTime() > ide:GetTime()-inactivity then
|
-- reschedule timer for later time
|
resetIndexTimer()
|
else
|
local fname = table.remove(outline.indexqueue, 1)
|
outline.indexqueue[0][fname] = nil
|
-- check if fname is already loaded
|
ide:SetStatusFor(TR("Indexing %d files: '%s'..."):format(#outline.indexqueue+1, fname))
|
local content, err = FileRead(fname)
|
if content then
|
local editor = ide:CreateBareEditor()
|
editor:SetupKeywords(GetFileExt(fname))
|
editor:SetTextDyn(content)
|
editor:Colourise(0, -1)
|
editor:ResetTokenList()
|
while editor:IndicateSymbols() do end
|
|
outline:UpdateSymbols(fname, outlineRefresh(editor))
|
editor:Destroy()
|
elseif not failures[fname] then
|
ide:Print(TR("Can't open file '%s': %s"):format(fname, err))
|
failures[fname] = true
|
end
|
if #outline.indexqueue == 0 then
|
outline:SaveSettings()
|
ide:SetStatusFor(TR("Indexing completed."))
|
end
|
ide:DoWhenIdle(indexFromQueue)
|
end
|
return
|
end
|
|
local function createOutlineWindow()
|
local width, height = 360, 200
|
local ctrl = ide:CreateTreeCtrl(ide.frame, wx.wxID_ANY,
|
wx.wxDefaultPosition, wx.wxSize(width, height),
|
wx.wxTR_LINES_AT_ROOT + wx.wxTR_HAS_BUTTONS
|
+ wx.wxTR_HIDE_ROOT + wx.wxNO_BORDER)
|
|
outline.outlineCtrl = ctrl
|
ide.timers.outline = ide:AddTimer(ctrl, function() outlineRefresh(ide:GetEditor(), false) end)
|
ide.timers.symbolindex = ide:AddTimer(ctrl, function() ide:DoWhenIdle(indexFromQueue) end)
|
|
ctrl:AddRoot("Outline")
|
ctrl:SetImageList(outline.imglist)
|
ctrl:SetFont(ide.font.tree)
|
|
function ctrl:ActivateItem(item_id)
|
local data = ctrl:GetItemData(item_id)
|
if ctrl:GetItemImage(item_id) == image.FILE then
|
-- activate editor tab
|
local editor = data:GetData()
|
if not ide:GetEditorWithFocus(editor) then ide:GetDocument(editor):SetActive() end
|
else
|
-- activate tab and move cursor based on stored pos
|
-- get file parent
|
local onefile = (ide.config.outline or {}).showonefile
|
local parent = ctrl:GetItemParent(item_id)
|
if not onefile then -- find the proper parent
|
while parent:IsOk() and ctrl:GetItemImage(parent) ~= image.FILE do
|
parent = ctrl:GetItemParent(parent)
|
end
|
if not parent:IsOk() then return end
|
end
|
-- activate editor tab
|
local editor = onefile and ide:GetEditor() or ctrl:GetItemData(parent):GetData()
|
local cache = caches[editor]
|
if editor and cache then
|
-- move to position in the file
|
editor:GotoPosEnforcePolicy(cache.funcs[data:GetData()].pos-1)
|
-- only set editor active after positioning as this may change focus,
|
-- which may regenerate the outline, which may invalidate `data` value
|
if not ide:GetEditorWithFocus(editor) then ide:GetDocument(editor):SetActive() end
|
end
|
end
|
end
|
|
local function activateByPosition(event)
|
local mask = (wx.wxTREE_HITTEST_ONITEMINDENT + wx.wxTREE_HITTEST_ONITEMLABEL
|
+ wx.wxTREE_HITTEST_ONITEMICON + wx.wxTREE_HITTEST_ONITEMRIGHT)
|
local item_id, flags = ctrl:HitTest(event:GetPosition())
|
|
if item_id and item_id:IsOk() and bit.band(flags, mask) > 0 then
|
ctrl:ActivateItem(item_id)
|
else
|
event:Skip()
|
end
|
return true
|
end
|
|
if (ide.config.outline or {}).activateonclick then
|
ctrl:Connect(wx.wxEVT_LEFT_DOWN, activateByPosition)
|
end
|
ctrl:Connect(wx.wxEVT_LEFT_DCLICK, activateByPosition)
|
ctrl:Connect(wx.wxEVT_COMMAND_TREE_ITEM_ACTIVATED, function(event)
|
ctrl:ActivateItem(event:GetItem())
|
end)
|
|
ctrl:Connect(ID.OUTLINESORT, wx.wxEVT_COMMAND_MENU_SELECTED,
|
function()
|
ide.config.outline.sort = not ide.config.outline.sort
|
local ed = ide:GetEditor()
|
if not ed then return end
|
-- when showing one file only refresh outline for the current editor
|
for editor, cache in pairs((ide.config.outline or {}).showonefile and {[ed] = caches[ed]} or caches) do
|
ide:SetStatus(("Refreshing '%s'..."):format(ide:GetDocument(editor):GetFileName()))
|
local isexpanded = ctrl:IsExpanded(cache.fileitem)
|
outlineRefresh(editor, true)
|
if not isexpanded then ctrl:Collapse(cache.fileitem) end
|
end
|
ide:SetStatus('')
|
end)
|
|
ctrl:Connect(wx.wxEVT_COMMAND_TREE_ITEM_MENU,
|
function (event)
|
local menu = ide:MakeMenu {
|
{ ID.OUTLINESORT, TR("Sort By Name"), "", wx.wxITEM_CHECK },
|
}
|
menu:Check(ID.OUTLINESORT, ide.config.outline.sort)
|
|
PackageEventHandle("onMenuOutline", menu, ctrl, event)
|
|
ctrl:PopupMenu(menu)
|
end)
|
|
|
local function reconfigure(pane)
|
pane:TopDockable(false):BottomDockable(false)
|
:MinSize(150,-1):BestSize(300,-1):FloatingSize(200,300)
|
end
|
|
local layout = ide:GetSetting("/view", "uimgrlayout")
|
if not layout or not layout:find("outlinepanel") then
|
ide:AddPanelDocked(ide:GetProjectNotebook(), ctrl, "outlinepanel", TR("Outline"), reconfigure, false)
|
else
|
ide:AddPanel(ctrl, "outlinepanel", TR("Outline"), reconfigure)
|
end
|
end
|
|
local function eachNode(eachFunc, root, recursive)
|
local ctrl = outline.outlineCtrl
|
if not ctrl then return end
|
root = root or ctrl:GetRootItem()
|
if not (root and root:IsOk()) then return end
|
local item = ctrl:GetFirstChild(root)
|
while true do
|
if not item:IsOk() then break end
|
if eachFunc and eachFunc(ctrl, item) then break end
|
if recursive and ctrl:ItemHasChildren(item) then eachNode(eachFunc, item, recursive) end
|
item = ctrl:GetNextSibling(item)
|
end
|
end
|
|
local pathsep = GetPathSeparator()
|
local function isInSubDir(name, path)
|
return #name > #path and path..pathsep == name:sub(1, #path+#pathsep)
|
end
|
|
local function isIgnoredInIndex(name)
|
local ignoredirs = outline.settings.ignoredirs
|
if ignoredirs[name] then return true end
|
|
-- check through ignored dirs to see if any of them match the file;
|
-- skip those that are outside of the current project tree to allow
|
-- scanning of the projects that may be inside ignored directories.
|
local proj = ide:GetProject() -- `nil` when not set
|
for path in pairs(ignoredirs) do
|
if (not proj or isInSubDir(path, proj)) and isInSubDir(name, path) then return true end
|
end
|
|
return false
|
end
|
|
local function purgeIndex(path)
|
local symbols = outline.settings.symbols
|
for name in pairs(symbols) do
|
if isInSubDir(name, path) then outline:UpdateSymbols(name, nil) end
|
end
|
end
|
|
local function purgeQueue(path)
|
local curqueue = outline.indexqueue
|
local newqueue = {[0] = {}}
|
for _, name in ipairs(curqueue) do
|
if not isInSubDir(name, path) then
|
table.insert(newqueue, name)
|
newqueue[0][name] = true
|
end
|
end
|
outline.indexqueue = newqueue
|
end
|
|
local function disableIndex(path)
|
outline.settings.ignoredirs[path] = true
|
outline:SaveSettings(true)
|
|
-- purge the path from the index and the (current) queue
|
purgeIndex(path)
|
purgeQueue(path)
|
end
|
|
local function enableIndex(path)
|
outline.settings.ignoredirs[path] = nil
|
outline:SaveSettings(true)
|
outline:RefreshSymbols(path)
|
end
|
|
local lastfocus
|
local package = ide:AddPackage('core.outline', {
|
onRegister = function(self)
|
if not ide.config.outlineinactivity then return end
|
|
createOutlineWindow()
|
end,
|
|
-- remove the editor from the list
|
onEditorClose = function(self, editor)
|
local cache = caches[editor]
|
local fileitem = cache and cache.fileitem
|
caches[editor] = nil -- remove from cache
|
|
if fileitem and fileitem:IsOk() then
|
local ctrl = outline.outlineCtrl
|
if (ide.config.outline or {}).showonefile then
|
ctrl:DeleteChildren(fileitem)
|
else
|
ctrl:Delete(fileitem)
|
end
|
end
|
end,
|
|
-- handle rename of the file in the current editor
|
onEditorSave = function(self, editor)
|
if (ide.config.outline or {}).showonefile then return end
|
local cache = caches[editor]
|
local fileitem = cache and cache.fileitem
|
local doc = ide:GetDocument(editor)
|
local ctrl = outline.outlineCtrl
|
if doc and fileitem and ctrl:GetItemText(fileitem) ~= doc:GetTabText() then
|
ctrl:SetItemText(fileitem, doc:GetTabText())
|
end
|
local path = doc and doc:GetFilePath()
|
if path and cache and cache.funcs then
|
outline:UpdateSymbols(path, cache.funcs.updated > editor:GetModifiedTime() and cache.funcs or nil)
|
outline:SaveSettings()
|
end
|
end,
|
|
-- go over the file items to turn bold on/off or collapse/expand
|
onEditorFocusSet = function(self, editor)
|
local cache = caches[editor]
|
|
-- if the editor is not in the cache, which may happen if the user
|
-- quickly switches between tabs that don't have outline generated,
|
-- regenerate it manually
|
if not cache then resetOutlineTimer() end
|
resetIndexTimer()
|
|
if (ide.config.outline or {}).showonefile and ide.config.outlineinactivity then
|
-- this needs to be done when editor gets focus, but during active auto-complete
|
-- the focus shifts between the editor and the popup after each character;
|
-- the refresh is not necessary in this case, so only refresh when the editor changes
|
if not lastfocus or editor:GetId() ~= lastfocus then
|
outlineRefresh(editor, true)
|
lastfocus = editor:GetId()
|
end
|
return
|
end
|
|
local fileitem = cache and cache.fileitem
|
local ctrl = outline.outlineCtrl
|
local itemname = ide:GetDocument(editor):GetTabText()
|
|
-- update file name if it changed in the editor
|
if fileitem and ctrl:GetItemText(fileitem) ~= itemname then
|
ctrl:SetItemText(fileitem, itemname)
|
end
|
|
eachNode(function(ctrl, item)
|
local found = fileitem and item:GetValue() == fileitem:GetValue()
|
if not found and ctrl:IsBold(item) then
|
ctrl:SetItemBold(item, false)
|
ctrl:CollapseAllChildren(item)
|
end
|
end)
|
|
if fileitem and not ctrl:IsBold(fileitem) then
|
-- run the following changes on idle as doing them inline is causing a strange
|
-- issue on OSX when clicking on a tab may skip several tabs (#546);
|
-- this is somehow caused by `ExpandAllChildren` triggered from `SetFocus` inside
|
-- `PAGE_CHANGED` handler for the notebook.
|
ide:DoWhenIdle(function()
|
-- check if this editor is still in the cache,
|
-- as it may be closed before this handler is executed
|
if not caches[editor] then return end
|
ctrl:SetItemBold(fileitem, true)
|
if (ide.config.outline or {}).showcompact then
|
ctrl:Expand(fileitem)
|
else
|
ctrl:ExpandAllChildren(fileitem)
|
end
|
ctrl:ScrollTo(fileitem)
|
ctrl:SetScrollPos(wx.wxHORIZONTAL, 0, true)
|
end)
|
end
|
end,
|
|
onMenuFiletree = function(self, menu, tree, event)
|
local item_id = event:GetItem()
|
local name = tree:GetItemFullName(item_id)
|
local symboldirmenu = ide:MakeMenu {
|
{ID.SYMBOLDIRREFRESH, TR("Refresh Index"), TR("Refresh indexed symbols from files in the selected directory")},
|
{ID.SYMBOLDIRDISABLE, TR("Disable Indexing For '%s'"):format(name), TR("Ignore and don't index symbols from files in the selected directory")},
|
}
|
local _, _, projdirpos = ide:FindMenuItem(ID.PROJECTDIR, menu)
|
if projdirpos then
|
local ignored = isIgnoredInIndex(name)
|
local enabledirmenu = ide:MakeMenu {}
|
local paths = {}
|
for path in pairs(outline.settings.ignoredirs) do table.insert(paths, path) end
|
table.sort(paths)
|
for i, path in ipairs(paths) do
|
local id = ID("file.enablesymboldir."..i)
|
enabledirmenu:Append(id, path, "")
|
tree:Connect(id, wx.wxEVT_COMMAND_MENU_SELECTED, function() enableIndex(path) end)
|
end
|
|
symboldirmenu:Append(wx.wxMenuItem(symboldirmenu, ID.SYMBOLDIRENABLE,
|
TR("Enable Indexing"), "", wx.wxITEM_NORMAL, enabledirmenu))
|
menu:Insert(projdirpos+1, wx.wxMenuItem(menu, ID.SYMBOLDIRINDEX,
|
TR("Symbol Index"), "", wx.wxITEM_NORMAL, symboldirmenu))
|
|
-- disable "enable" if it's empty
|
menu:Enable(ID.SYMBOLDIRENABLE, #paths > 0)
|
-- disable "refresh" and "disable" if the directory is ignored
|
-- or if any of the directories above it are ignored
|
menu:Enable(ID.SYMBOLDIRREFRESH, tree:IsDirectory(item_id) and not ignored)
|
menu:Enable(ID.SYMBOLDIRDISABLE, tree:IsDirectory(item_id) and not ignored)
|
|
tree:Connect(ID.SYMBOLDIRREFRESH, wx.wxEVT_COMMAND_MENU_SELECTED, function()
|
-- purge files in this directory as some might have been removed;
|
-- files will be purged based on time, but this is a good time to clean.
|
purgeIndex(name)
|
outline:RefreshSymbols(name)
|
resetIndexTimer(1) -- start after 1ms
|
end)
|
tree:Connect(ID.SYMBOLDIRDISABLE, wx.wxEVT_COMMAND_MENU_SELECTED, function()
|
disableIndex(name)
|
end)
|
end
|
end,
|
|
onEditorUpdateUI = function(self, editor, event)
|
-- only update when content or selection changes; ignore scrolling events
|
if bit.band(event:GetUpdated(), wxstc.wxSTC_UPDATE_CONTENT + wxstc.wxSTC_UPDATE_SELECTION) > 0 then
|
ide.outline.needrefresh = editor
|
end
|
end,
|
|
onIdle = function(self)
|
local editor = ide.outline.needrefresh
|
if not editor then return end
|
|
ide.outline.needrefresh = nil
|
|
local ctrl = ide.outline.outlineCtrl
|
if not ide:IsWindowShown(ctrl) then return end
|
|
local cache = ide:IsValidCtrl(editor) and caches[editor]
|
if not cache or not ide.config.outline.showcurrentfunction then return end
|
|
local edpos = editor:GetCurrentPos()+1
|
local edline = editor:LineFromPosition(edpos-1)+1
|
if cache.pos and cache.pos == edpos then return end
|
if cache.line and cache.line == edline then return end
|
|
cache.pos = edpos
|
cache.line = edline
|
|
local n = 0
|
local MIN, MAX = 1, 2
|
local visible = {[MIN] = math.huge, [MAX] = 0}
|
local needshown = {[MIN] = math.huge, [MAX] = 0}
|
|
ctrl:Unselect()
|
-- scan all items recursively starting from the current file
|
eachNode(function(ctrl, item)
|
local func = cache.funcs[ctrl:GetItemData(item):GetData()]
|
if not func then return end
|
local val = edpos >= func.pos and func.poe and edpos <= func.poe
|
if edline == editor:LineFromPosition(func.pos)+1
|
or (func.poe and edline == editor:LineFromPosition(func.poe)+1) then
|
cache.line = nil
|
end
|
ctrl:SetItemBold(item, val)
|
if val then ctrl:SelectItem(item, val) end
|
|
if not ide.config.outline.jumptocurrentfunction then return end
|
n = n + 1
|
-- check that this and the items around it are all visible;
|
-- this is to avoid the situation when the current item is only partially visible
|
local isvisible = ctrl:IsVisible(item) and ctrl:GetNextVisible(item):IsOk() and ctrl:GetPrevVisible(item):IsOk()
|
if val and not isvisible then
|
needshown[MIN] = math.min(needshown[MIN], n)
|
needshown[MAX] = math.max(needshown[MAX], n)
|
elseif isvisible then
|
visible[MIN] = math.min(visible[MIN], n)
|
visible[MAX] = math.max(visible[MAX], n)
|
end
|
end, cache.fileitem, true)
|
|
if not ide.config.outline.jumptocurrentfunction then return end
|
if needshown[MAX] > visible[MAX] then
|
ctrl:ScrollLines(needshown[MAX]-visible[MAX]) -- scroll forward to the last hidden line
|
elseif needshown[MIN] < visible[MIN] then
|
ctrl:ScrollLines(needshown[MIN]-visible[MIN]) -- scroll backward to the first hidden line
|
end
|
end,
|
})
|
|
local function queuePath(path)
|
-- only queue if symbols inactivity is set, so files will be indexed
|
if ide.config.symbolindexinactivity and not outline.indexqueue[0][path] then
|
outline.indexqueue[0][path] = true
|
table.insert(outline.indexqueue, 1, path)
|
end
|
end
|
|
function outline:GetFileSymbols(path)
|
local symbols = self.settings.symbols[path]
|
-- queue path to process when appropriate
|
if not symbols then queuePath(path) end
|
return symbols
|
end
|
|
function outline:GetEditorSymbols(editor)
|
-- force token refresh (as these may be not updated yet)
|
if #editor:GetTokenList() == 0 then
|
while editor:IndicateSymbols() do end
|
end
|
|
-- only refresh the functions when none is present
|
if not caches[editor] or #(caches[editor].funcs or {}) == 0 then outlineRefresh(editor, true) end
|
return caches[editor] and caches[editor].funcs or {}
|
end
|
|
function outline:RefreshSymbols(path, callback)
|
if isIgnoredInIndex(path) then return end
|
|
local exts = {}
|
for _, ext in pairs(ide:GetKnownExtensions()) do
|
local spec = ide:FindSpec(ext)
|
if spec and spec.marksymbols then table.insert(exts, ext) end
|
end
|
|
local opts = {sort = false, folder = false, skipbinary = true, yield = true,
|
-- skip those directories that are on the "ignore" list
|
ondirectory = function(name) return outline.settings.ignoredirs[name] == nil end
|
}
|
local nextfile = coroutine.wrap(function() ide:GetFileList(path, true, table.concat(exts, ";"), opts) end)
|
while true do
|
local file = nextfile()
|
if not file then break end
|
if not isIgnoredInIndex(file) then (callback or queuePath)(file) end
|
end
|
end
|
|
function outline:UpdateSymbols(fname, symb)
|
local symbols = self.settings.symbols
|
symbols[fname] = symb
|
|
-- purge outdated records
|
local threshold = ide:GetTime() - 60*60*24*7 -- cache for 7 days
|
if not self.indexpurged then
|
for k, v in pairs(symbols) do
|
if v.updated < threshold then symbols[k] = nil end
|
end
|
self.indexpurged = true
|
end
|
|
self.needsaving = true
|
end
|
|
function outline:SaveSettings(force)
|
if self.needsaving or force then
|
ide:PushStatus(TR("Updating symbol index and settings..."))
|
package:SetSettings(self.settings, {keyignore = {depth = true, image = true, poe = true, item = true, skip = true}})
|
ide:PopStatus()
|
self.needsaving = false
|
end
|
end
|
|
MergeSettings(outline.settings, package:GetSettings())
|