Newer
Older
dotfiles / .config / lite-xl / plugins / scm / init.lua
@Edoko Edoko 25 days ago 27 KB initial commit
-- mod-version:3
--
-- Source Control Management plugin.
-- @copyright Jefferson Gonzalez <[email protected]>
-- @license MIT
--
-- Note: Some ideas and bits taken from:
-- https://github.com/vincens2005/lite-xl-gitdiff-highlight
-- https://github.com/lite-xl/lite-xl-plugins/blob/master/plugins/gitstatus.lua
-- Thanks to everyone involved!
--
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 util = require "plugins.scm.util"
local changes = require "plugins.scm.changes"
local Doc = require "core.doc"
local DocView = require "core.docview"
local StatusView = require "core.statusview"
local ReadDoc = require "plugins.scm.readdoc"
local Git = require "plugins.scm.backend.git"
local Fossil = require "plugins.scm.backend.fossil"
local MessageBox = require "libraries.widget.messagebox"

---@class config.plugins.smc
---@field highlighter boolean
---@field highlighter_alignment "right" | "left"
config.plugins.smc = common.merge({
  highlighter = true,
  highlighter_alignment = "right",
  config_spec = {
    name = "Source Control Management",
    {
      label = "Highlighter",
      description = "Display or hide the changes highlighter from the gutter.",
      path = "highlighter",
      type = "toggle",
      default = true
    },
    {
      label = "Highlighter Alignment",
      description = "The position on the gutter to draw the changes highlighter.",
      path = "highlighter_alignment",
      type = "selection",
      default = "left",
      values = {
        {"Left", "left"},
        {"Right", "right"}
      }
    }
  }
}, config.plugins.smc)

---@class plugins.scm.filechange : plugins.scm.backend.filechange
---@field color renderer.color?
---@field text string?

---@class plugins.scm
local scm = {}

---Backends shipped with the plugin.
---@type plugins.scm.backend[]
local BACKENDS = { Git(), Fossil() }

---List of loaded projects current branch.
---@type table<string, string>
local BRANCHES = {}

---List of loaded projects current stats.
---@type table<string, plugins.scm.backend.stats>
local STATS = {}

---List of loaded project changes.
---@type table<string,table<string,plugins.scm.filechange>>
local CHANGES = {}

---Opened projects SCM backends list
---@type table<string,plugins.scm.backend>
local PROJECTS = {}
setmetatable(PROJECTS, {
  __index = function(t, k)
    if k == nil then return nil end
    local v = rawget(t, k)
    if v == nil then
      for _, backend in ipairs(BACKENDS) do
        if backend:detect(k) then
          v = backend
          backend:get_branch(k, function(branch)
            BRANCHES[k] = branch
            backend:get_stats(k, function(stats) STATS[k] = stats end)
          end)
          if backend.name == "Fossil" then
            table.insert(config.ignore_files, "%-shm$")
            table.insert(config.ignore_files, "%-wal$")
          end
          rawset(t, k, v)
        end
      end
    end
    return v
  end
})

--------------------------------------------------------------------------------
-- Helper functions
--------------------------------------------------------------------------------
---@param doc core.doc
local function update_doc_diff(doc)
  if doc.abs_filename then
    local project_dir = util.get_file_project_dir(doc.abs_filename)
    if project_dir and PROJECTS[project_dir] then
      local backend = PROJECTS[project_dir]
      backend:get_file_diff(doc.abs_filename, project_dir, function(diff)
        if diff and diff ~= "" then
          local parsed_diff = changes.parse(diff)
          doc.scm_diff = nil
          for _, _ in pairs(parsed_diff) do
            doc.scm_diff = parsed_diff
            break
          end
        else
          doc.scm_diff = nil
        end
      end)
      return
    end
  end
  doc.scm_diff = nil
end

---@param path string
---@param nonblocking? boolean
local function update_doc_status(path, nonblocking)
  local project_dir = util.get_file_project_dir(path)
  local backend = PROJECTS[project_dir]
  if backend then
    if not nonblocking then backend:set_blocking_mode(true) end
    backend:get_file_status(path, project_dir, function(status)
      if status and status ~= "" then
        local color
        if status == "added" then
          color = style.good
        elseif status == "edited" then
          color = style.warn
        elseif status == "renamed" then
          color = style.warn
        elseif status == "deleted" then
          color = style.error
        elseif status == "untracked" then
          color = style.dim
        end
        if color then
          if not CHANGES[project_dir] then CHANGES[project_dir] = {} end
          CHANGES[project_dir][path] = {
            path = path,
            color = color,
            status = status
          }
        else
          if CHANGES[project_dir] and CHANGES[project_dir][path] then
            CHANGES[project_dir][path] = nil
          end
        end
      end
    end)
    if not nonblocking then backend:set_blocking_mode(true) end
  end
