-- Copyright 2015-18 Paul Kulchenko, ZeroBrane LLC local ide = ide ide.markers = { markersCtrl = nil, imglist = ide:CreateImageList("MARKERS", "FILE-NORMAL", "DEBUG-BREAKPOINT-TOGGLE", "BOOKMARK-TOGGLE"), needrefresh = {}, settings = {markers = {}}, } local unpack = table.unpack or unpack local markers = ide.markers local caches = {} local image = { FILE = 0, BREAKPOINT = 1, BOOKMARK = 2 } local markertypes = {breakpoint = 0, bookmark = 0} local maskall = 0 for markertype in pairs(markertypes) do markertypes[markertype] = 2^ide:GetMarker(markertype) maskall = maskall + markertypes[markertype] end -- make these two IDs local to this menu, -- as their status differs from bookmarks/breakpoints in the editor local BOOKMARKTOGGLE = ID("markers.bookmarktoggle") local BREAKPOINTTOGGLE = ID("markers.breakpointtoggle") ide.config.toolbar.iconmap[BOOKMARKTOGGLE] = ide.config.toolbar.iconmap[ID.BOOKMARKTOGGLE] ide.config.toolbar.iconmap[BREAKPOINTTOGGLE] = ide.config.toolbar.iconmap[ID.BREAKPOINTTOGGLE] local function resetMarkersTimer() if ide.config.markersinactivity then ide.timers.markers:Start(ide.config.markersinactivity*1000, wx.wxTIMER_ONE_SHOT) end end local function needRefresh(editor) ide.markers.needrefresh[editor] = true resetMarkersTimer() end local function getMarkers(editor, mtype) local edmarkers = {} local line = editor:MarkerNext(0, maskall) while line ~= wx.wxNOT_FOUND do local markerval = editor:MarkerGet(line) for markertype, val in pairs(markertypes) do if bit.band(markerval, val) > 0 and (not mtype or markertype == mtype) then table.insert(edmarkers, {line, markertype}) end end line = editor:MarkerNext(line + 1, maskall) end return edmarkers end local function markersRefresh() local ctrl = ide.markers.markersCtrl local win = ide:GetMainFrame():FindFocus() ctrl:Freeze() for editor in pairs(ide.markers.needrefresh) do local cache = caches[editor] if cache then local fileitem = cache.fileitem if not fileitem then local filename = ide:GetDocument(editor):GetTabText() local root = ctrl:GetRootItem() if not root or not root:IsOk() then return end fileitem = ctrl:AppendItem(root, filename, image.FILE) ctrl:SortChildren(root) cache.fileitem = fileitem end -- 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) for _, edmarker in ipairs(getMarkers(editor)) do local line, markertype = unpack(edmarker) local text = ("%d: %s"):format(line+1, FixUTF8(editor:GetLineDyn(line), function(s) return '\\'..string.byte(s) end)) ctrl:AppendItem(fileitem, text:gsub("[\r\n]+$",""), image[markertype:upper()]) end -- if no markers added, then remove the file from the markers list ctrl:Expand(fileitem) if not ctrl:ItemHasChildren(fileitem) then ctrl:Delete(fileitem) cache.fileitem = nil end end end ctrl:Thaw() if win and win ~= ide:GetMainFrame():FindFocus() then win:SetFocus() end end local function item2editor(item_id) for editor, cache in pairs(caches) do if cache.fileitem and cache.fileitem:GetValue() == item_id:GetValue() then return editor end end end local function clearAllEditorMarkers(mtype, editor) for _, edmarker in ipairs(getMarkers(editor, mtype)) do local line = unpack(edmarker) editor:MarkerToggle(mtype, line, false) end end local function clearAllProjectMarkers(mtype) for filepath, markers in pairs(markers.settings.markers) do if ide:IsProjectSubDirectory(filepath) then local doc = ide:FindDocument(filepath) local editor = doc and doc:GetEditor() for m = #markers, 1, -1 do local line, markertype = unpack(markers[m]) if markertype == mtype then if editor then editor:MarkerToggle(markertype, line, false) else table.remove(markers, m) end end end end end end local function createMarkersWindow() 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) markers.markersCtrl = ctrl ide.timers.markers = ide:AddTimer(ctrl, function() markersRefresh() end) ctrl:AddRoot("Markers") ctrl:SetImageList(markers.imglist) ctrl:SetFont(ide.font.tree) function ctrl:ActivateItem(item_id, marker) local itemimage = ctrl:GetItemImage(item_id) if itemimage == image.FILE then -- activate editor tab local editor = item2editor(item_id) if editor then ide:GetDocument(editor):SetActive() end else -- clicked on the marker item local parent = ctrl:GetItemParent(item_id) if parent:IsOk() and ctrl:GetItemImage(parent) == image.FILE then local editor = item2editor(parent) if editor then local line = tonumber(ctrl:GetItemText(item_id):match("^(%d+):")) if line then if marker then editor:MarkerToggle(marker, line-1, false) ctrl:Delete(item_id) return -- don't activate the editor when the breakpoint is toggled end editor:GotoLine(line-1) editor:EnsureVisibleEnforcePolicy(line-1) end 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 local marker local itemimage = ctrl:GetItemImage(item_id) if bit.band(flags, wx.wxTREE_HITTEST_ONITEMICON) > 0 then for iname, itype in pairs(image) do if itemimage == itype then marker = iname:lower() end end end ctrl:ActivateItem(item_id, marker) else event:Skip() end return true end local function clearMarkersInFile(item_id, marker) local editor = item2editor(item_id) local itemimage = ctrl:GetItemImage(item_id) if itemimage ~= image.FILE then local parent = ctrl:GetItemParent(item_id) if parent:IsOk() and ctrl:GetItemImage(parent) == image.FILE then editor = item2editor(parent) end end if editor then clearAllEditorMarkers(marker, editor) end end ctrl:Connect(wx.wxEVT_LEFT_DOWN, activateByPosition) ctrl:Connect(wx.wxEVT_LEFT_DCLICK, activateByPosition) ctrl:Connect(wx.wxEVT_COMMAND_TREE_ITEM_ACTIVATED, function(event) ctrl:ActivateItem(event:GetItem()) end) ctrl:Connect(wx.wxEVT_COMMAND_TREE_ITEM_MENU, function (event) local item_id = event:GetItem() local menu = ide:MakeMenu { { BOOKMARKTOGGLE, TR("Toggle Bookmark"), TR("Toggle bookmark") }, { BREAKPOINTTOGGLE, TR("Toggle Breakpoint"), TR("Toggle breakpoint") }, { }, { ID.BOOKMARKFILECLEAR, TR("Clear Bookmarks In File")..KSC(ID.BOOKMARKFILECLEAR) }, { ID.BREAKPOINTFILECLEAR, TR("Clear Breakpoints In File")..KSC(ID.BREAKPOINTFILECLEAR) }, { }, { ID.BOOKMARKPROJECTCLEAR, TR("Clear Bookmarks In Project")..KSC(ID.BOOKMARKPROJECTCLEAR) }, { ID.BREAKPOINTPROJECTCLEAR, TR("Clear Breakpoints In Project")..KSC(ID.BREAKPOINTPROJECTCLEAR) }, } local itemimage = ctrl:GetItemImage(item_id) menu:Enable(BOOKMARKTOGGLE, itemimage == image.BOOKMARK) menu:Connect(BOOKMARKTOGGLE, wx.wxEVT_COMMAND_MENU_SELECTED, function() ctrl:ActivateItem(item_id, "bookmark") end) menu:Enable(BREAKPOINTTOGGLE, itemimage == image.BREAKPOINT) menu:Connect(BREAKPOINTTOGGLE, wx.wxEVT_COMMAND_MENU_SELECTED, function() ctrl:ActivateItem(item_id, "breakpoint") end) menu:Enable(ID.BOOKMARKFILECLEAR, itemimage == image.BOOKMARK or itemimage == image.FILE) menu:Connect(ID.BOOKMARKFILECLEAR, wx.wxEVT_COMMAND_MENU_SELECTED, function() clearMarkersInFile(item_id, "bookmark") end) menu:Enable(ID.BREAKPOINTFILECLEAR, itemimage == image.BREAKPOINT or itemimage == image.FILE) menu:Connect(ID.BREAKPOINTFILECLEAR, wx.wxEVT_COMMAND_MENU_SELECTED, function() clearMarkersInFile(item_id, "breakpoint") end) PackageEventHandle("onMenuMarkers", 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("markerspanel") then ide:AddPanelDocked(ide:GetOutputNotebook(), ctrl, "markerspanel", TR("Markers"), reconfigure, false) else ide:AddPanel(ctrl, "markerspanel", TR("Markers"), reconfigure) end end local package = ide:AddPackage('core.markers', { onRegister = function(self) if not ide.config.markersinactivity then return end createMarkersWindow() local bmmenu = ide:FindMenuItem(ID.BOOKMARK):GetSubMenu() bmmenu:AppendSeparator() bmmenu:Append(ID.BOOKMARKFILECLEAR, TR("Clear Bookmarks In File")..KSC(ID.BOOKMARKFILECLEAR)) bmmenu:Append(ID.BOOKMARKPROJECTCLEAR, TR("Clear Bookmarks In Project")..KSC(ID.BOOKMARKPROJECTCLEAR)) local bpmenu = ide:FindMenuItem(ID.BREAKPOINT):GetSubMenu() bpmenu:AppendSeparator() bpmenu:Append(ID.BREAKPOINTFILECLEAR, TR("Clear Breakpoints In File")..KSC(ID.BREAKPOINTFILECLEAR)) bpmenu:Append(ID.BREAKPOINTPROJECTCLEAR, TR("Clear Breakpoints In Project")..KSC(ID.BREAKPOINTPROJECTCLEAR)) ide:GetMainFrame():Connect(ID.BOOKMARKFILECLEAR, wx.wxEVT_COMMAND_MENU_SELECTED, function() local editor = ide:GetEditor() if editor then clearAllEditorMarkers("bookmark", editor) end end) ide:GetMainFrame():Connect(ID.BOOKMARKPROJECTCLEAR, wx.wxEVT_COMMAND_MENU_SELECTED, function() clearAllProjectMarkers("bookmark") end) ide:GetMainFrame():Connect(ID.BREAKPOINTFILECLEAR, wx.wxEVT_COMMAND_MENU_SELECTED, function() local editor = ide:GetEditor() if editor then clearAllEditorMarkers("breakpoint", editor) end end) ide:GetMainFrame():Connect(ID.BREAKPOINTPROJECTCLEAR, wx.wxEVT_COMMAND_MENU_SELECTED, function() clearAllProjectMarkers("breakpoint") end) end, -- save markers; remove tab from the list onEditorClose = function(self, editor) local cache = caches[editor] if not cache then return end if cache.fileitem then markers.markersCtrl:Delete(cache.fileitem) end caches[editor] = nil end, -- schedule marker update if the change is for one of the editors with markers onEditorUpdateUI = function(self, editor, event) if not caches[editor] then return end if bit.band(event:GetUpdated(), wxstc.wxSTC_UPDATE_CONTENT) == 0 then return end needRefresh(editor) end, onEditorMarkerUpdate = function(self, editor) -- if no marker, then all markers in a file need to be refreshed if not caches[editor] then caches[editor] = {} end needRefresh(editor) -- delay saving markers as other EditorMarkerUpdate handlers may still modify them, -- but check to make sure that the editor is still valid ide:DoWhenIdle(function() if ide:IsValidCtrl(editor) then markers:SaveMarkers(editor) end end) end, onEditorSave = function(self, editor) markers:SaveMarkers(editor) end, onEditorLoad = function(self, editor) markers:LoadMarkers(editor) end, }) function markers:SaveSettings() package:SetSettings(self.settings) end function markers:SaveMarkers(editor, force) -- if the file has the name and has not been modified, save the breakpoints -- this also works when the file is saved as the modified flag is already set to `false` local doc = ide:GetDocument(editor) local filepath = doc and doc:GetFilePath() if filepath and (force or not doc:IsModified()) then -- remove it from the list if it has no breakpoints local edmarkers = getMarkers(editor) self.settings.markers[filepath] = #edmarkers > 0 and edmarkers or nil self:SaveSettings() end end function markers:LoadMarkers(editor) local doc = ide:GetDocument(editor) local filepath = doc:GetFilePath() if filepath then for _, edmarker in ipairs(self.settings.markers[filepath] or {}) do local line, markertype = unpack(edmarker) editor:MarkerToggle(markertype, line, true) end end end MergeSettings(markers.settings, package:GetSettings())