ext/ext-editor/embed/lite/data/plugins/treeview.lua
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"

config.treeview_size = 200 * SCALE

local function get_depth(filename)
  local n = 0
  for sep in filename:gmatch("[\\/]") do
    n = n + 1
  end
  return n
end


local TreeView = View:extend()

function TreeView:new()
  TreeView.super.new(self)
  self.scrollable = true
  self.visible = false --< @r-lyeh true>false
  self.init_size = true
  self.cache = {}
end


function TreeView:get_cached(item)
  local t = self.cache[item.filename]
  if not t then
    t = {}
    t.filename = item.filename
    t.abs_filename = system.absolute_path(item.filename)
    t.name = t.filename:match("[^\\/]+$")
    t.depth = get_depth(t.filename)
    t.type = item.type
    self.cache[t.filename] = 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:check_cache()
  -- invalidate cache's skip values if project_files has changed
  if core.project_files ~= self.last_project_files then
    for _, v in pairs(self.cache) do
      v.skip = nil
    end
    self.last_project_files = core.project_files
  end
end


function TreeView:each_item()
  return coroutine.wrap(function()
    self:check_cache()
    local ox, oy = self:get_content_offset()
    local y = oy + style.padding.y
    local w = self.size.x
    local h = self:get_item_height()

    local i = 1
    while i <= #core.project_files do
      local item = core.project_files[i]
      local cached = self:get_cached(item)

      coroutine.yield(cached, ox, y, w, h)
      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 <= #core.project_files do
            local filename = core.project_files[i].filename
            if get_depth(filename) <= depth then break end
            i = i + 1
          end
          cached.skip = i
        end
      end
    end
  end)
end


function TreeView: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 TreeView:on_mouse_pressed(button, x, y)
  if not self.hovered_item then
    return
  elseif self.hovered_item.type == "dir" then
    self.hovered_item.expanded = not self.hovered_item.expanded
  else
    core.try(function()
      core.root_view:open_doc(core.open_doc(self.hovered_item.filename))
    end)
  end
end


function TreeView:update()
  -- 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

  TreeView.super.update(self)
end


function TreeView:draw()
  self:draw_background(style.background2)

  local icon_width = style.icon_font:get_width("D")
  local spacing = style.font:get_width(" ") * 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
end


-- init
local view = TreeView()
local node = core.root_view:get_active_node()
node:split("left", view, true)

-- register commands and keymap
command.add(nil, {
  ["treeview:toggle"] = function()
    view.visible = not view.visible
  end,
})

keymap.add { ["ctrl+t"] = "treeview:toggle" } --< @r-lyeh ctrl+// > ctrl+t

-- register some context menu items, if available
local has_menu, menu = core.try(require, "plugins.contextmenu")
local has_fsutils, fsutils = core.try(require, "plugins.fsutils")

if has_menu and has_fsutils then
  local function new_file_f(path)
    command.perform "core:new-doc"
  end

  local function new_file()
    new_file_f(view.hovered_item.abs_filename)
  end

  local function new_dir_f(path)
    core.command_view:enter("New directory name", function(dir)
      fsutils.mkdir(dir)
    end)
    core.command_view:set_text(path .. PATHSEP .. "New folder")
  end

  local function new_dir()
    new_dir_f(view.hovered_item.abs_filename)
  end

  local function delete_f(path)
    core.add_thread(function()
      local function wrap()
        return coroutine.wrap(function() fsutils.delete(path, true) end)
      end

      for n in wrap() do
        if n % 100 == 0 then
          core.log("Deleted %d items.", n)
          coroutine.yield(0)
        end
      end

      core.log("%q deleted.", path)
    end)
  end

  local function delete()
    local path = view.hovered_item.abs_filename
    if view.hovered_item.type == "dir"
      and system.show_confirm_dialog("Delete confirmation", string.format("Do you really want to delete %q ?", path)) then
      delete_f(path)
    else
      delete_f(path)
    end
  end

  local function dirname(path)
    local p = fsutils.split(path)
    table.remove(p)
    return table.concat(p, PATHSEP)
  end

  local function rename()
    local oldname = view.hovered_item.abs_filename
    core.command_view:enter("Rename to", function(newname)
      fsutils.move(oldname, newname)
      core.log("Moved %q to %q", oldname, newname)
    end, common.path_suggest)
    core.command_view:set_text(dirname(oldname))
  end

  local function copy_path()
    system.set_clipboard(view.hovered_item.abs_filename)
  end

  menu:register(function() return view.hovered_item and view.hovered_item.type == "dir" end, {
    { text = "New file", command = new_file },
    { text = "New folder", command = new_dir },
    menu.DIVIDER,
    { text = "Rename", command = rename },
    { text = "Delete", command = delete },
    menu.DIVIDER,
    { text = "Copy directory name", command = copy_path }
  })
  menu:register(function() return view.hovered_item and view.hovered_item.type == "file" end, {
    { text = "Rename", command = rename },
    { text = "Delete", command = delete },
    menu.DIVIDER,
    { text = "Copy filename", command = copy_path }
  })
  -- general region of the treeview
  menu:register(function(x, y)
    local x1, y1, x2, y2 = view:get_content_bounds()
    return not view.hovered_item and x > x1 and x <= x2 and y > y1 and y <= y2
  end, {
    { text = "New file", command = function() new_file_f(system.absolute_path('.')) end },
    { text = "New folder", command = function() new_dir_f(system.absolute_path('.')) end }
  })
end

return view --< @r-lyeh