end

--------------------------------------------------------------------------------
-- Source Control Management API
--------------------------------------------------------------------------------
---Get a file branch or current project branch if no file given.
---@param abs_filename? string
---@return string?
function scm.get_branch(abs_filename)
  local project = util.get_project_dir(abs_filename)
  return BRANCHES[project]
end

---Get current project insert and delete stats.
---@return plugins.scm.backend.stats?
function scm.get_stats()
  local project = util.get_current_project()
  return STATS[project]
end

---Get current project scm backend
---@return plugins.scm.backend?
function scm.get_backend()
  local project = util.get_current_project()
  return PROJECTS[project]
end

---@param path string
---@param is_changed? boolean Only get backend if file has changed
---@param is_tracked? boolean Only get backend if file is tracked
---@return plugins.scm.backend?
function scm.get_path_backend(path, is_changed, is_tracked)
  local project_dir = util.get_project_dir(path)
  if project_dir then
    if is_changed then
      if CHANGES[project_dir] and CHANGES[project_dir][path] then
        if is_tracked then
          if
            CHANGES[project_dir][path].status
            and
            CHANGES[project_dir][path].status == "untracked"
          then
            return nil
          end
        end
        return PROJECTS[project_dir]
      end
    else
      local backend = PROJECTS[project_dir]
      if is_tracked and backend then
        ---@type plugins.scm.backend.filestatus
        local status
        backend:set_blocking_mode(true)
        backend:get_file_status(path, project_dir, function(file_status)
          status = file_status
        end)
        backend:set_blocking_mode(false)
        if status == "untracked" then return nil end
      end
      return backend
    end
  end
  return nil
end

---@return plugins.scm.backend.filestatus
function scm.get_path_status(path)
  local backend = scm.get_path_backend(path)
  local project_dir = util.get_project_dir(path)
  if backend and project_dir then
    local status
    backend:set_blocking_mode(true)
    backend:get_file_status(path, project_dir, function(file_status)
      status = file_status
    end)
    backend:set_blocking_mode(false)
    return status
  end
  return "untracked"
end

---@return plugins.scm.filechange?
function scm.get_path_changes(path)
  local project_dir = util.get_project_dir(path)
  if CHANGES[project_dir] and CHANGES[project_dir][path] then
    return CHANGES[project_dir] and CHANGES[project_dir][path]
  end
  return nil
end

---@return boolean
function scm.is_staged(path)
  local backend = scm.get_path_backend(path)
  local project_dir = util.get_project_dir(path)
  if backend and project_dir then
    if CHANGES[project_dir] and CHANGES[project_dir][path] then
      if
        CHANGES[project_dir][path].path
        and
        CHANGES[project_dir][path].new_path
        and
        not system.get_file_info(CHANGES[project_dir][path].path)
        and
        system.get_file_info(CHANGES[project_dir][path].new_path)
      then
        path = CHANGES[project_dir][path].new_path
      else
        return CHANGES[project_dir][path].staged
      end
    end
    local staged_files
    local path_rel = common.relative_path(project_dir, path)
    backend:set_blocking_mode(true)
    backend:get_staged(project_dir, function(files)
      staged_files = files
    end)
    backend:set_blocking_mode(false)
    if staged_files[path_rel] then return true end
  end
  return false
end

---Check if the given project path is source control managed.
---@param path string
---@return boolean
function scm.is_scm_project(path)
  for _, project in ipairs(core.project_directories) do
    if path == project.name and PROJECTS[path] then
      return true
    end
  end
  return false
end

---Add a new SCM backend.
---@param backend plugins.scm.backend
function scm.register_backend(backend)
  table.insert(BACKENDS, backend)
end

---Remove an existing SCM backend.
---@param name string
function scm.unregister_backend(name)
  for i, backend in ipairs(BACKENDS) do
    if backend.name == name then
      table.remove(BACKENDS, i)
      break
    end
  end
end

