v4k-git-backup/engine/art/editor/lite/data/plugins/todotreeview.lua

390 lines
10 KiB
Lua
Raw Normal View History

2023-11-15 19:14:14 +00:00
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" }