local core = require "core" local common = require "core.common" local command = require "core.command" local config = require "core.config" local keymap = require "core.keymap" local style = require "core.style" local View = require "core.view" local TodoTreeView = View:extend() config.todo_tags = { --"TODO", "BUG", "FIX", "FIXME", "IMPROVEMENT", "@todo", "@fixme", "@testme", "@leak" } --< @r-lyeh -- Paths or files to be ignored config.todo_ignore_paths = { "tools/tcc", --< @r-lyeh "tools\\tcc", --< @r-lyeh "engine/fwk", --< @r-lyeh "engine\\fwk", --< @r-lyeh "engine/joint", --< @r-lyeh "engine\\joint", --< @r-lyeh } -- 'tag' mode can be used to group the todos by tags -- 'file' mode can be used to group the todos by files config.todo_mode = "tag" -- Tells if the plugin should start with the nodes expanded. default: true for tag mode config.todo_expanded = config.todo_mode == "tag" -- list of allowed extensions: items must start and end with a dot character config.todo_allowed_extensions = '.h.c.m.hh.cc.hpp.cpp.cxx.lua.py.cs.vs.fs.bat.' --< @r-lyeh -- whether the sidebar treeview is initially visible or not config.todo_visible = false function TodoTreeView:new() TodoTreeView.super.new(self) self.scrollable = true self.focusable = false self.visible = config.todo_visible self.times_cache = {} self.cache = {} self.cache_updated = false self.init_size = true -- Items are generated from cache according to the mode self.items = {} end local function is_file_ignored(filename) for _, path in ipairs(config.todo_ignore_paths) do local s, _ = filename:find(path) if s then return true end end return false end function TodoTreeView:refresh_cache() local items = {} if not next(self.items) then items = self.items end self.updating_cache = true core.add_thread(function() for _, item in ipairs(core.project_files) do local ignored = is_file_ignored(item.filename) if not ignored and item.type == "file" then local cached = self:get_cached(item) if config.todo_mode == "file" then items[cached.filename] = cached else for _, todo in ipairs(cached.todos) do local tag = todo.tag if not items[tag] then local t = {} t.expanded = config.todo_expanded t.type = "group" t.todos = {} t.tag = tag items[tag] = t end table.insert(items[tag].todos, todo) end end end end -- Copy expanded from old items if config.todo_mode == "tag" and next(self.items) then for tag, data in pairs(self.items) do if items[tag] then items[tag].expanded = data.expanded end end end self.items = items core.redraw = true self.cache_updated = true self.updating_cache = false end, self) end local function find_file_todos(t, filename) --< @r-lyeh local ext = (filename:match "[^.]+$") .. '.' if not string.find(config.todo_allowed_extensions,ext) then return end --< local fp = io.open(filename) if not fp then return t end --< @r-lyeh: optimized loops: early exit if quicksearch fails local function lines(str) local result = {} for line in string.gmatch(str, "(.-)%c") do -- line in str:gmatch '[^\n]+' do -- Add spaces at the start and end of line so the pattern will pick -- tags at the start and at the end of lines table.insert(result, " "..line.." ") end return result end local before = #t local content = fp:read("*all") for _, todo_tag in ipairs(config.todo_tags) do if string.find(content, todo_tag) then local n = 0 for _, line in ipairs(lines(content)) do n = n + 1 local match_str = todo_tag[1] == '@' and todo_tag or "[^a-zA-Z_\"'`]"..todo_tag.."[^a-zA-Z_\"'`]+" local s, e = line:find(match_str) if s then local d = {} d.tag = string.sub(string.upper(todo_tag), todo_tag:byte(1) == 64 and 2 or 1) .. 's' d.filename = filename d.text = line:sub(e+1) if d.text == "" then d.text = config.todo_mode == "tag" and filename:match("^.+[/\\](.+)$") or "blank" end d.line = n d.col = s table.insert(t, d) end end end end fp:close() if #t ~= before then coroutine.yield() core.redraw = true end --< end function TodoTreeView:get_cached(item) local t = self.cache[item.filename] if not t then t = {} t.expanded = config.todo_expanded t.filename = item.filename t.abs_filename = system.absolute_path(item.filename) t.type = item.type t.todos = {} find_file_todos(t.todos, t.filename) self.cache[t.filename] = t end return t end function TodoTreeView:get_name() return "Todo Tree" end function TodoTreeView:get_item_height() return style.font:get_height() + style.padding.y end function TodoTreeView:get_cached_time(doc) local t = self.times_cache[doc] if not t then local info = system.get_file_info(doc.filename) if not info then return nil end self.times_cache[doc] = info.modified end return t end function TodoTreeView:check_cache() for _, doc in ipairs(core.docs) do if doc.filename then local info = system.get_file_info(doc.filename) local cached = self:get_cached_time(doc) if not info and cached then -- document deleted self.times_cache[doc] = nil self.cache[doc.filename] = nil self.cache_updated = false elseif cached and cached ~= info.modified then -- document modified self.times_cache[doc] = info.modified self.cache[doc.filename] = nil self.cache_updated = false end end end if core.project_files ~= self.last_project_files then self.last_project_files = core.project_files self.cache_updated = false end end function TodoTreeView:each_item() self:check_cache() if not self.updating_cache and not self.cache_updated then self:refresh_cache() end return coroutine.wrap(function() local ox, oy = self:get_content_offset() local y = oy + style.padding.y local w = self.size.x local h = self:get_item_height() for _, item in pairs(self.items) do if #item.todos > 0 then coroutine.yield(item, ox, y, w, h) y = y + h for _, todo in ipairs(item.todos) do if item.expanded then coroutine.yield(todo, ox, y, w, h) y = y + h end end end end end) end function TodoTreeView:on_mouse_moved(px, py) self.hovered_item = nil for item, x,y,w,h in self:each_item() do if px > x and py > y and px <= x + w and py <= y + h then self.hovered_item = item break end end end function TodoTreeView:on_mouse_pressed(button, x, y) if not self.hovered_item then return elseif self.hovered_item.type == "file" or self.hovered_item.type == "group" then self.hovered_item.expanded = not self.hovered_item.expanded else core.try(function() local i = self.hovered_item local dv = core.root_view:open_doc(core.open_doc(i.filename)) core.root_view.root_node:update_layout() dv.doc:set_selection(i.line, i.col) dv:scroll_to_line(i.line, false, true) end) end end function TodoTreeView:update() self.scroll.to.y = math.max(0, self.scroll.to.y) -- update width local dest = self.visible and config.treeview_size or 0 if self.init_size then self.size.x = dest self.init_size = false else self:move_towards(self.size, "x", dest) end TodoTreeView.super.update(self) end function TodoTreeView:draw() self:draw_background(style.background2) --local h = self:get_item_height() local icon_width = style.icon_font:get_width("D") local spacing = style.font:get_width(" ") * 2 local root_depth = 0 for item, x,y,w,h in self:each_item() do local color = style.text -- hovered item background if item == self.hovered_item then renderer.draw_rect(x, y, w, h, style.line_highlight) color = style.accent end -- icons local item_depth = 0 x = x + (item_depth - root_depth) * style.padding.x + style.padding.x if item.type == "file" then local icon1 = item.expanded and "-" or "+" common.draw_text(style.icon_font, color, icon1, nil, x, y, 0, h) x = x + style.padding.x common.draw_text(style.icon_font, color, "f", nil, x, y, 0, h) x = x + icon_width elseif item.type == "group" then local icon1 = item.expanded and "-" or "+" common.draw_text(style.icon_font, color, icon1, nil, x, y, 0, h) x = x + icon_width / 2 else if config.todo_mode == "tag" then x = x + style.padding.x else x = x + style.padding.x * 1.5 end common.draw_text(style.icon_font, color, "i", nil, x, y, 0, h) x = x + icon_width end -- text x = x + spacing if item.type == "file" then common.draw_text(style.font, color, item.filename, nil, x, y, 0, h) elseif item.type == "group" then common.draw_text(style.font, color, item.tag, nil, x, y, 0, h) else if config.todo_mode == "file" then common.draw_text(style.font, color, item.tag.." - "..item.text, nil, x, y, 0, h) else common.draw_text(style.font, color, item.text, nil, x, y, 0, h) end end end end -- init local view = TodoTreeView() local node = core.root_view:get_active_node() view.size.x = config.treeview_size node:split("right", view, true) -- register commands and keymap command.add(nil, { ["todotreeview:toggle"] = function() view.visible = not view.visible end, ["todotreeview:expand-items"] = function() for _, item in pairs(view.items) do item.expanded = true end end, ["todotreeview:hide-items"] = function() for _, item in pairs(view.items) do item.expanded = false end end, }) keymap.add { ["ctrl+shift+t"] = "todotreeview:toggle" } keymap.add { ["ctrl+shift+e"] = "todotreeview:expand-items" } keymap.add { ["ctrl+shift+h"] = "todotreeview:hide-items" }