Git panel integrated into the treeview pane: uncommitted changes with diff stats, staging, commits and history, toggled from a git-branch button in the treeview toolbar or the status bar. History is pinned to the bottom half of the panel; commit patches open with diff syntax highlighting from the already bundled language_diff. Pinned to lite-xl-gitpanel @ 0731a63 (see debian/extra/README.source).
679 lines
24 KiB
Lua
679 lines
24 KiB
Lua
-- mod-version:3
|
|
-- version: 0.3.0
|
|
-- Git panel for Lite XL: uncommitted changes, staging, commits, history.
|
|
-- Self-contained, needs only the `git` CLI in PATH.
|
|
-- Toggle with Ctrl+Shift+G, the git button in the treeview toolbar or the
|
|
-- branch name in the status bar. When the treeview plugin is present the
|
|
-- panel replaces the folder list in the left pane; otherwise it opens as
|
|
-- its own pane.
|
|
-- In the panel: click a file to open it, click a commit to view its patch.
|
|
-- Keys (panel focused): s stage · u unstage · a stage all · c commit · r refresh
|
|
|
|
local core = require "core"
|
|
local common = require "core.common"
|
|
local config = require "core.config"
|
|
local style = require "core.style"
|
|
local keymap = require "core.keymap"
|
|
local command = require "core.command"
|
|
local syntax = require "core.syntax"
|
|
local View = require "core.view"
|
|
local Doc = require "core.doc"
|
|
local StatusView = require "core.statusview"
|
|
|
|
config.plugins.gitpanel = common.merge({
|
|
history_length = 40,
|
|
}, config.plugins.gitpanel)
|
|
|
|
|
|
--------------------------------------------------------------------------------
|
|
-- Async git helpers (run inside core.add_thread coroutines)
|
|
--------------------------------------------------------------------------------
|
|
|
|
-- Run a command, reading incrementally so large outputs (git show) cannot
|
|
-- deadlock on the pipe buffer. Must be called from a coroutine.
|
|
local function exec(args)
|
|
local proc = process.start(args)
|
|
if not proc then return "" end
|
|
local chunks = {}
|
|
while proc:running() do
|
|
local chunk = proc:read_stdout(65536)
|
|
if chunk and #chunk > 0 then
|
|
chunks[#chunks + 1] = chunk
|
|
else
|
|
coroutine.yield(0.05)
|
|
end
|
|
end
|
|
while true do
|
|
local chunk = proc:read_stdout(65536)
|
|
if chunk and #chunk > 0 then chunks[#chunks + 1] = chunk else break end
|
|
end
|
|
return table.concat(chunks)
|
|
end
|
|
|
|
local state = { branch = nil, changes = {}, history = {}, clean = false, loading = false }
|
|
|
|
local function parse_numstat(out, map)
|
|
for line in (out .. "\n"):gmatch("([^\n]*)\n") do
|
|
local ins, del, path = line:match("^(%d+)%s+(%d+)%s+(.+)$")
|
|
if path then
|
|
path = path:match("^.- ->%s*(.+)$") or path -- renames: keep new path
|
|
map[path] = { n = tonumber(ins) or 0, d = tonumber(del) or 0 }
|
|
end
|
|
end
|
|
end
|
|
|
|
local function refresh(callback)
|
|
if state.loading then return end
|
|
state.loading = true
|
|
core.add_thread(function()
|
|
local branch, changes, history
|
|
-- pcall so a failed git run (spawn error, ...) can never leave
|
|
-- state.loading stuck and silently kill every later refresh
|
|
local ok, err = pcall(function()
|
|
branch, changes, history = nil, {}, {}
|
|
if system.get_file_info(core.project_dir .. PATHSEP .. ".git") then
|
|
local work, staged = {}, {}
|
|
parse_numstat(exec { "git", "diff", "--numstat" }, work)
|
|
parse_numstat(exec { "git", "diff", "--cached", "--numstat" }, staged)
|
|
|
|
local status = exec { "git", "status", "--porcelain=v1", "-b" }
|
|
local first = true
|
|
for line in (status .. "\n"):gmatch("([^\n]*)\n") do
|
|
if first and line:match("^## ") then
|
|
branch = line:sub(4):match("^(.-)%.%.%.") or line:sub(4):match("^(%S+)")
|
|
first = false
|
|
elseif line ~= "" then
|
|
local xy, path = line:match("^(..) (.+)$")
|
|
if xy then
|
|
path = path:match("^(.-)%s+->%s+(.+)$") or path
|
|
local x, y = xy:sub(1, 1), xy:sub(2, 2)
|
|
local nums = (x ~= " ") and staged[path] or work[path]
|
|
changes[#changes + 1] = {
|
|
xy = xy,
|
|
st = xy == "??" and "??" or (x ~= " " and x or y),
|
|
path = path,
|
|
staged = x ~= " " and x ~= "?",
|
|
untracked = xy == "??",
|
|
plus = nums and nums.n or nil,
|
|
minus = nums and nums.d or nil,
|
|
}
|
|
end
|
|
end
|
|
end
|
|
|
|
local log = exec { "git", "log", "-n", tostring(config.plugins.gitpanel.history_length),
|
|
"--pretty=format:%h %s" }
|
|
for line in (log .. "\n"):gmatch("([^\n]*)\n") do
|
|
local hash, subject = line:match("^(%S+) (.*)$")
|
|
if hash then history[#history + 1] = { hash = hash, subject = subject } end
|
|
end
|
|
end
|
|
end)
|
|
state.loading = false
|
|
if ok then
|
|
state.branch, state.changes, state.history = branch, changes, history
|
|
state.clean = branch ~= nil and #changes == 0
|
|
else
|
|
core.error("git-panel: refresh failed: %s", tostring(err))
|
|
end
|
|
if callback and ok then callback() end
|
|
end)
|
|
end
|
|
|
|
|
|
--------------------------------------------------------------------------------
|
|
-- Panel view
|
|
--------------------------------------------------------------------------------
|
|
|
|
local function row_height()
|
|
return style.font:get_height() + common.round(4 * SCALE)
|
|
end
|
|
|
|
local function status_color(st)
|
|
if st == "M" or st == "T" or st == "R" or st == "C" then
|
|
return style.gitstatus_modification or style.warn
|
|
end
|
|
if st == "A" or st == "??" then return style.gitstatus_addition or style.good end
|
|
if st == "D" or st == "U" then return style.gitstatus_deletion or style.error end
|
|
return style.text
|
|
end
|
|
|
|
local GitPanelView = View:extend()
|
|
|
|
local show_commit -- forward declaration: called from on_mouse_pressed below
|
|
|
|
-- Set once the treeview plugin is available (it loads after us); when nil
|
|
-- the panel falls back to opening as its own split pane.
|
|
local treeview = nil
|
|
|
|
function GitPanelView:new()
|
|
GitPanelView.super.new(self)
|
|
self.hover = nil -- { section = "changes"|"history", idx }
|
|
self.mouse = { x = 0, y = 0 }
|
|
self.changes_offset = 0
|
|
self.history_offset = 0
|
|
self.changes_rows = {}
|
|
self.history_rows = {}
|
|
self.changes_visible = 1
|
|
self.history_visible = 1
|
|
self.mid_y = 0
|
|
end
|
|
|
|
function GitPanelView:get_name()
|
|
return "Git"
|
|
end
|
|
|
|
-- While embedded in the treeview's pane, the pane width follows our size.x
|
|
-- (the node is locked along x). Animate toward the treeview's target so
|
|
-- divider drags resize us, and the folder view inherits the final width.
|
|
function GitPanelView:set_target_size(axis, value)
|
|
if axis == "x" and treeview then
|
|
treeview:set_target_size(axis, value)
|
|
return true
|
|
end
|
|
end
|
|
|
|
function GitPanelView:update()
|
|
if treeview then
|
|
self:move_towards(self.size, "x", treeview.target_size)
|
|
end
|
|
GitPanelView.super.update(self)
|
|
end
|
|
|
|
-- The panel is split in two independently scrolling sections: the changes list
|
|
-- on the top half and the history pinned to the bottom half.
|
|
function GitPanelView:build_changes_rows()
|
|
local rows = {}
|
|
for _, e in ipairs(state.changes) do rows[#rows + 1] = { type = "file", entry = e } end
|
|
if #rows == 0 then
|
|
rows[#rows + 1] = {
|
|
type = "empty",
|
|
label = state.loading and "refreshing..."
|
|
or (state.branch and "working tree clean" or "not a git repository"),
|
|
}
|
|
end
|
|
return rows
|
|
end
|
|
|
|
function GitPanelView:build_history_rows()
|
|
local rows = {}
|
|
for _, c in ipairs(state.history) do rows[#rows + 1] = { type = "commit", entry = c } end
|
|
if #rows == 0 then
|
|
rows[#rows + 1] = {
|
|
type = "empty",
|
|
label = state.loading and "refreshing..." or "no commits yet",
|
|
}
|
|
end
|
|
return rows
|
|
end
|
|
|
|
local function draw_row_hover(x, y, w, rh, hovered)
|
|
if hovered then
|
|
local c = { table.unpack(style.treeview_hover_bg or style.line_highlight) }
|
|
c[4] = 120
|
|
renderer.draw_rect(x, y, w, rh, c)
|
|
end
|
|
end
|
|
|
|
local function draw_file_row(x, y, w, rh, e, pad)
|
|
common.draw_text(style.font, status_color(e.st), e.st, nil, x + pad, y, 0, rh)
|
|
local tx = x + pad + style.font:get_width(e.st) + common.round(6 * SCALE)
|
|
common.draw_text(style.font, style.treeview_text or style.text, e.path, nil, tx, y, 0, rh)
|
|
if e.plus or e.minus then
|
|
local label = string.format("+%d -%d", e.plus or 0, e.minus or 0)
|
|
common.draw_text(style.font, style.dim, label, nil,
|
|
x + w - style.font:get_width(label) - pad, y, 0, rh)
|
|
end
|
|
end
|
|
|
|
local function draw_commit_row(x, y, rh, e, pad)
|
|
common.draw_text(style.font, style.dim, e.hash, nil, x + pad, y, 0, rh)
|
|
local tx = x + pad + style.font:get_width("0000000") + common.round(6 * SCALE)
|
|
common.draw_text(style.font, style.treeview_text or style.text, e.subject, nil, tx, y, 0, rh)
|
|
end
|
|
|
|
function GitPanelView:draw()
|
|
self:draw_background(style.background2)
|
|
local x, y = self.position.x, self.position.y
|
|
local w, h = self.size.x, self.size.y
|
|
local rh = row_height()
|
|
local pad = common.round(5 * SCALE)
|
|
local font_h = style.font:get_height()
|
|
|
|
-- history is pinned to the bottom half; snap the divider to a row boundary
|
|
local footer_h = font_h + common.round(4 * SCALE)
|
|
local half_rows = math.max(1, math.floor((h - footer_h) / 2 / rh))
|
|
local mid = y + half_rows * rh
|
|
self.mid_y = mid
|
|
|
|
self.changes_rows = self:build_changes_rows()
|
|
self.history_rows = self:build_history_rows()
|
|
self.changes_visible = math.max(1, half_rows - 2) -- branch + section headers
|
|
self.history_visible = math.max(1, half_rows - 1) -- section header
|
|
self.changes_offset = common.clamp(self.changes_offset,
|
|
0, math.max(0, #self.changes_rows - self.changes_visible))
|
|
self.history_offset = common.clamp(self.history_offset,
|
|
0, math.max(0, #self.history_rows - self.history_visible))
|
|
|
|
if state.branch then
|
|
common.draw_text(style.font, style.accent, "branch " .. state.branch, nil, x + pad, y, 0, rh)
|
|
else
|
|
common.draw_text(style.font, style.dim,
|
|
state.loading and "loading..." or "not a git repository", nil, x + pad, y, 0, rh)
|
|
end
|
|
|
|
common.draw_text(style.font, style.accent,
|
|
string.format("changes (%d)", #state.changes), nil, x + pad, y + rh, 0, rh)
|
|
local cy = y + 2 * rh
|
|
core.push_clip_rect(x, cy, w, mid - cy)
|
|
for i = self.changes_offset + 1,
|
|
math.min(#self.changes_rows, self.changes_offset + self.changes_visible) do
|
|
local row = self.changes_rows[i]
|
|
local ry = cy + (i - 1 - self.changes_offset) * rh
|
|
draw_row_hover(x, ry, w, rh,
|
|
self.hover and self.hover.section == "changes" and self.hover.idx == i)
|
|
if row.type == "file" then
|
|
draw_file_row(x, ry, w, rh, row.entry, pad)
|
|
else
|
|
common.draw_text(style.font, style.dim, row.label, nil, x + pad, ry, 0, rh)
|
|
end
|
|
end
|
|
core.pop_clip_rect()
|
|
|
|
renderer.draw_rect(x, mid, w, SCALE, style.divider or style.dim)
|
|
|
|
common.draw_text(style.font, style.accent,
|
|
string.format("history (%d)", #state.history), nil, x + pad, mid, 0, rh)
|
|
local hy = mid + rh
|
|
core.push_clip_rect(x, hy, w, y + h - footer_h - hy)
|
|
for i = self.history_offset + 1,
|
|
math.min(#self.history_rows, self.history_offset + self.history_visible) do
|
|
local row = self.history_rows[i]
|
|
local ry = hy + (i - 1 - self.history_offset) * rh
|
|
draw_row_hover(x, ry, w, rh,
|
|
self.hover and self.hover.section == "history" and self.hover.idx == i)
|
|
if row.type == "commit" then
|
|
draw_commit_row(x, ry, rh, row.entry, pad)
|
|
else
|
|
common.draw_text(style.font, style.dim, row.label, nil, x + pad, ry, 0, rh)
|
|
end
|
|
end
|
|
core.pop_clip_rect()
|
|
|
|
local hint = "s stage - u unstage - a all - c commit - r refresh"
|
|
common.draw_text(style.font, style.dim, hint, nil, x + pad,
|
|
y + h - font_h - common.round(2 * SCALE), 0, font_h)
|
|
end
|
|
|
|
function GitPanelView:on_mouse_moved(px, py, ...)
|
|
GitPanelView.super.on_mouse_moved(self, px, py, ...)
|
|
self.mouse.x, self.mouse.y = px, py
|
|
self.hover = nil
|
|
local rh = row_height()
|
|
local top_start = self.position.y + 2 * rh
|
|
local hist_start = self.mid_y + rh
|
|
if py >= top_start and py < self.mid_y then
|
|
local idx = math.floor((py - top_start) / rh) + self.changes_offset + 1
|
|
if self.changes_rows[idx] then self.hover = { section = "changes", idx = idx } end
|
|
elseif py >= hist_start then
|
|
local idx = math.floor((py - hist_start) / rh) + self.history_offset + 1
|
|
if self.history_rows[idx] then self.hover = { section = "history", idx = idx } end
|
|
end
|
|
end
|
|
|
|
function GitPanelView:on_mouse_pressed(button, px, py_, clicks)
|
|
GitPanelView.super.on_mouse_pressed(self, button, px, py_, clicks)
|
|
local hov = self.hover
|
|
if hov then
|
|
local row = hov.section == "changes"
|
|
and self.changes_rows[hov.idx] or self.history_rows[hov.idx]
|
|
if row and row.type == "file" and row.entry.st ~= "D" then
|
|
-- deleted files can't be opened
|
|
core.root_view:open_doc(core.open_doc(core.project_dir .. PATHSEP .. row.entry.path))
|
|
elseif row and row.type == "commit" then
|
|
show_commit(row.entry.hash, row.entry.subject)
|
|
end
|
|
end
|
|
return true
|
|
end
|
|
|
|
function GitPanelView:on_mouse_left()
|
|
GitPanelView.super.on_mouse_left(self)
|
|
self.hover = nil
|
|
end
|
|
|
|
function GitPanelView:on_mouse_wheel(dy)
|
|
GitPanelView.super.on_mouse_wheel(self, dy)
|
|
local in_history = self.mouse.y >= self.mid_y
|
|
local rows_n = in_history and #self.history_rows or #self.changes_rows
|
|
local visible = in_history and self.history_visible or self.changes_visible
|
|
local offset = in_history and self.history_offset or self.changes_offset
|
|
offset = common.clamp(offset - dy * 3, 0, math.max(0, rows_n - visible))
|
|
if in_history then self.history_offset = offset else self.changes_offset = offset end
|
|
end
|
|
|
|
|
|
--------------------------------------------------------------------------------
|
|
-- Actions
|
|
--------------------------------------------------------------------------------
|
|
|
|
local panel = nil -- the open GitPanelView instance, if any
|
|
|
|
local function open_scratch(name, text, syn)
|
|
local doc = Doc()
|
|
doc:reset()
|
|
local lines = {}
|
|
for line in (text .. "\n"):gmatch("([^\n]*)\n") do lines[#lines + 1] = line .. "\n" end
|
|
if #lines == 0 then lines[1] = "\n" end
|
|
doc.lines = lines
|
|
doc:set_filename(name, nil)
|
|
doc:reset_syntax()
|
|
if syn then doc.syntax = syn end -- else it stays whatever reset_syntax picked
|
|
core.root_view:open_doc(doc)
|
|
end
|
|
|
|
-- Commit diffs are highlighted with the official language_diff plugin when it
|
|
-- is installed (it registers its syntax under the name "Diff"); otherwise this
|
|
-- fallback covers `git show` output with the same colors.
|
|
style.syntax.diff_add = style.syntax.diff_add or { common.color "#72b886" }
|
|
style.syntax.diff_del = style.syntax.diff_del or { common.color "#f36161" }
|
|
|
|
local diff_syntax_fallback = syntax.add({
|
|
name = "Git Diff (gitpanel)",
|
|
patterns = {
|
|
{ pattern = "^commit %x+", type = "function" },
|
|
{ pattern = "^[%a%-]+: ", type = "keyword" },
|
|
{ pattern = "^diff %-%-git .*", type = "function" },
|
|
{ pattern = "^index .*", type = "comment" },
|
|
{ pattern = "^new .*", type = "comment" },
|
|
{ pattern = "^deleted .*", type = "comment" },
|
|
{ pattern = "^similarity index .*", type = "comment" },
|
|
{ pattern = "^rename .*", type = "comment" },
|
|
{ pattern = "^@@ .-@@", type = "number" },
|
|
{ pattern = "^%-%-%- .*", type = "keyword" },
|
|
{ pattern = "^%+%+%+ .*", type = "keyword" },
|
|
{ pattern = "^%-.*", type = "diff_del" },
|
|
{ pattern = "^%+.*", type = "diff_add" },
|
|
{ pattern = "^ %d+ files? changed.*", type = "function" },
|
|
},
|
|
symbols = {},
|
|
})
|
|
|
|
local function get_diff_syntax()
|
|
for _, s in ipairs(syntax.items) do
|
|
if s.name == "Diff" then return s end
|
|
end
|
|
return diff_syntax_fallback
|
|
end
|
|
|
|
function show_commit(hash, subject)
|
|
core.add_thread(function()
|
|
open_scratch("[git] " .. hash .. " " .. (subject or ""),
|
|
exec { "git", "show", "--no-color", hash }, get_diff_syntax())
|
|
end)
|
|
end
|
|
local function hovered_file_entry()
|
|
if not (panel and panel.hover and panel.hover.section == "changes") then return nil end
|
|
local row = panel.changes_rows[panel.hover.idx]
|
|
return row and row.type == "file" and row.entry or nil
|
|
end
|
|
|
|
local function run_on_entry(args_for)
|
|
local e = hovered_file_entry()
|
|
if not e or e.untracked and args_for == "unstage" then return end
|
|
local args
|
|
if args_for == "stage" then
|
|
args = { "git", "add", "--", e.path }
|
|
else
|
|
args = { "git", "reset", "-q", "HEAD", "--", e.path }
|
|
end
|
|
core.add_thread(function()
|
|
exec(args)
|
|
refresh()
|
|
end)
|
|
end
|
|
|
|
local function panel_active()
|
|
return panel ~= nil and core.active_view == panel
|
|
end
|
|
|
|
|
|
--------------------------------------------------------------------------------
|
|
-- Treeview integration: the panel replaces the folder list in the left pane,
|
|
-- toggled from the toolbar's git button. The treeview plugin loads after us,
|
|
-- so require it lazily. The treeview's leaf node is locked (no tab bar is
|
|
-- drawn there), which makes it a clean container to swap views in.
|
|
--------------------------------------------------------------------------------
|
|
|
|
local function treeview_ready()
|
|
if treeview then return true end
|
|
if config.plugins.treeview == false then return false end
|
|
local ok, tv = pcall(require, "plugins.treeview")
|
|
if ok and tv and tv.node then
|
|
treeview = tv
|
|
return true
|
|
end
|
|
return false
|
|
end
|
|
|
|
local function treeview_leaf()
|
|
return treeview.node:get_node_for_view(treeview)
|
|
end
|
|
|
|
local function pane_active()
|
|
if not (treeview and panel) then return false end
|
|
local leaf = treeview_leaf()
|
|
return leaf ~= nil and leaf.active_view == panel
|
|
end
|
|
|
|
local function pane_attach()
|
|
if not panel then panel = GitPanelView() end
|
|
local leaf = treeview_leaf()
|
|
if not leaf:get_view_idx(panel) then
|
|
panel.size.x = treeview.size.x -- else the locked pane would collapse on us
|
|
local locked = leaf.locked
|
|
leaf.locked = nil -- add_view() refuses locked nodes
|
|
leaf:add_view(panel) -- makes the panel the node's active view and focuses it
|
|
leaf.locked = locked
|
|
else
|
|
leaf:set_active_view(panel)
|
|
end
|
|
refresh()
|
|
end
|
|
|
|
local function pane_detach()
|
|
local leaf = treeview_leaf()
|
|
local focused = core.active_view == panel
|
|
leaf.active_view = treeview -- route the pane back without stealing focus
|
|
leaf:remove_view(core.root_view.root_node, panel)
|
|
panel:on_mouse_left()
|
|
if focused then core.set_active_view(treeview) end
|
|
end
|
|
|
|
local function open_panel()
|
|
if panel and core.root_view:get_node_for_view(panel) then
|
|
refresh()
|
|
core.set_active_view(panel)
|
|
return
|
|
end
|
|
panel = GitPanelView()
|
|
refresh()
|
|
local node = core.root_view:get_active_node()
|
|
node:split("left", panel)
|
|
core.set_active_view(panel)
|
|
end
|
|
|
|
local function close_panel()
|
|
if not panel then return end
|
|
local node = core.root_view:get_node_for_view(panel)
|
|
if node then node:close_view(core.root_view.node, panel) end
|
|
panel = nil
|
|
end
|
|
|
|
local function toggle_panel()
|
|
if treeview_ready() then
|
|
if pane_active() then pane_detach() else pane_attach() end
|
|
else
|
|
if panel_active() then close_panel() else open_panel() end
|
|
end
|
|
end
|
|
|
|
command.add(nil, {
|
|
["git-panel:toggle"] = function() toggle_panel() end,
|
|
})
|
|
|
|
command.add(panel_active, {
|
|
["git-panel:stage"] = function()
|
|
if hovered_file_entry() then run_on_entry("stage") end
|
|
end,
|
|
["git-panel:unstage"] = function()
|
|
if hovered_file_entry() then run_on_entry("unstage") end
|
|
end,
|
|
["git-panel:stage-all"] = function()
|
|
core.add_thread(function()
|
|
exec { "git", "add", "-A" }
|
|
refresh()
|
|
end)
|
|
end,
|
|
["git-panel:refresh"] = function() refresh() end,
|
|
["git-panel:commit"] = function()
|
|
local staged = 0
|
|
for _, e in ipairs(state.changes) do if e.staged then staged = staged + 1 end end
|
|
if staged == 0 then
|
|
core.error("git-panel: nothing staged, press s on changed files first")
|
|
return
|
|
end
|
|
core.command_view:enter("Commit message", function(text)
|
|
text = text:match("^%s*(.-)%s*$")
|
|
if text == "" then
|
|
core.error("git-panel: empty commit message")
|
|
return
|
|
end
|
|
core.add_thread(function()
|
|
exec { "git", "commit", "-m", text }
|
|
refresh(function()
|
|
local left = 0
|
|
for _, e in ipairs(state.changes) do if e.staged then left = left + 1 end end
|
|
if left == 0 then
|
|
core.log("git-panel: committed: " .. text)
|
|
else
|
|
core.error("git-panel: commit failed, staged files remain")
|
|
end
|
|
end)
|
|
end)
|
|
end)
|
|
end,
|
|
})
|
|
|
|
keymap.add {
|
|
["ctrl+shift+g"] = "git-panel:toggle",
|
|
}
|
|
|
|
keymap.add {
|
|
["s"] = "git-panel:stage",
|
|
["u"] = "git-panel:unstage",
|
|
["a"] = "git-panel:stage-all",
|
|
["c"] = "git-panel:commit",
|
|
["r"] = "git-panel:refresh",
|
|
}
|
|
|
|
|
|
--------------------------------------------------------------------------------
|
|
-- Toolbar button: a git-branch icon next to the treeview toolbar buttons,
|
|
-- highlighted while the panel replaces the folder list. treeview loads after
|
|
-- us (plugins load alphabetically), so wait for it in a thread.
|
|
--------------------------------------------------------------------------------
|
|
|
|
-- VS Code-style git-branch icon (vscode-codicons, MIT), rasterized offline to
|
|
-- opaque rect runs on a 34x34 grid; soft = partial-coverage edge cells
|
|
local GIT_ICON_GRID = 34
|
|
local GIT_ICON = {
|
|
{8,0,3,1},{6,1,7,1},{5,2,3,1},{11,2,3,1},{5,3,2,1},{12,3,2,1},{5,4,1,1},
|
|
{13,4,2,3},{4,5,2,1},{5,6,1,1},{5,7,2,1},{12,7,2,1},{21,7,6,1},{6,8,2,1},
|
|
{11,8,3,1},{20,8,3,1},{26,8,2,1},{7,9,6,1},{20,9,2,1},{27,9,2,1},{9,10,2,10},
|
|
{19,10,2,3},{28,10,1,1},{28,11,2,1},{28,12,1,1},{20,13,2,1},{27,13,2,1},
|
|
{20,14,3,1},{26,14,3,1},{21,15,7,1},{23,16,3,1},{23,17,2,1},{22,18,3,1},
|
|
{12,19,12,1},{9,20,14,1},{9,21,3,1},{9,22,2,2},{7,24,6,1},{6,25,2,1},
|
|
{11,25,3,1},{5,26,2,1},{12,26,2,1},{5,27,1,1},{13,27,2,3},{4,28,2,1},
|
|
{5,29,1,1},{5,30,2,1},{12,30,2,1},{5,31,3,1},{11,31,3,1},{6,32,7,1},{8,33,3,1}
|
|
}
|
|
local GIT_ICON_SOFT = {
|
|
{7,0,5,1},{5,1,9,1},{5,2,4,1},{10,2,4,1},{4,3,3,4},{12,3,3,5},{22,6,5,1},
|
|
{4,7,4,1},{21,7,7,1},{5,8,9,1},{20,8,9,1},{6,9,7,1},{19,9,3,5},{26,9,4,1},
|
|
{7,10,5,1},{27,10,3,4},{8,11,3,8},{20,14,4,1},{25,14,4,1},{20,15,9,1},
|
|
{22,16,5,1},{23,17,3,1},{21,18,4,1},{8,19,16,1},{8,20,15,1},{8,21,13,1},
|
|
{8,22,4,1},{7,23,5,1},{6,24,7,1},{5,25,9,1},{4,26,4,1},{12,26,3,5},
|
|
{4,27,3,4},{5,31,4,1},{10,31,4,1},{5,32,9,1},{7,33,5,1}
|
|
}
|
|
|
|
local function draw_git_icon(x, y, h, color)
|
|
local s = h / GIT_ICON_GRID
|
|
local soft = { table.unpack(color) }
|
|
soft[4] = (soft[4] or 255) * 0.5
|
|
local eps = s / 6 -- hide seams between adjacent runs
|
|
for _, r in ipairs(GIT_ICON_SOFT) do
|
|
renderer.draw_rect(x + r[1] * s, y + r[2] * s, r[3] * s + eps, r[4] * s + eps, soft)
|
|
end
|
|
for _, r in ipairs(GIT_ICON) do
|
|
renderer.draw_rect(x + r[1] * s, y + r[2] * s, r[3] * s + eps, r[4] * s + eps, color)
|
|
end
|
|
end
|
|
|
|
core.add_thread(function()
|
|
for _ = 1, 20 do
|
|
if treeview_ready() then break end
|
|
coroutine.yield(1)
|
|
end
|
|
if not treeview then return end
|
|
local toolbar = treeview.toolbar
|
|
if not toolbar then return end -- toolbarview disabled: keybinding still works
|
|
-- blank symbol: the icon itself is painted by hand below
|
|
local git_button = { symbol = " ", command = "git-panel:toggle" }
|
|
table.insert(toolbar.toolbar_commands, git_button)
|
|
-- widen the pane if the default is too narrow for the extra button
|
|
treeview:set_target_size("x", math.max(treeview.target_size or 0, toolbar:get_min_width()))
|
|
-- dim by default, lit on hover, accent while the panel is showing
|
|
local toolbar_draw = toolbar.draw
|
|
function toolbar.draw(self)
|
|
toolbar_draw(self)
|
|
for item, x, y, w, h in self:each_item() do
|
|
if item == git_button then
|
|
local color = pane_active() and style.accent
|
|
or (self.hovered_item == item and style.text or style.dim)
|
|
draw_git_icon(x + (w - h) / 2, y, h, color)
|
|
end
|
|
end
|
|
end
|
|
end)
|
|
|
|
|
|
--------------------------------------------------------------------------------
|
|
-- Status bar "git button": make gitstatus's branch item open the panel.
|
|
-- gitstatus loads after us, so poll until its item exists.
|
|
--------------------------------------------------------------------------------
|
|
|
|
core.add_thread(function()
|
|
for _ = 1, 20 do
|
|
local ok, item = pcall(core.status_view.get_item, core.status_view, "status:git")
|
|
if ok and item then
|
|
item.command = "git-panel:toggle"
|
|
item.tooltip = "git: click to open panel"
|
|
return
|
|
end
|
|
coroutine.yield(1)
|
|
end
|
|
-- gitstatus absent: add our own fallback branch button
|
|
core.status_view:add_item({
|
|
name = "git-panel:branch",
|
|
alignment = StatusView.Item.RIGHT,
|
|
position = -2,
|
|
get_item = function()
|
|
if not state.branch then return {} end
|
|
return { style.accent, "branch: ", style.text, state.branch }
|
|
end,
|
|
on_click = function() command.perform("git-panel:toggle") end,
|
|
tooltip = "git: click to open panel",
|
|
separator = core.status_view.separator2,
|
|
})
|
|
end)
|