---@param project_dir? string
function scm.open_diff(project_dir)
  project_dir = project_dir or util.get_current_project()
  local backend = PROJECTS[project_dir]
  if backend then
    backend:get_diff(project_dir, function(diff)
      if diff and diff ~= "" then
        local title = "[CHANGES].diff"
          ---@type plugins.scm.readdoc
          local diffdoc = ReadDoc(title, title)
          diffdoc:set_text(diff)
          core.root_view:open_doc(diffdoc)
      else
        core.warn("SCM: no changes detected.")
      end
    end)
  else
    core.warn("SCM: current project directory is not versioned.")
  end
end

function scm.open_path_diff(path)
  local project_dir = util.get_project_dir(path)
  local backend = PROJECTS[project_dir]
  if backend then
    local path_rel = common.relative_path(project_dir, path)
    backend:get_file_diff(path, project_dir, function(diff)
      if diff and diff ~= "" then
        local title = string.format("%s.diff", path_rel)
        ---@type plugins.scm.readdoc
        local diffdoc = ReadDoc(title, title)
        diffdoc:set_text(diff)
        core.root_view:open_doc(diffdoc)
      else
        local info = system.get_file_info(path)
        if info and info.type == "file" then
          core.warn("SCM: seems like the file is untracked.")
        else
          core.warn("SCM: seems like the path only contains untracked files.")
        end
      end
    end)
  end
end

---@param project_dir? string
function scm.open_project_status(project_dir)
  project_dir = project_dir or util.get_current_project()
  local backend = PROJECTS[project_dir]
  if backend then
    backend:get_status(project_dir, function(status)
      if status and status ~= "" then
        local title = "Project Status"
          ---@type plugins.scm.readdoc
          local doc = ReadDoc(title, title)
          doc:set_text(status)
          core.root_view:open_doc(doc)
      else
        core.warn("SCM: no status to report.")
      end
    end)
  else
    core.warn("SCM: current project directory is not versioned.")
  end
end

---@param project_dir string
function scm.pull(project_dir)
  local backend = PROJECTS[project_dir]
  if backend then
    backend:pull(project_dir, function(success, errmsg)
      if success then
        core.log("SCM: pulled latest changes for '%s'", project_dir)
      else
        core.error("SCM: failed to pull '%s', %s", project_dir, errmsg)
      end
    end)
  end
end

---@param path string
function scm.revert_file(path)
  local project_dir = util.get_project_dir(path)
  local backend = PROJECTS[project_dir]
  if project_dir and backend then
    local path_rel = common.relative_path(project_dir, path)
    MessageBox.warning(
      "SCM Restore File",
      {
        "Do you really want to revert local changes?\n\n",
        "File: " .. path_rel
      },
      function(_, button_id)
        if button_id == 1 then
          backend:revert_file(path, project_dir, function(success, errmsg)
            if success then
              core.log("SCM: file '%s' changes reverted", path_rel)
              update_doc_status(path)
              util.reload_doc(path)
            else
              core.error("SCM: failed reverting '%s', %s", path_rel, errmsg)
            end
          end)
        end
      end,
      MessageBox.BUTTONS_YES_NO
    )
  end
end

---@param path string
function scm.add_path(path)
  local project_dir = util.get_project_dir(path)
  local backend = PROJECTS[project_dir]
  if project_dir and backend then
    local path_rel = common.relative_path(project_dir, path)
    backend:add_path(path, project_dir, function(success, errmsg)
      if success then
        core.log("SCM: file '%s' added", path_rel)
        update_doc_status(path)
      else
        core.error("SCM: failed adding '%s', %s", path_rel, errmsg)
      end
    end)
  end
end

---@param path string
function scm.remove_path(path)
  local project_dir = util.get_project_dir(path)
  local backend = PROJECTS[project_dir]
  if project_dir and backend then
    local path_rel = common.relative_path(project_dir, path)
    backend:remove_path(path, project_dir, function(success, errmsg)
      if success then
        core.log("SCM: file '%s' removed", path_rel)
        update_doc_status(path)
      else
        core.error("SCM: failed removing '%s', %s", path_rel, errmsg)
      end
    end)
  end
end

---@field from string
---@field to string
---@field callback fun(oldname:string, newname:string):any
function scm.move_path(from, to, callback)
  local project_dir = util.get_project_dir(from)
  local backend = PROJECTS[project_dir]
  local moved = false
  if
    backend and common.path_belongs_to(from, project_dir)
    and
    common.path_belongs_to(to, project_dir)
  then
    local from_rel = common.relative_path(project_dir, from)
    local to_rel = common.relative_path(project_dir, to)
    backend:set_blocking_mode(true)
    backend:move_path(from, to, project_dir, function(success, errmsg)
      if success then
        core.log("SCM: file '%s' moved to '%s'", from_rel, to_rel)
        update_doc_status(to, true)
      else
        core.error(
          "SCM: failed moving '%s' to '%s' with: %s",
          from_rel, to_rel, errmsg
        )
      end
    end)
    backend:set_blocking_mode(false)
  end
  if system.get_file_info(to) then
    return true
  end
  return callback(from, to)
