-- mod-version:1 -- lite-xl 1.16 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"} -- Paths or files to be ignored config.ignore_paths = {} -- Tells if the plugin should start with the nodes expanded config.todo_expanded = true -- '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" config.treeview_size = 200 * SCALE -- default size function TodoTreeView:new() TodoTreeView.super.new(self) self.scrollable = true self.focusable = false self.visible = false 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.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) local fp = io.open(filename) if not fp then return t end local n = 1 for line in fp:lines() do for _, todo_tag in ipairs(config.todo_tags) do local match_str = "[^a-zA-Z_\"'`]"..todo_tag.."[^\"'a-zA-Z_`]+" local s, e = line:find(match_str) if s then local d = {} d.tag = todo_tag d.filename = filename d.text = line:sub(e+1) d.line = n d.col = s table.insert(t, d) end core.redraw = true end if n % 100 == 0 then coroutine.yield() end n = n + 1 core.redraw = true end fp:close() 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:set_target_size(axis, value) if axis == "x" then config.treeview_size = value return true end 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, {x=true}, 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" }