In TreeView:on_mouse_pressed() we need to find the directory a relative filename belongs to from its absolute filename. The code was using string.find to locate the relative filename within the absolute path but in some very specific cases we can find a pattern which is not the right-most one leading to a wrong directory name. Fix the error by adding a loop to make sure we find the right-most match. The standard Lua library has not a string.rfind to make a reverse search. Add a check to avoid trying to updating a topdir project directory.
378 lines
11 KiB
Lua
378 lines
11 KiB
Lua
-- 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 default_treeview_size = 200 * SCALE
|
|
local tooltip_offset = style.font:get_height("A")
|
|
local tooltip_border = 1
|
|
local tooltip_delay = 0.5
|
|
local tooltip_alpha = 255
|
|
local tooltip_alpha_rate = 1
|
|
|
|
|
|
local function get_depth(filename)
|
|
local n = 1
|
|
for sep in filename:gmatch("[\\/]") do
|
|
n = n + 1
|
|
end
|
|
return n
|
|
end
|
|
|
|
local function replace_alpha(color, alpha)
|
|
local r, g, b = table.unpack(color)
|
|
return { r, g, b, alpha }
|
|
end
|
|
|
|
|
|
local TreeView = View:extend()
|
|
|
|
function TreeView:new()
|
|
TreeView.super.new(self)
|
|
self.scrollable = true
|
|
self.visible = true
|
|
self.init_size = true
|
|
self.target_size = default_treeview_size
|
|
self.cache = {}
|
|
self.last = {}
|
|
self.tooltip = { x = 0, y = 0, begin = 0, alpha = 0 }
|
|
end
|
|
|
|
|
|
function TreeView:set_target_size(axis, value)
|
|
if axis == "x" then
|
|
self.target_size = value
|
|
return true
|
|
end
|
|
end
|
|
|
|
|
|
function TreeView:get_cached(item, dirname)
|
|
local dir_cache = self.cache[dirname]
|
|
if not dir_cache then
|
|
dir_cache = {}
|
|
self.cache[dirname] = dir_cache
|
|
end
|
|
-- to discriminate top directories from regular files or subdirectories
|
|
-- we add ':' at the end of the top directories' filename. it will be
|
|
-- used only to identify the entry into the cache.
|
|
local cache_name = item.filename .. (item.topdir and ":" or "")
|
|
local t = dir_cache[cache_name]
|
|
if not t then
|
|
t = {}
|
|
local basename = common.basename(item.filename)
|
|
if item.topdir then
|
|
t.filename = basename
|
|
t.expanded = true
|
|
t.depth = 0
|
|
t.abs_filename = dirname
|
|
else
|
|
t.filename = item.filename
|
|
t.depth = get_depth(item.filename)
|
|
t.abs_filename = dirname .. PATHSEP .. item.filename
|
|
end
|
|
t.name = basename
|
|
t.type = item.type
|
|
dir_cache[cache_name] = t
|
|
end
|
|
return t
|
|
end
|
|
|
|
|
|
function TreeView:get_name()
|
|
return "Project"
|
|
end
|
|
|
|
|
|
function TreeView:get_item_height()
|
|
return style.font:get_height() + style.padding.y
|
|
end
|
|
|
|
|
|
function TreeView:invalidate_cache(dirname)
|
|
for _, v in pairs(self.cache[dirname]) do
|
|
v.skip = nil
|
|
end
|
|
end
|
|
|
|
|
|
function TreeView:check_cache()
|
|
-- invalidate cache's skip values if project_files has changed
|
|
for i = 1, #core.project_directories do
|
|
local dir = core.project_directories[i]
|
|
local last_files = self.last[dir.name]
|
|
if not last_files then
|
|
self.last[dir.name] = dir.files
|
|
else
|
|
if dir.files ~= last_files then
|
|
self:invalidate_cache(dir.name)
|
|
self.last[dir.name] = dir.files
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
|
|
function TreeView:each_item()
|
|
return coroutine.wrap(function()
|
|
self:check_cache()
|
|
local count_lines = 0
|
|
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 k = 1, #core.project_directories do
|
|
local dir = core.project_directories[k]
|
|
local dir_cached = self:get_cached(dir.item, dir.name)
|
|
coroutine.yield(dir_cached, ox, y, w, h)
|
|
count_lines = count_lines + 1
|
|
y = y + h
|
|
local i = 1
|
|
while i <= #dir.files and dir_cached.expanded do
|
|
local item = dir.files[i]
|
|
local cached = self:get_cached(item, dir.name)
|
|
|
|
coroutine.yield(cached, ox, y, w, h)
|
|
count_lines = count_lines + 1
|
|
y = y + h
|
|
i = i + 1
|
|
|
|
if not cached.expanded then
|
|
if cached.skip then
|
|
i = cached.skip
|
|
else
|
|
local depth = cached.depth
|
|
while i <= #dir.files do
|
|
if get_depth(dir.files[i].filename) <= depth then break end
|
|
i = i + 1
|
|
end
|
|
cached.skip = i
|
|
end
|
|
end
|
|
end -- while files
|
|
end -- for directories
|
|
self.count_lines = count_lines
|
|
end)
|
|
end
|
|
|
|
|
|
function TreeView:get_text_bounding_box(item, x, y, w, h)
|
|
local icon_width = style.icon_font:get_width("D")
|
|
local xoffset = item.depth * style.padding.x + style.padding.x + icon_width
|
|
x = x + xoffset
|
|
w = style.font:get_width(item.name) + 2 * style.padding.x
|
|
return x, y, w, h
|
|
end
|
|
|
|
|
|
function TreeView:on_mouse_moved(px, py, ...)
|
|
TreeView.super.on_mouse_moved(self, px, py, ...)
|
|
if self.dragging_scrollbar then return end
|
|
|
|
local item_changed, tooltip_changed
|
|
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
|
|
item_changed = true
|
|
self.hovered_item = item
|
|
|
|
x,y,w,h = self:get_text_bounding_box(item, x,y,w,h)
|
|
if px > x and py > y and px <= x + w and py <= y + h then
|
|
tooltip_changed = true
|
|
self.tooltip.x, self.tooltip.y = px, py
|
|
self.tooltip.begin = system.get_time()
|
|
end
|
|
break
|
|
end
|
|
end
|
|
if not item_changed then self.hovered_item = nil end
|
|
if not tooltip_changed then self.tooltip.x, self.tooltip.y = nil, nil end
|
|
end
|
|
|
|
|
|
local function create_directory_in(item)
|
|
local path = item.abs_filename
|
|
core.command_view:enter("Create directory in " .. path, function(text)
|
|
local dirname = path .. PATHSEP .. text
|
|
local success, err = system.mkdir(dirname)
|
|
if not success then
|
|
core.error("cannot create directory %q: %s", dirname, err)
|
|
end
|
|
item.expanded = true
|
|
core.reschedule_project_scan()
|
|
end)
|
|
end
|
|
|
|
|
|
function TreeView:on_mouse_pressed(button, x, y, clicks)
|
|
local caught = TreeView.super.on_mouse_pressed(self, button, x, y, clicks)
|
|
if caught then
|
|
return
|
|
end
|
|
local hovered_item = self.hovered_item
|
|
if not hovered_item then
|
|
return
|
|
elseif hovered_item.type == "dir" then
|
|
if keymap.modkeys["ctrl"] and button == "left" then
|
|
create_directory_in(hovered_item)
|
|
else
|
|
if core.project_files_limit and not hovered_item.expanded then
|
|
local filename, abs_filename = hovered_item.filename, hovered_item.abs_filename
|
|
local index = 0
|
|
-- The loop below is used to find the first match starting from the end
|
|
-- in case there are multiple matches.
|
|
while index and index + #filename < #abs_filename do
|
|
index = string.find(abs_filename, filename, index + 1, true)
|
|
end
|
|
-- we assume here index is not nil because the abs_filename must contain the
|
|
-- relative filename
|
|
local dirname = string.sub(abs_filename, 1, index - 2)
|
|
if core.is_project_folder(dirname) then
|
|
core.scan_project_folder(dirname, filename)
|
|
self:invalidate_cache(dirname)
|
|
end
|
|
end
|
|
hovered_item.expanded = not hovered_item.expanded
|
|
end
|
|
else
|
|
core.try(function()
|
|
local doc_filename = common.relative_path(core.project_dir, hovered_item.abs_filename)
|
|
core.root_view:open_doc(core.open_doc(doc_filename))
|
|
end)
|
|
end
|
|
end
|
|
|
|
|
|
function TreeView:update()
|
|
-- update width
|
|
local dest = self.visible and self.target_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
|
|
|
|
local duration = system.get_time() - self.tooltip.begin
|
|
if self.hovered_item and self.tooltip.x and duration > tooltip_delay then
|
|
self:move_towards(self.tooltip, "alpha", tooltip_alpha, tooltip_alpha_rate)
|
|
else
|
|
self.tooltip.alpha = 0
|
|
end
|
|
|
|
TreeView.super.update(self)
|
|
end
|
|
|
|
|
|
function TreeView:get_scrollable_size()
|
|
return self.count_lines and self:get_item_height() * (self.count_lines + 1) or math.huge
|
|
end
|
|
|
|
|
|
function TreeView:draw_tooltip()
|
|
local text = common.home_encode(self.hovered_item.abs_filename)
|
|
local w, h = style.font:get_width(text), style.font:get_height(text)
|
|
|
|
local x, y = self.tooltip.x + tooltip_offset, self.tooltip.y + tooltip_offset
|
|
w, h = w + style.padding.x, h + style.padding.y
|
|
|
|
if x + w > core.root_view.root_node.size.x then -- check if we can span right
|
|
x = x - w -- span left instead
|
|
end
|
|
|
|
local bx, by = x - tooltip_border, y - tooltip_border
|
|
local bw, bh = w + 2 * tooltip_border, h + 2 * tooltip_border
|
|
renderer.draw_rect(bx, by, bw, bh, replace_alpha(style.text, self.tooltip.alpha))
|
|
renderer.draw_rect(x, y, w, h, replace_alpha(style.background2, self.tooltip.alpha))
|
|
common.draw_text(style.font, replace_alpha(style.text, self.tooltip.alpha), text, "center", x, y, w, h)
|
|
end
|
|
|
|
|
|
function TreeView:draw()
|
|
self:draw_background(style.background2)
|
|
|
|
local icon_width = style.icon_font:get_width("D")
|
|
local spacing = style.icon_font:get_width("f") / 2
|
|
|
|
local doc = core.active_view.doc
|
|
local active_filename = doc and system.absolute_path(doc.filename or "")
|
|
|
|
for item, x,y,w,h in self:each_item() do
|
|
local color = style.text
|
|
|
|
-- highlight active_view doc
|
|
if item.abs_filename == active_filename then
|
|
color = style.accent
|
|
end
|
|
|
|
-- hovered item background
|
|
if item == self.hovered_item then
|
|
renderer.draw_rect(x, y, w, h, style.line_highlight)
|
|
color = style.accent
|
|
end
|
|
|
|
-- icons
|
|
x = x + item.depth * style.padding.x + style.padding.x
|
|
if item.type == "dir" then
|
|
local icon1 = item.expanded and "-" or "+"
|
|
local icon2 = item.expanded and "D" or "d"
|
|
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, icon2, nil, x, y, 0, h)
|
|
x = x + icon_width
|
|
else
|
|
x = x + style.padding.x
|
|
common.draw_text(style.icon_font, color, "f", nil, x, y, 0, h)
|
|
x = x + icon_width
|
|
end
|
|
|
|
-- text
|
|
x = x + spacing
|
|
x = common.draw_text(style.font, color, item.name, nil, x, y, 0, h)
|
|
end
|
|
|
|
self:draw_scrollbar()
|
|
if self.hovered_item and self.tooltip.alpha > 0 then
|
|
core.root_view:defer_draw(self.draw_tooltip, self)
|
|
end
|
|
end
|
|
|
|
|
|
-- init
|
|
local view = TreeView()
|
|
local node = core.root_view:get_active_node()
|
|
local treeview_node = node:split("left", view, {x = true}, true)
|
|
|
|
-- The toolbarview plugin is special because it is plugged inside
|
|
-- a treeview pane which is itelf provided in a plugin.
|
|
-- We therefore break the usual plugin's logic that would require each
|
|
-- plugin to be independent of each other. In addition it is not the
|
|
-- plugin module that plug itself in the active node but it is plugged here
|
|
-- in the treeview node.
|
|
local toolbar_plugin, ToolbarView = core.try(require, "plugins.toolbarview")
|
|
if config.toolbarview ~= false and toolbar_plugin then
|
|
local toolbar_view = ToolbarView()
|
|
treeview_node:split("down", toolbar_view, {y = true})
|
|
local min_toolbar_width = toolbar_view:get_min_width()
|
|
view:set_target_size("x", math.max(default_treeview_size, min_toolbar_width))
|
|
command.add(nil, {
|
|
["toolbar:toggle"] = function()
|
|
toolbar_view:toggle_visible()
|
|
end,
|
|
})
|
|
end
|
|
|
|
|
|
-- register commands and keymap
|
|
command.add(nil, {
|
|
["treeview:toggle"] = function()
|
|
view.visible = not view.visible
|
|
end,
|
|
})
|
|
|
|
keymap.add { ["ctrl+\\"] = "treeview:toggle" }
|