end

---@param path string
function scm.stage_file(path)
  local project_dir = util.get_project_dir(path)
  local backend = PROJECTS[project_dir]
  if project_dir and backend and backend:has_staging() then
    local path_rel = common.relative_path(project_dir, path)
    backend:stage_file(path, project_dir, function(success, errmsg)
      if success then
        core.log("SCM: file '%s' staged", path_rel)
        update_doc_status(path)
      else
        core.error("SCM: failed staging '%s', %s", path_rel, errmsg)
      end
    end)
  end
end

---@param path string
function scm.unstage_file(path)
  local project_dir = util.get_project_dir(path)
  local backend = PROJECTS[project_dir]
  if project_dir and backend and backend:has_staging() then
    local path_rel = common.relative_path(project_dir, path)
    backend:unstage_file(path, project_dir, function(success, errmsg)
      if success then
        core.log("SCM: file '%s' unstaged", path_rel)
        update_doc_status(path)
      else
        core.error("SCM: failed unstaging '%s', %s", path_rel, errmsg)
      end
    end)
  end
end

---Go to next change in a file.
---@param doc? core.doc
function scm.next_change(doc)
  doc = doc or util.get_current_doc()
	if not doc or not doc.scm_diff then return end
	local line, col = doc:get_selection()

	while doc.scm_diff[line] do
		line = line + 1
	end

	while line < #doc.lines do
		if doc.scm_diff[line] then
			doc:set_selection(line, col, line, col)
			return
		end
		line = line + 1
	end
end

---Go to previous change in a file.
---@param doc? core.doc
function scm.previous_change(doc)
	doc = doc or util.get_current_doc()
	if not doc or not doc.scm_diff then return end
	local line, col = doc:get_selection()

	while doc.scm_diff[line] do
		line = line - 1
	end

	while line > 0 do
		if doc.scm_diff[line] then
			doc:set_selection(line, col, line, col)
			return
		end
		line = line - 1
	end
end

---Update the SCM status of all open projects.
function scm.update()
  for project_dir, project_backend in pairs(PROJECTS) do
    project_backend:get_branch(project_dir, function(branch, cached)
      if not cached then BRANCHES[project_dir] = branch end

      project_backend:get_stats(project_dir, function(stats, cached)
        if not cached then STATS[project_dir] = stats end

        project_backend:get_changes(project_dir, function(filechanges, cached)
          if cached then return end
          local changed_files = {}
          for i, change in ipairs(filechanges) do
            local color = style.modified
            if change.status == "added" then
              color = style.good
            elseif change.status == "edited" then
              color = style.warn
            elseif change.status == "renamed" then
              color = style.warn
            elseif change.status == "deleted" then
              color = style.error
            elseif change.status == "untracked" then
              color = style.dim
            end
            change.color = color
            local path = ""
            if change.new_path then
              change.text = common.basename(change.path)
                .. " -> "
                .. common.basename(change.new_path)
              changed_files[change.new_path] = change
              path = common.dirname(change.new_path)
            else
              changed_files[change.path] = change
              path = common.dirname(change.path)
            end
            while path do
              if #path < #project_dir then break end
              changed_files[path] = { color = style.modified }
              path = common.dirname(path)
            end
            if i % 10 == 0 then
              coroutine.yield()
            end
          end
          CHANGES[project_dir] = changed_files
        end)
      end)
    end)
  end
end

--------------------------------------------------------------------------------
-- Keep the project branch, changes and stats updated
--------------------------------------------------------------------------------
core.add_thread(function()
  while true do
    scm.update()
    coroutine.yield(1)
  end
end)

--------------------------------------------------------------------------------
-- Override Doc to register diff changes
--------------------------------------------------------------------------------
local doc_save = Doc.save
function Doc:save(...) doc_save(self, ...) update_doc_diff(self) end

local doc_new = Doc.new
function Doc:new(...) doc_new(self, ...) update_doc_diff(self) end

local doc_load = Doc.load
function Doc:load(...) doc_load(self, ...) update_doc_diff(self) end

local doc_raw_insert = Doc.raw_insert
function Doc:raw_insert(line, col, text, undo_stack, time)
  doc_raw_insert(self, line, col, text, undo_stack, time)
  local diffs = self.scm_diff or {}
  if diffs[line] ~= "addition" then
    diffs[line] = "modification"
  end
  local count = line
  for _ in (text .. "\n"):gmatch("(.-)\n") do
    if count ~= line then
      diffs[count] = "addition"
    end
    count = count + 1
  end
  self.scm_diff = diffs
end

local doc_raw_remove = Doc.raw_remove
function Doc:raw_remove(line1, col1, line2, col2, undo_stack, time)
  doc_raw_remove(self, line1, col1, line2, col2, undo_stack, time)
  local diffs = self.scm_diff or {}
  if line1 ~= line2 then
    local minline = math.min(line1, line2)
    local maxline = math.max(line1, line2)
    for line = minline+1, maxline do
      diffs[line] = "deletion"
    end
  else
    diffs[line1] = "modification"
  end
  self.scm_diff = diffs
end

--------------------------------------------------------------------------------
-- Override DocView to draw changes on gutter
--------------------------------------------------------------------------------
local DIFF_WIDTH = 3
local docview_draw_line_gutter = DocView.draw_line_gutter
local docview_get_gutter_width = DocView.get_gutter_width
function DocView:draw_line_gutter(line, x, y, width)
  if not self.doc or not self.doc.scm_diff or not config.plugins.smc.highlighter then
    return docview_draw_line_gutter(self, line, x, y, width)
  end

  local lh = self:get_line_height()
  local gw, gpad = docview_get_gutter_width(self)
  local diff_type = self.doc.scm_diff[line]

  local align = config.plugins.smc.highlighter_alignment

  if align == "right" then
    docview_draw_line_gutter(self, line, x, y, gpad and gw - gpad or gw)
  else
    local tox = style.padding.x * DIFF_WIDTH / 12
    docview_draw_line_gutter(self, line, x + tox, y, gpad and gw - gpad or gw)
  end

  if diff_type == nil then return end

  local color = style.good
  if diff_type == "deletion" then
    color = style.error
  elseif diff_type == "modification" then
    color = style.warn
  end

  local colw = self:get_font():get_width(#self.doc.lines)

  -- add margin in between highlight and text
  if align == "right" then
    if colw + style.padding.x * 2 >= gw then
      x = x + style.padding.x * 1.5 + colw
    else
      x = x + gw - style.padding.x * 2 + (style.padding.x * DIFF_WIDTH / 12)
    end
  else
    local spacing = (style.padding.x * DIFF_WIDTH / 12)
    if colw + style.padding.x * 2 >= gw then
      x = x + gw + spacing - colw - gpad
    else
      x = x + math.max(gw, colw) - (colw) - math.min(colw, gw) - spacing
    end
  end

  local yoffset = self:get_line_text_y_offset()
  if diff_type ~= "deletion" then
    renderer.draw_rect(x, y + yoffset, DIFF_WIDTH, self:get_line_height(), color)
    return
  end
  renderer.draw_rect(x - DIFF_WIDTH * 2, y + yoffset, DIFF_WIDTH * 4, 2, color)
  return lh
end

function DocView:get_gutter_width()
  if not self.doc or not self.doc.scm_diff or not config.plugins.smc.highlighter then
    return docview_get_gutter_width(self)
  end
  return docview_get_gutter_width(self)
    + style.padding.x * DIFF_WIDTH / 12
end

--------------------------------------------------------------------------------
-- Override rename to execute it on the SCM
--------------------------------------------------------------------------------
local os_rename = os.rename
function os.rename(oldname, newname)
  return scm.move_path(oldname, newname, os_rename)
end

--------------------------------------------------------------------------------
-- StatusBar Item to show current branch and stats
--------------------------------------------------------------------------------
local scm_status_item = core.status_view:add_item({
  name = "status:scm",
  alignment = StatusView.Item.RIGHT,
  get_item = function()
    local project = util.get_current_project()

    if
      not PROJECTS[project]
      or
      not BRANCHES[project] or not STATS[project]
    then
      return {}
    end

    local bcolor = (STATS[project].inserts ~= 0 or STATS[project].deletes ~= 0)
      and style.accent or style.text
    local icolor = STATS[project].inserts ~= 0 and style.accent or style.text
    local dcolor = STATS[project].deletes ~= 0 and style.accent or style.text

    return {
      bcolor, BRANCHES[project],
      style.dim, "  ",
      icolor, "+", STATS[project].inserts,
      style.dim, " / ",
      dcolor, "-", STATS[project].deletes,
    }
  end,
  position = -1,
  tooltip = "current branch",
  separator = core.status_view.separator2
})

scm_status_item.on_click = function(button)
  if button == "right" then
    command.perform "scm:global-diff"
  else
    core.command_view:set_text("Scm: ")
    command.perform "core:find-command"
  end
end

--------------------------------------------------------------------------------
-- Commands
--------------------------------------------------------------------------------
command.add(
  function()
    local valid = false
    local project_dir = nil
    local av = core.active_view
    if av and av.doc and av.doc.abs_filename then
      project_dir = util.get_project_dir(av.doc.abs_filename)
      if project_dir and PROJECTS[project_dir] then valid = true end
    end
    if not valid and PROJECTS[core.project_dir] then
      valid, project_dir = true, core.project_dir
    end
    return valid, project_dir
  end, {

  ["scm:global-diff"] = function(project_dir)
    scm.open_diff(project_dir)
  end,

  ["scm:project-status"] = function(project_dir)
    scm.open_project_status(project_dir)
  end
})

command.add(
  function()
    local doc = util.get_current_doc()
    return doc
      and scm.get_path_backend(doc.abs_filename)
      and scm.get_path_status(doc.abs_filename) == "untracked"
      , doc
  end, {

  ["scm:file-add"] = function(doc)
    ---@cast doc core.doc
    scm.add_path(doc.abs_filename)
	end
})

command.add(
  function()
    local doc = util.get_current_doc()
    return doc
      and scm.get_path_status(doc.abs_filename) == "unchanged"
      , doc
  end, {

  ["scm:file-remove"] = function(doc)
    scm.remove_path(doc.abs_filename)
  end
})

command.add(
  function()
    local doc = util.get_current_doc()
    if doc then
      local path = doc.abs_filename
      local status = scm.get_path_status(path)
      if status == "edited" and not scm.is_staged(path) then
        local backend = scm.get_path_backend(path)
        if backend and backend:has_staging() then
          return true, doc
        end
      end
    end
    return false
  end, {

  ["scm:staging-add"] = function(doc)
    scm.stage_file(doc.abs_filename)
  end
})

command.add(
  function()
    local doc = util.get_current_doc()
    if doc then
      local backend = scm.get_path_backend(doc.abs_filename)
      if backend and backend:has_staging() then
        if scm.is_staged(doc.abs_filename) then
          return true, doc
        end
      end
    end
    return false
  end, {

  ["scm:staging-remove"] = function(doc)
    scm.unstage_file(doc.abs_filename)
  end
})

command.add(
  function()
    local doc = util.get_current_doc()
    if doc then
      local path = doc.abs_filename
      local status = scm.get_path_status(path)
      local backend = scm.get_path_backend(path)
      if backend and backend:has_staging() then
        if status == "edited" and not scm.is_staged(path) then
          return true
        end
      elseif backend then
        return scm.get_path_backend(doc.abs_filename, true, true), doc
      end
    end
    return false
  end, {

  ["scm:file-revert"] = function(doc)
    scm.revert_file(doc.abs_filename)
  end,
})

command.add(
  function()
    local doc = util.get_current_doc()
    return doc
      and scm.get_path_status(doc.abs_filename) == "edited"
      and not scm.is_staged(doc.abs_filename)
      , doc
  end, {

  ["scm:file-diff"] = function(doc)
    scm.open_path_diff(doc.abs_filename)
  end
})

command.add(
  function()
    local doc = util.get_current_doc()
    if doc then
      local project_dir = util.get_file_project_dir(doc.abs_filename)
      if
        CHANGES[project_dir] and CHANGES[project_dir][doc.abs_filename]
        and
        doc.scm_diff
      then
        return true, doc
      end
    end
    return false
  end, {

	["scm:goto-previous-change"] = function(doc)
		scm.previous_change(doc)
	end,

	["scm:goto-next-change"] = function(doc)
		scm.next_change(doc)
	end,
})

--------------------------------------------------------------------------------
-- Keymaps
--------------------------------------------------------------------------------
keymap.add {
  ["ctrl+alt+["]    = "scm:goto-previous-change",
  ["ctrl+alt+]"]  = "scm:goto-next-change"
}

--------------------------------------------------------------------------------
-- Load TreeView support if the plugin is enabled
--------------------------------------------------------------------------------
require "plugins.scm.treeview"


return scm