From 94fa5d85ee38d11460483669931445296be0b9be Mon Sep 17 00:00:00 2001 From: tea06194 Date: Sat, 5 Jul 2025 03:16:56 +0300 Subject: [PATCH 1/3] initial dual --- DUAL_PANE.md | 131 +++++++++++++++ DUAL_PANE_CHANGES.md | 128 +++++++++++++++ README.md | 44 +++++ lua/gitgraph.lua | 8 + lua/gitgraph/core.lua | 308 ++++++++++++++++++++++++++++++++++- lua/gitgraph/draw_dual.lua | 282 ++++++++++++++++++++++++++++++++ lua/gitgraph/test_simple.lua | 33 ++++ plugin/gitgraph.lua | 61 +++++++ 8 files changed, 987 insertions(+), 8 deletions(-) create mode 100644 DUAL_PANE.md create mode 100644 DUAL_PANE_CHANGES.md create mode 100644 lua/gitgraph/draw_dual.lua create mode 100644 lua/gitgraph/test_simple.lua create mode 100644 plugin/gitgraph.lua diff --git a/DUAL_PANE.md b/DUAL_PANE.md new file mode 100644 index 0000000..78618cc --- /dev/null +++ b/DUAL_PANE.md @@ -0,0 +1,131 @@ +# GitGraph Dual-Pane Mode + +GitGraph dual-pane mode provides a split-screen interface for viewing Git commit graphs with synchronized scrolling. + +## Features + +- **Split-screen layout**: Graph visualization in left pane, commit details in right pane +- **Synchronized vertical scrolling**: Both panes scroll together vertically +- **Independent horizontal scrolling**: Each pane can scroll horizontally independently +- **Window switching**: Use `` to switch between panes +- **Scroll sync toggle**: Use `gs` to enable/disable scroll synchronization + +## Usage + +### Command + +```vim +:GitGraphDual [args] +``` + +### Programmatic Usage + +```lua +-- Basic usage +require('gitgraph').draw_dual({}, { all = true, max_count = 5000 }) + +-- With options +require('gitgraph').draw_dual({}, { + all = true, + max_count = 1000, + revision_range = "HEAD~10..HEAD" +}) +``` + +### Plugin Configuration + +Add this to your plugin configuration to automatically register the `GitGraphDual` command: + +```lua +return { + { + 'isakbm/gitgraph.nvim', + opts = { + symbols = { + merge_commit = 'M', + commit = '*', + }, + format = { + timestamp = '%H:%M:%S %d-%m-%Y', + fields = { 'hash', 'timestamp', 'author', 'branch_name', 'tag' }, + }, + hooks = { + on_select_commit = function(commit) + print('selected commit:', commit.hash) + end, + on_select_range_commit = function(from, to) + print('selected range:', from.hash, to.hash) + end, + }, + }, + keys = { + { + "gph", + function() + require('gitgraph').draw({}, { all = true, max_count = 5000 }) + end, + desc = "GitGraph - Single Pane", + }, + { + "gpd", + function() + require('gitgraph').draw_dual({}, { all = true, max_count = 5000 }) + end, + desc = "GitGraph - Dual Pane", + }, + }, + } +} +``` + +## Keybindings + +| Key | Action | +|-----|--------| +| `` | Select commit under cursor | +| `` | Switch between graph and text panes | +| `gs` | Toggle scroll synchronization | +| `j/k` | Navigate up/down (synchronized) | +| `h/l` | Horizontal scroll (independent per pane) | +| `v + ` | Select commit range (visual mode) | + +## Pane Layout + +``` +┌─────────────────┬─────────────────────────────────────┐ +│ │ │ +│ Git Graph │ Commit Details │ +│ (Visual) │ (Hash, Author, Date, Message) │ +│ │ │ +│ * │ abc1234 12:34:56 John Doe │ +│ │ │ feat: add new feature │ +│ * │ │ +│ │ │ def5678 11:20:30 Jane Smith │ +│ * │ fix: resolve bug in parser │ +│ │ │ +└─────────────────┴─────────────────────────────────────┘ +``` + +## Arguments + +The dual-pane mode supports the same arguments as the regular GitGraph: + +- `all`: Show all branches +- `max_count=N`: Limit number of commits +- `revision_range`: Specify commit range (e.g., "HEAD~10..HEAD") + +## Customization + +All standard GitGraph configuration options apply to dual-pane mode: + +- `symbols`: Customize graph symbols +- `format`: Configure timestamp and field display +- `hooks`: Set up commit selection handlers +- `highlights`: Customize colors and highlighting + +## Technical Notes + +- Requires Neovim 0.8+ +- Must be run from within a Git repository +- Creates two synchronized buffers with independent horizontal scrolling +- Scroll synchronization can be toggled at runtime \ No newline at end of file diff --git a/DUAL_PANE_CHANGES.md b/DUAL_PANE_CHANGES.md new file mode 100644 index 0000000..cb9891e --- /dev/null +++ b/DUAL_PANE_CHANGES.md @@ -0,0 +1,128 @@ +# GitGraph Dual-Pane Implementation - Changes Summary + +## Overview + +This document summarizes the implementation of dual-pane functionality for the GitGraph.nvim plugin. The dual-pane mode provides a split-screen interface with synchronized vertical scrolling and independent horizontal scrolling. + +## Files Added + +### Core Implementation +- `lua/gitgraph/draw_dual.lua` - Main dual-pane rendering logic +- `plugin/gitgraph.lua` - Auto-registration of GitGraphDual command +- `DUAL_PANE.md` - Comprehensive documentation for dual-pane mode + +## Files Modified + +### `lua/gitgraph.lua` +- Added `draw_dual()` function for dual-pane mode +- Maintained backward compatibility with existing API + +### `lua/gitgraph/core.lua` +- Added `gitgraph_dual()` function +- Added `_gitgraph_dual()` internal function +- Added `graph_to_lines_dual()` function to separate graph and text data +- Enhanced data processing to support dual-pane rendering + +### `README.md` +- Added dual-pane mode documentation +- Updated feature list +- Added usage examples and keybindings + +## Key Features Implemented + +### 1. Dual-Pane Layout +- **Left pane**: Visual git graph representation +- **Right pane**: Commit details (hash, timestamp, author, message, branches, tags) +- Automatic vertical split creation + +### 2. Synchronized Scrolling +- Vertical scrolling synchronized between panes +- Independent horizontal scrolling per pane +- Toggle synchronization with `gs` + +### 3. Window Management +- Switch between panes with `` +- Proper window and buffer handling +- Automatic cleanup on buffer deletion + +### 4. Enhanced Navigation +- All original keybindings preserved +- Additional dual-pane specific keybindings +- Visual mode support for commit range selection + +## Technical Implementation Details + +### Data Separation +The `graph_to_lines_dual()` function separates the original combined output into: +- `graph_lines[]` - Pure graph visualization (symbols only) +- `text_lines[]` - Commit information (hash, author, date, message, etc.) +- Separate highlight arrays for each pane + +### Window Synchronization +- Uses Neovim's autocmd system for cursor movement detection +- Maintains window validity checks +- Prevents recursive synchronization with `updating_scroll` flag + +### Error Handling +- Safe API calls with pcall where appropriate +- Graceful fallbacks for invalid data +- Proper validation of window and buffer handles + +## Usage Integration + +### Command Registration +The plugin automatically registers the `GitGraphDual` command when loaded, making it available immediately after plugin setup. + +### API Compatibility +```lua +-- Original single-pane mode (unchanged) +require('gitgraph').draw({}, { all = true, max_count = 5000 }) + +-- New dual-pane mode +require('gitgraph').draw_dual({}, { all = true, max_count = 5000 }) +``` + +### Plugin Configuration +Works seamlessly with existing plugin configurations. Users can add dual-pane keybindings alongside existing ones: + +```lua +keys = { + -- Original single-pane + { "gph", function() require('gitgraph').draw({}, { all = true, max_count = 5000 }) end }, + -- New dual-pane + { "gpd", function() require('gitgraph').draw_dual({}, { all = true, max_count = 5000 }) end }, +} +``` + +## Keybindings + +| Key | Action | +|-----|--------| +| `` | Select commit under cursor | +| `` | Switch between graph and text panes | +| `gs` | Toggle scroll synchronization | +| `j/k` | Navigate up/down (synchronized) | +| `h/l` | Horizontal scroll (independent per pane) | +| `v + ` | Select commit range (visual mode) | + +## Benefits + +1. **Enhanced Readability**: Separate panes make it easier to read both graph structure and commit details +2. **Better Space Utilization**: Graph symbols don't compete with text for horizontal space +3. **Flexible Navigation**: Independent horizontal scrolling allows focusing on either graph or details +4. **Preserved Functionality**: All original features remain available +5. **Easy Adoption**: Minimal configuration required, works with existing setups + +## Backward Compatibility + +- All existing functionality preserved +- Original `draw()` function unchanged +- Existing configurations continue to work +- No breaking changes to the API + +## Performance Considerations + +- Minimal overhead compared to single-pane mode +- Same data processing with additional separation step +- Efficient buffer and window management +- No impact on original functionality performance \ No newline at end of file diff --git a/README.md b/README.md index 4b92af0..5eb6918 100644 --- a/README.md +++ b/README.md @@ -16,6 +16,7 @@ Git Graph plugin for neovim. - ✔️ easily configurable highlight groups - ✔️ performant scrolling - ✔️ easy to follow branch crossings +- ✔️ dual-pane mode with synchronized scrolling ### Future - auto updating graph - performant load times for large repos @@ -59,11 +60,49 @@ Git Graph plugin for neovim. end, desc = "GitGraph - Draw", }, + { + "gd", + function() + require('gitgraph').draw_dual({}, { all = true, max_count = 5000 }) + end, + desc = "GitGraph - Dual Pane", + }, }, }, ``` +## Dual-Pane Mode + +GitGraph supports a dual-pane mode that splits the interface into two synchronized windows: +- **Left pane**: Visual git graph +- **Right pane**: Commit details (hash, timestamp, author, message, etc.) + +### Usage + +```vim +:GitGraphDual [args] +``` + +Or programmatically: +```lua +require('gitgraph').draw_dual({}, { all = true, max_count = 5000 }) +``` + +### Dual-Pane Keybindings + +| Key | Action | +|-----|--------| +| `` | Switch between graph and text panes | +| `gs` | Toggle scroll synchronization | +| `` | Select commit under cursor | +| `j/k` | Navigate up/down (synchronized) | +| `h/l` | Horizontal scroll (independent per pane) | + +For more details, see [DUAL_PANE.md](DUAL_PANE.md). + +``` + ## View commit with [Diffview.nvim](https://github.com/sindrets/diffview.nvim) When in the git graph buffer you can open `Diffview` on the commit under the cursor with `Enter`. @@ -124,6 +163,11 @@ For example, use **kitty** branch symbols [more detail](https://github.com/kovid }, ``` +# Commands + +- `:GitGraph [args]` - Open single-pane git graph +- `:GitGraphDual [args]` - Open dual-pane git graph + # Keymaps ... more keymaps to come ... diff --git a/lua/gitgraph.lua b/lua/gitgraph.lua index 8476b2e..cc73c8a 100644 --- a/lua/gitgraph.lua +++ b/lua/gitgraph.lua @@ -29,6 +29,14 @@ function M.draw(options, args) return require('gitgraph.draw').draw(M.config, options, args) end +--- Draws the gitgraph in dual-pane mode +---@param options I.DrawOptions +---@param args I.GitLogArgs +---@return nil +function M.draw_dual(options, args) + return require('gitgraph.draw_dual').draw(M.config, options, args) +end + --- Tests the gitgraph plugin function M.test() local lines, _failure = require('gitgraph.tests').run_tests(M.config.symbols, M.config.format.fields) diff --git a/lua/gitgraph/core.lua b/lua/gitgraph/core.lua index 13605b9..4aaf44b 100644 --- a/lua/gitgraph/core.lua +++ b/lua/gitgraph/core.lua @@ -42,12 +42,38 @@ function M.gitgraph(config, options, args) return graph, lines, highlights, head_loc end +---@param config I.GGConfig +---@param options I.DrawOptions +---@param args I.GitLogArgs +---@return I.Row[] +---@return string[] +---@return string[] +---@return I.Highlight[] +---@return I.Highlight[] +---@return integer? +function M.gitgraph_dual(config, options, args) + --- depends on `git` + local data = require('gitgraph.git').git_log_pretty(args, config.format.timestamp) + + --- does the magic + local start = os.clock() + local graph, graph_lines, text_lines, graph_highlights, text_highlights, head_loc = M._gitgraph_dual( + data, + options, + config.symbols, + config.format.fields) + local dur = os.clock() - start + log.info('_gitgraph_dual dur:', dur * 1000, 'ms') + + return graph, graph_lines, text_lines, graph_highlights, text_highlights, head_loc +end + ---@param raw_commits I.RawCommit[] ---@return table, string[] local function process_raw_commits(raw_commits) local start = os.clock() - local commits = {} --- @type table + local commits = {} --- @type table local sorted_commits = {} --- @type string[] for _, rc in ipairs(raw_commits) do @@ -342,14 +368,14 @@ local function insert_symbols_on_connector_rows(graph, sym) -- two neighbors (no straights) -- - 8421 [10] = sym.GCLU, -- '1010' - [9] = sym.GCLD, -- '1001' - [6] = sym.GCRU, -- '0110' - [5] = sym.GCRD, -- '0101' + [9] = sym.GCLD, -- '1001' + [6] = sym.GCRU, -- '0110' + [5] = sym.GCRD, -- '0101' -- three neighbors [14] = sym.GLRU, -- '1110' [13] = sym.GLRD, -- '1101' [11] = sym.GLUD, -- '1011' - [7] = sym.GRUD, -- '0111' + [7] = sym.GRUD, -- '0111' } for i = 2, #graph, 2 do @@ -734,6 +760,232 @@ local function graph_to_lines(options, graph, sym, fields, commits) return lines, highlights, head_loc end +---@param options I.DrawOptions +---@param graph I.Row[] +---@param sym I.GGSymbols +---@param fields string[] +---@param commits table +---@return string[] +---@return string[] +---@return I.Highlight[] +---@return I.Highlight[] +---@return integer? +local function graph_to_lines_dual(options, graph, sym, fields, commits) + local ITEM_HGS = require('gitgraph.highlights').ITEM_HGS + local BRANCH_HGS = require('gitgraph.highlights').BRANCH_HGS + + local NUM_BRANCH_COLORS = #BRANCH_HGS + + local start = os.clock() + + ---@type integer? + local head_loc = 1 + + ---@type string[] + local graph_lines = {} + + ---@type string[] + local text_lines = {} + + ---@type I.Highlight[] + local graph_highlights = {} + + ---@type I.Highlight[] + local text_highlights = {} + + ---@param cell I.Cell + ---@return string + local function commit_cell_symb(cell) + assert(cell.is_commit) + + if options.mode == 'debug' then + return cell.commit.msg + end + + if #cell.commit.parents > 1 then + -- merge commit + return #cell.commit.children == 0 and sym.merge_commit_end or sym.merge_commit + else + -- regular commit + return #cell.commit.children == 0 and sym.commit_end or sym.commit + end + end + + ---@param row I.Row + ---@return string + local function row_to_graph_str(row) + local row_strs = {} + for j = 1, #row.cells do + local cell = row.cells[j] + if cell.connector then + cell.symbol = cell.connector + else + assert(cell.commit) + cell.symbol = commit_cell_symb(cell) + end + row_strs[#row_strs + 1] = cell.symbol + end + return table.concat(row_strs) + end + + ---@param row I.Row + ---@param row_idx integer + ---@param continuation_symbols string[] + ---@return I.Highlight[] + local function row_to_graph_highlights(row, row_idx, continuation_symbols) + local row_hls = {} + local offset = 0 + + for j = 1, #row.cells do + local cell = row.cells[j] + + local width = cell.symbol and #cell.symbol or 1 + local start = offset + local stop = start + width + offset = offset + width + + if cell.commit then + local hg = 'GitGraphBranch' .. tostring(j % NUM_BRANCH_COLORS + 1) + row_hls[#row_hls + 1] = { hg = hg, row = row_idx, start = start, stop = stop } + elseif cell.symbol == sym.GHOR then + -- take color from first right cell that attaches to this connector + for k = j + 1, #row.cells do + local rcell = row.cells[k] + + if rcell.commit and vim.tbl_contains(continuation_symbols, rcell.symbol) then + local hg = 'GitGraphBranch' .. tostring(k % NUM_BRANCH_COLORS + 1) + row_hls[#row_hls + 1] = { hg = hg, row = row_idx, start = start, stop = stop } + break + end + end + end + end + return row_hls + end + + local continuation_symbols = { + sym.GCLD, + sym.GCLU, + sym.GFORKD, + sym.GFORKU, + sym.GLUDCD, + sym.GLUDCU, + sym.GLRDCL, + sym.GLRUCL, + } + + local head_found = false + + for idx = 1, #graph do + local proper_row = graph[idx] + + -- Generate graph line + if options.mode == 'test' then + local row_strs = {} + for i = 1, #proper_row.cells do + local cell = proper_row.cells[i] + if cell.connector then + row_strs[#row_strs + 1] = cell.connector + else + assert(cell.commit) + local symbol = cell.commit.msg + symbol = cell.emphasis and symbol:lower() or symbol + row_strs[#row_strs + 1] = symbol + end + end + graph_lines[#graph_lines + 1] = table.concat(row_strs) + else + graph_lines[#graph_lines + 1] = row_to_graph_str(proper_row) + end + + -- Generate text line + local text_parts = {} + local text_offset = 0 + + local function add_text_part(text, highlight_type) + if text and text ~= '' then + if highlight_type then + text_highlights[#text_highlights + 1] = { + hg = ITEM_HGS[highlight_type].name, + row = idx, + start = text_offset, + stop = text_offset + #text, + } + end + text_parts[#text_parts + 1] = text + text_offset = text_offset + #text + 1 -- +1 for space separator + end + end + + if options.mode ~= 'test' then + local c = proper_row.commit + if c then + local hash = c.hash:sub(1, 7) + local timestamp = c.author_date + local author = c.author_name + local branch_names = #c.branch_names > 0 and ('(%s)'):format(table.concat(c.branch_names, ' | ')) or nil + local tags = #c.tags > 0 and ('(%s)'):format(table.concat(c.tags, ' | ')) or nil + + local is_head = false + if not head_found then + is_head = branch_names and branch_names:match('HEAD %->') or false + if is_head then + head_found = true + head_loc = idx + end + end + + if is_head then + add_text_part('*') + end + + add_text_part(hash, 'hash') + add_text_part(timestamp, 'timestamp') + add_text_part(author, 'author') + add_text_part(branch_names, 'branch_name') + add_text_part(tags, 'tag') + + if options.mode == 'debug' then + local parents = '' + for _, h in ipairs(c.parents) do + local p = commits[h] + parents = parents .. (p and p.msg or '?') + end + + local children = '' + for _, h in ipairs(c.children) do + local p = commits[h] + children = children .. (p and p.msg or '?') + end + if #children == 0 then + children = '_' + end + add_text_part(': ' .. children .. ' ' .. c.msg .. ' ' .. parents) + end + else + -- Message row + local c = graph[idx - 1].commit + assert(c) + if options.mode ~= 'debug' then + add_text_part(c.msg, 'message') + end + end + + -- Add graph highlights + for _, hl in ipairs(row_to_graph_highlights(proper_row, idx, continuation_symbols)) do + graph_highlights[#graph_highlights + 1] = hl + end + end + + text_lines[#text_lines + 1] = table.concat(text_parts, ' ') + end + + local dur = os.clock() - start + log.info('graph_to_lines_dual dur:', dur * 1000, 'ms') + + return graph_lines, text_lines, graph_highlights, text_highlights, head_loc +end + ---@param cells I.Cell[] ---@return I.Cell[] local function propagate(cells) @@ -824,7 +1076,14 @@ end ---@param curr_commit I.Commit ---@param next_commit I.Commit? ---@return I.Row, boolean -local function generate_connector_row(commits, prev_commit_row, prev_connector_row, commit_row, commit_loc, curr_commit, next_commit) +local function generate_connector_row( + commits, + prev_commit_row, + prev_connector_row, + commit_row, + commit_loc, + curr_commit, + next_commit) local found_bi_crossing = false -- connector row (reservation row) @@ -932,7 +1191,10 @@ local function generate_connector_row(commits, prev_commit_row, prev_connector_r local connector_row = { cells = connector_cells } ---@type I.Row -- handle bi-connector rows - local is_bi_crossing, bi_crossing_safely_resolveable = utils.get_is_bi_crossing(commit_row, connector_row, next_commit) + local is_bi_crossing, bi_crossing_safely_resolveable = utils.get_is_bi_crossing( + commit_row, + connector_row, + next_commit) -- used for troubleshooting and tracking complexity of tests if is_bi_crossing then @@ -984,7 +1246,14 @@ local function straight_j(commits, sorted_commits) local connector_row = nil ---@type I.Row local bi_crossing = false if i < #sorted_commits then - connector_row, bi_crossing = generate_connector_row(commits, prev_commit_row, prev_connector_row, commit_row, commit_loc, curr_commit, next_commit) + connector_row, bi_crossing = generate_connector_row( + commits, + prev_commit_row, + prev_connector_row, + commit_row, + commit_loc, + curr_commit, + next_commit) if bi_crossing then found_bi_crossing = true end @@ -1031,4 +1300,27 @@ function M._gitgraph(raw_commits, opt, sym, fields) return graph, lines, highlights, head_loc, found_bi_crossing end +function M._gitgraph_dual(raw_commits, opt, sym, fields) + sym = get_symbols(sym, opt) + + local commits, sorted_commits = process_raw_commits(raw_commits) + + populate_child_parent_data(commits, sorted_commits) + + local graph, found_bi_crossing = straight_j(commits, sorted_commits) + + insert_vert_and_hor_pipes(graph, sym) + + insert_symbols_on_connector_rows(graph, sym) + + local graph_lines, text_lines, graph_highlights, text_highlights, head_loc = graph_to_lines_dual( + opt, + graph, + sym, + fields, + commits) + + return graph, graph_lines, text_lines, graph_highlights, text_highlights, head_loc +end + return M diff --git a/lua/gitgraph/draw_dual.lua b/lua/gitgraph/draw_dual.lua new file mode 100644 index 0000000..3a650ed --- /dev/null +++ b/lua/gitgraph/draw_dual.lua @@ -0,0 +1,282 @@ +local log = require('gitgraph.log') +local utils = require('gitgraph.utils') +local core = require('gitgraph.core') + +local M = {} + +-- Store window and buffer IDs for synchronization +M.graph_win = nil +M.graph_buf = nil +M.text_win = nil +M.text_buf = nil + +-- Synchronization state +M.sync_scroll = true +M.updating_scroll = false + +-- Synchronize vertical scrolling between windows +local function sync_vertical_scroll() + if M.updating_scroll or not M.sync_scroll then + return + end + + M.updating_scroll = true + + local current_win = vim.api.nvim_get_current_win() + local current_line = vim.api.nvim_win_get_cursor(current_win)[1] + + -- Sync to the other window + if current_win == M.graph_win and M.text_win and vim.api.nvim_win_is_valid(M.text_win) then + vim.api.nvim_win_set_cursor(M.text_win, { current_line, 0 }) + elseif current_win == M.text_win and M.graph_win and vim.api.nvim_win_is_valid(M.graph_win) then + vim.api.nvim_win_set_cursor(M.graph_win, { current_line, 0 }) + end + + M.updating_scroll = false +end + +-- Set up scroll synchronization autocmds +local function setup_scroll_sync() + local group = vim.api.nvim_create_augroup('GitGraphDualSync', { clear = true }) + + -- Cursor moved events + vim.api.nvim_create_autocmd('CursorMoved', { + group = group, + callback = function() + local current_win = vim.api.nvim_get_current_win() + if current_win == M.graph_win or current_win == M.text_win then + sync_vertical_scroll() + end + end, + }) + + -- Clean up when buffers are deleted + vim.api.nvim_create_autocmd('BufDelete', { + group = group, + callback = function(args) + if args.buf == M.graph_buf or args.buf == M.text_buf then + M.graph_win = nil + M.graph_buf = nil + M.text_win = nil + M.text_buf = nil + end + end, + }) +end + +-- Apply buffer options for graph window +local function apply_graph_buffer_options(buf, win) + vim.api.nvim_set_option_value('modifiable', false, { buf = buf }) + vim.api.nvim_buf_set_name(buf, 'GitGraph') + vim.api.nvim_set_option_value('buflisted', false, { buf = buf }) + + -- Set window-local options + if win and vim.api.nvim_win_is_valid(win) then + vim.api.nvim_set_option_value('wrap', false, { win = win }) + vim.api.nvim_set_option_value('cursorline', true, { win = win }) + end + + local options = { + 'foldcolumn=0', + 'foldlevel=999', + 'norelativenumber', + 'nospell', + 'noswapfile', + } + + -- Apply options to buffer + vim.api.nvim_buf_call(buf, function() + vim.cmd(('silent! noautocmd setlocal %s'):format(table.concat(options, ' '))) + end) +end + +-- Apply buffer options for text window +local function apply_text_buffer_options(buf, win) + vim.api.nvim_set_option_value('modifiable', false, { buf = buf }) + vim.api.nvim_buf_set_name(buf, 'GitGraph-Text') + vim.api.nvim_set_option_value('buflisted', false, { buf = buf }) + + -- Set window-local options + if win and vim.api.nvim_win_is_valid(win) then + vim.api.nvim_set_option_value('wrap', false, { win = win }) + vim.api.nvim_set_option_value('cursorline', true, { win = win }) + end + + local options = { + 'foldcolumn=0', + 'foldlevel=999', + 'norelativenumber', + 'nospell', + 'noswapfile', + } + + -- Apply options to buffer + vim.api.nvim_buf_call(buf, function() + vim.cmd(('silent! noautocmd setlocal %s'):format(table.concat(options, ' '))) + end) +end + +-- Apply key mappings for both buffers +local function apply_dual_buffer_mappings(graph_buf, text_buf, graph_data, hooks) + -- Function to get commit from current line + local function get_current_commit() + local current_win = vim.api.nvim_get_current_win() + local row = vim.api.nvim_win_get_cursor(current_win)[1] + return utils.get_commit_from_row(graph_data, row) + end + + -- Function to get commit range from visual selection + local function get_visual_commit_range() + local start_row = vim.fn.getpos("'<")[2] + local end_row = vim.fn.getpos("'>")[2] + local to_commit = utils.get_commit_from_row(graph_data, start_row) + local from_commit = utils.get_commit_from_row(graph_data, end_row) + return from_commit, to_commit + end + + -- Set up mappings for both buffers + for _, buf in ipairs({ graph_buf, text_buf }) do + vim.keymap.set('n', '', function() + local commit = get_current_commit() + if commit then + hooks.on_select_commit(commit) + end + end, { buffer = buf, desc = 'select commit under cursor' }) + + vim.keymap.set('v', '', function() + vim.cmd('noau normal! "vy"') + local from_commit, to_commit = get_visual_commit_range() + if from_commit and to_commit then + hooks.on_select_range_commit(from_commit, to_commit) + end + end, { buffer = buf, desc = 'select range of commits' }) + + -- Add toggle for scroll synchronization + vim.keymap.set('n', 'gs', function() + M.sync_scroll = not M.sync_scroll + print('GitGraph scroll sync: ' .. (M.sync_scroll and 'ON' or 'OFF')) + end, { buffer = buf, desc = 'toggle scroll synchronization' }) + + -- Navigation shortcuts + vim.keymap.set('n', '', function() + if vim.api.nvim_get_current_win() == M.graph_win and M.text_win and vim.api.nvim_win_is_valid(M.text_win) then + vim.api.nvim_set_current_win(M.text_win) + elseif vim.api.nvim_get_current_win() == M.text_win and M.graph_win and vim.api.nvim_win_is_valid(M.graph_win) then + vim.api.nvim_set_current_win(M.graph_win) + end + end, { buffer = buf, desc = 'switch between graph and text windows' }) + end +end + +---@param config I.GGConfig +---@param options I.DrawOptions +---@param args I.GitLogArgs +function M.draw(config, options, args) + if utils.check_cmd('git --version') then + log.error('git command not found, please install it') + return + end + + if utils.check_cmd('git status') then + log.error('does not seem to be a valid git repo') + return + end + + -- Create or reuse buffers + if not M.graph_buf or not vim.api.nvim_buf_is_valid(M.graph_buf) then + M.graph_buf = vim.api.nvim_create_buf(false, true) + end + + if not M.text_buf or not vim.api.nvim_buf_is_valid(M.text_buf) then + M.text_buf = vim.api.nvim_create_buf(false, true) + end + + -- Create vertical split layout + vim.cmd('vsplit') + + -- Set up graph window (left) + M.graph_win = vim.api.nvim_get_current_win() + vim.api.nvim_win_set_buf(M.graph_win, M.graph_buf) + + -- Set up text window (right) + vim.cmd('wincmd l') + M.text_win = vim.api.nvim_get_current_win() + vim.api.nvim_win_set_buf(M.text_win, M.text_buf) + + -- Make buffers modifiable for editing + vim.api.nvim_set_option_value('modifiable', true, { buf = M.graph_buf }) + vim.api.nvim_set_option_value('modifiable', true, { buf = M.text_buf }) + + -- Clear both buffers + vim.api.nvim_buf_set_lines(M.graph_buf, 0, -1, false, {}) + vim.api.nvim_buf_set_lines(M.text_buf, 0, -1, false, {}) + + -- Clear highlights + vim.api.nvim_buf_clear_namespace(M.graph_buf, -1, 0, -1) + vim.api.nvim_buf_clear_namespace(M.text_buf, -1, 0, -1) + + -- Extract graph data using modified core function + local graph, graph_lines, text_lines, graph_highlights, text_highlights, head_loc = core.gitgraph_dual(config, + options, args) + M.graph = graph -- Store for mappings compatibility + + -- Populate graph buffer + vim.api.nvim_buf_set_lines(M.graph_buf, 0, #graph_lines, false, graph_lines) + + -- Populate text buffer + vim.api.nvim_buf_set_lines(M.text_buf, 0, #text_lines, false, text_lines) + + -- Apply highlights asynchronously + local function apply_highlights() + -- Graph highlights + for _, hl in ipairs(graph_highlights) do + if hl.start and hl.stop and hl.start >= 0 and hl.stop >= hl.start then + vim.api.nvim_buf_add_highlight(M.graph_buf, -1, hl.hg, hl.row - 1, hl.start, hl.stop) + end + end + + -- Text highlights + for _, hl in ipairs(text_highlights) do + if hl.start and hl.stop and hl.start >= 0 and hl.stop >= hl.start then + vim.api.nvim_buf_add_highlight(M.text_buf, -1, hl.hg, hl.row - 1, hl.start, hl.stop) + end + end + end + + local co = coroutine.create(apply_highlights) + + local function wait_poll() + if coroutine.status(co) ~= 'dead' then + coroutine.resume(co) + vim.defer_fn(wait_poll, 16) -- Adjust delay as needed + end + end + + vim.defer_fn(wait_poll, 1) + + -- Set cursor position to head location + local cursor_line = head_loc or 1 + if M.graph_win and vim.api.nvim_win_is_valid(M.graph_win) then + vim.api.nvim_win_set_cursor(M.graph_win, { cursor_line, 0 }) + end + if M.text_win and vim.api.nvim_win_is_valid(M.text_win) then + vim.api.nvim_win_set_cursor(M.text_win, { cursor_line, 0 }) + end + + -- Apply buffer options + apply_graph_buffer_options(M.graph_buf, M.graph_win) + apply_text_buffer_options(M.text_buf, M.text_win) + + -- Apply mappings + apply_dual_buffer_mappings(M.graph_buf, M.text_buf, graph, config.hooks) + + -- Set up scroll synchronization + setup_scroll_sync() + + -- Set focus to graph window + if M.graph_win and vim.api.nvim_win_is_valid(M.graph_win) then + vim.api.nvim_set_current_win(M.graph_win) + end +end + +return M diff --git a/lua/gitgraph/test_simple.lua b/lua/gitgraph/test_simple.lua new file mode 100644 index 0000000..4ef61bf --- /dev/null +++ b/lua/gitgraph/test_simple.lua @@ -0,0 +1,33 @@ +-- Simple test for GitGraph dual-pane functionality +local M = {} + +function M.test_dual() + print("Testing GitGraph dual-pane...") + + -- Check if we're in a git repo + local utils = require('gitgraph.utils') + if utils.check_cmd('git status') then + print("ERROR: Not in a git repository") + return false + end + + -- Try to call the dual-pane function + local ok, err = pcall(function() + require('gitgraph').draw_dual({}, { all = true, max_count = 10 }) + end) + + if ok then + print("✓ GitGraph dual-pane test PASSED") + return true + else + print("✗ GitGraph dual-pane test FAILED:", err) + return false + end +end + +-- Create test command +vim.api.nvim_create_user_command('GitGraphTest', M.test_dual, { + desc = 'Test GitGraph dual-pane functionality' +}) + +return M diff --git a/plugin/gitgraph.lua b/plugin/gitgraph.lua new file mode 100644 index 0000000..b7441f5 --- /dev/null +++ b/plugin/gitgraph.lua @@ -0,0 +1,61 @@ +-- GitGraph.nvim plugin initialization +-- Auto-registers commands when plugin is loaded + +local function setup_commands() + -- Register GitGraphDual command + vim.api.nvim_create_user_command('GitGraphDual', function(opts) + local gitgraph = require('gitgraph') + local args = {} + + -- Parse command arguments + if opts.args and opts.args ~= '' then + -- Simple parsing for common arguments + if string.find(opts.args, 'all') then + args.all = true + end + + local max_count = string.match(opts.args, 'max_count=(%d+)') + if max_count then + args.max_count = tonumber(max_count) + end + end + + gitgraph.draw_dual({}, args) + end, { + desc = 'Open GitGraph in dual-pane mode', + nargs = '*', + complete = function(arglead, cmdline, cursorpos) + return { 'all', 'max_count=1000', 'max_count=5000' } + end + }) + + -- Register original GitGraph command if not already registered + if vim.fn.exists(':GitGraph') == 0 then + vim.api.nvim_create_user_command('GitGraph', function(opts) + local gitgraph = require('gitgraph') + local args = {} + + if opts.args and opts.args ~= '' then + if string.find(opts.args, 'all') then + args.all = true + end + + local max_count = string.match(opts.args, 'max_count=(%d+)') + if max_count then + args.max_count = tonumber(max_count) + end + end + + gitgraph.draw({}, args) + end, { + desc = 'Open GitGraph in single-pane mode', + nargs = '*', + complete = function(arglead, cmdline, cursorpos) + return { 'all', 'max_count=1000', 'max_count=5000' } + end + }) + end +end + +-- Setup commands when plugin loads +setup_commands() From 6b275f12f1b7bd5a5a75393929bffa8cce0dfb9f Mon Sep 17 00:00:00 2001 From: tea06194 Date: Sun, 6 Jul 2025 00:01:07 +0300 Subject: [PATCH 2/3] color commit text (except msg) as branch opt --- DUAL_PANE.md | 8 +++++-- README.md | 12 +++++++++- lua/gitgraph/core.lua | 49 ++++++++++++++++++++++++++++++++------ lua/gitgraph/draw_dual.lua | 45 +++++++++++++++++++++++++--------- 4 files changed, 93 insertions(+), 21 deletions(-) diff --git a/DUAL_PANE.md b/DUAL_PANE.md index 78618cc..a1b0acd 100644 --- a/DUAL_PANE.md +++ b/DUAL_PANE.md @@ -1,6 +1,6 @@ # GitGraph Dual-Pane Mode -GitGraph dual-pane mode provides a split-screen interface for viewing Git commit graphs with synchronized scrolling. +GitGraph dual-pane mode provides a split-screen interface for viewing Git commit graphs with synchronized scrolling. The dual-pane mode opens in a new tab to avoid conflicts with existing buffers. ## Features @@ -88,6 +88,7 @@ return { | `j/k` | Navigate up/down (synchronized) | | `h/l` | Horizontal scroll (independent per pane) | | `v + ` | Select commit range (visual mode) | +| `q` | Close dual-pane tab | ## Pane Layout @@ -127,5 +128,8 @@ All standard GitGraph configuration options apply to dual-pane mode: - Requires Neovim 0.8+ - Must be run from within a Git repository +- Opens in a new tab to avoid buffer name conflicts - Creates two synchronized buffers with independent horizontal scrolling -- Scroll synchronization can be toggled at runtime \ No newline at end of file +- Scroll synchronization can be toggled at runtime +- Uses unique buffer names with timestamps to prevent conflicts +- Press `q` to close the dual-pane tab \ No newline at end of file diff --git a/README.md b/README.md index 5eb6918..98fdb00 100644 --- a/README.md +++ b/README.md @@ -43,6 +43,10 @@ Git Graph plugin for neovim. timestamp = '%H:%M:%S %d-%m-%Y', fields = { 'hash', 'timestamp', 'author', 'branch_name', 'tag' }, }, + -- Dual-pane specific options + dual = { + commit_info_as_branch_color = false, -- Use branch colors for commit info instead of default colors + }, hooks = { on_select_commit = function(commit) print('selected commit:', commit.hash) @@ -74,9 +78,10 @@ Git Graph plugin for neovim. ## Dual-Pane Mode -GitGraph supports a dual-pane mode that splits the interface into two synchronized windows: +GitGraph supports a dual-pane mode that splits the interface into two synchronized windows in a new tab: - **Left pane**: Visual git graph - **Right pane**: Commit details (hash, timestamp, author, message, etc.) +- Opens in a new tab to avoid buffer conflicts with existing GitGraph instances ### Usage @@ -98,6 +103,11 @@ require('gitgraph').draw_dual({}, { all = true, max_count = 5000 }) | `` | Select commit under cursor | | `j/k` | Navigate up/down (synchronized) | | `h/l` | Horizontal scroll (independent per pane) | +| `q` | Close dual-pane tab | + +### Dual-Pane Configuration Options + +- `commit_info_as_branch_color`: When enabled in dual-pane mode, commit information (hash, timestamp, author, branch names) uses the same colors as the corresponding branch in the graph instead of the default highlight groups. For more details, see [DUAL_PANE.md](DUAL_PANE.md). diff --git a/lua/gitgraph/core.lua b/lua/gitgraph/core.lua index 4aaf44b..beaf88d 100644 --- a/lua/gitgraph/core.lua +++ b/lua/gitgraph/core.lua @@ -7,6 +7,7 @@ local log = require('gitgraph.log') ---@class I.DrawOptions ---@field mode? 'debug' | 'test' ---@field pretty? boolean +---@field commit_info_as_branch_color? boolean ---@class I.Highlight ---@field hg string -- NOTE: fine to use string since lua internalizes strings @@ -229,7 +230,8 @@ local function insert_vert_and_hor_pipes(graph, sym) local first_repeat = nil for k = 1, #row.cells, 2 do local cell_k, cell_j = row.cells[k], row.cells[j] - local rkc, rjc = (not cell_k.connector and cell_k.commit), (not cell_j.connector and cell_j.commit) + local rkc, rjc = (not cell_k.connector and cell_k.commit), + (not cell_j.connector and cell_j.commit) -- local rkc, rjc = row.cells[k].commit, row.cells[j].commit @@ -248,7 +250,8 @@ local function insert_vert_and_hor_pipes(graph, sym) local this_k = graph[i].cells[k] local below_k = graph[i + 1].cells[k] - local bkc, tkc = (not below_k.connector and below_k.commit), (not this_k.connector and this_k.commit) + local bkc, tkc = (not below_k.connector and below_k.commit), + (not this_k.connector and this_k.commit) -- local bkc, tkc = below_k.commit, this_k.commit if (bkc and tkc) and bkc.hash == tkc.hash then @@ -863,6 +866,18 @@ local function graph_to_lines_dual(options, graph, sym, fields, commits) return row_hls end + local function get_commit_branch_color_from_row(row) + for j = 1, #row.cells do + local cell = row.cells[j] + if cell.is_commit then + local color_index = j % NUM_BRANCH_COLORS + 1 + local color_name = 'GitGraphBranch' .. tostring(color_index) + return color_name + end + end + return nil + end + local continuation_symbols = { sym.GCLD, sym.GCLU, @@ -917,6 +932,19 @@ local function graph_to_lines_dual(options, graph, sym, fields, commits) end end + local function add_text_part_branch(text, branch_color) + if text and text ~= '' then + text_highlights[#text_highlights + 1] = { + hg = branch_color, + row = idx, + start = text_offset, + stop = text_offset + #text, + } + text_parts[#text_parts + 1] = text + text_offset = text_offset + #text + 1 -- +1 for space separator + end + end + if options.mode ~= 'test' then local c = proper_row.commit if c then @@ -939,11 +967,18 @@ local function graph_to_lines_dual(options, graph, sym, fields, commits) add_text_part('*') end - add_text_part(hash, 'hash') - add_text_part(timestamp, 'timestamp') - add_text_part(author, 'author') - add_text_part(branch_names, 'branch_name') - add_text_part(tags, 'tag') + if options.commit_info_as_branch_color then + local commit_branch_color = get_commit_branch_color_from_row(proper_row) + add_text_part_branch(hash, commit_branch_color) + add_text_part_branch(timestamp, commit_branch_color) + add_text_part_branch(author, commit_branch_color) + add_text_part_branch(branch_names, commit_branch_color) + else + add_text_part(hash, 'hash') + add_text_part(timestamp, 'timestamp') + add_text_part(author, 'author') + add_text_part(branch_names, 'branch_name') + end if options.mode == 'debug' then local parents = '' diff --git a/lua/gitgraph/draw_dual.lua b/lua/gitgraph/draw_dual.lua index 3a650ed..9425675 100644 --- a/lua/gitgraph/draw_dual.lua +++ b/lua/gitgraph/draw_dual.lua @@ -4,6 +4,8 @@ local core = require('gitgraph.core') local M = {} +local NS = vim.api.nvim_create_namespace('GitGraphDual') + -- Store window and buffer IDs for synchronization M.graph_win = nil M.graph_buf = nil @@ -67,7 +69,9 @@ end -- Apply buffer options for graph window local function apply_graph_buffer_options(buf, win) vim.api.nvim_set_option_value('modifiable', false, { buf = buf }) - vim.api.nvim_buf_set_name(buf, 'GitGraph') + -- Use unique name with timestamp to avoid conflicts + local timestamp = os.time() + vim.api.nvim_buf_set_name(buf, 'GitGraph-Dual-' .. timestamp) vim.api.nvim_set_option_value('buflisted', false, { buf = buf }) -- Set window-local options @@ -93,7 +97,9 @@ end -- Apply buffer options for text window local function apply_text_buffer_options(buf, win) vim.api.nvim_set_option_value('modifiable', false, { buf = buf }) - vim.api.nvim_buf_set_name(buf, 'GitGraph-Text') + -- Use unique name with timestamp to avoid conflicts + local timestamp = os.time() + vim.api.nvim_buf_set_name(buf, 'GitGraph-Text-Dual-' .. timestamp) vim.api.nvim_set_option_value('buflisted', false, { buf = buf }) -- Set window-local options @@ -165,6 +171,11 @@ local function apply_dual_buffer_mappings(graph_buf, text_buf, graph_data, hooks vim.api.nvim_set_current_win(M.graph_win) end end, { buffer = buf, desc = 'switch between graph and text windows' }) + + -- Close dual-pane tab + vim.keymap.set('n', 'q', function() + vim.cmd('tabclose') + end, { buffer = buf, desc = 'close dual-pane tab' }) end end @@ -182,14 +193,12 @@ function M.draw(config, options, args) return end - -- Create or reuse buffers - if not M.graph_buf or not vim.api.nvim_buf_is_valid(M.graph_buf) then - M.graph_buf = vim.api.nvim_create_buf(false, true) - end + -- Open in new tab for dual-pane mode + vim.cmd('tabnew') - if not M.text_buf or not vim.api.nvim_buf_is_valid(M.text_buf) then - M.text_buf = vim.api.nvim_create_buf(false, true) - end + -- Create new buffers for this tab + M.graph_buf = vim.api.nvim_create_buf(false, true) + M.text_buf = vim.api.nvim_create_buf(false, true) -- Create vertical split layout vim.cmd('vsplit') @@ -228,17 +237,31 @@ function M.draw(config, options, args) -- Apply highlights asynchronously local function apply_highlights() + vim.api.nvim_buf_clear_namespace(M.graph_buf, NS, 0, -1) + vim.api.nvim_buf_clear_namespace(M.text_buf, NS, 0, -1) -- Graph highlights for _, hl in ipairs(graph_highlights) do if hl.start and hl.stop and hl.start >= 0 and hl.stop >= hl.start then - vim.api.nvim_buf_add_highlight(M.graph_buf, -1, hl.hg, hl.row - 1, hl.start, hl.stop) + vim.hl.range( + M.graph_buf, + NS, + hl.hg, + { hl.row - 1, hl.start }, + { hl.row - 1, hl.stop } + ) end end -- Text highlights for _, hl in ipairs(text_highlights) do if hl.start and hl.stop and hl.start >= 0 and hl.stop >= hl.start then - vim.api.nvim_buf_add_highlight(M.text_buf, -1, hl.hg, hl.row - 1, hl.start, hl.stop) + vim.hl.range( + M.text_buf, + NS, + hl.hg, + { hl.row - 1, hl.start }, + { hl.row - 1, hl.stop } + ) end end end From f595028eb97b6a903f9190fb3c70f111f4a889d8 Mon Sep 17 00:00:00 2001 From: tea06194 Date: Sun, 6 Jul 2025 18:15:34 +0300 Subject: [PATCH 3/3] refresh feature --- README.md | 84 +++++++++++++++++++++++--------- lua/gitgraph.lua | 12 +++++ lua/gitgraph/draw.lua | 32 +++++++++++-- lua/gitgraph/draw_dual.lua | 98 +++++++++++++++++++++++++++----------- lua/gitgraph/utils.lua | 6 ++- plugin/gitgraph.lua | 16 +++++++ 6 files changed, 193 insertions(+), 55 deletions(-) diff --git a/README.md b/README.md index 98fdb00..f31c9ca 100644 --- a/README.md +++ b/README.md @@ -7,6 +7,7 @@ Git Graph plugin for neovim. # Roadmap - Goals ### Completed + - ✔️ 100% lua - ✔️ temporal topological order - ✔️ branches stick to their lane @@ -17,7 +18,9 @@ Git Graph plugin for neovim. - ✔️ performant scrolling - ✔️ easy to follow branch crossings - ✔️ dual-pane mode with synchronized scrolling + ### Future + - auto updating graph - performant load times for large repos @@ -71,6 +74,13 @@ Git Graph plugin for neovim. end, desc = "GitGraph - Dual Pane", }, + { + "gr", + function() + require('gitgraph').refresh() + end, + desc = "GitGraph - Refresh", + }, }, }, @@ -79,6 +89,7 @@ Git Graph plugin for neovim. ## Dual-Pane Mode GitGraph supports a dual-pane mode that splits the interface into two synchronized windows in a new tab: + - **Left pane**: Visual git graph - **Right pane**: Commit details (hash, timestamp, author, message, etc.) - Opens in a new tab to avoid buffer conflicts with existing GitGraph instances @@ -90,20 +101,28 @@ GitGraph supports a dual-pane mode that splits the interface into two synchroniz ``` Or programmatically: + ```lua require('gitgraph').draw_dual({}, { all = true, max_count = 5000 }) ``` +### Refreshing GitGraph + +You can refresh the current git graph without reopening: + +```lua +-- Refresh single-pane mode +require('gitgraph').refresh() + +-- Refresh dual-pane mode +require('gitgraph').refresh_dual() +``` + +Or use the built-in `` shortcut (similar to Oil.nvim). + ### Dual-Pane Keybindings -| Key | Action | -|-----|--------| -| `` | Switch between graph and text panes | -| `gs` | Toggle scroll synchronization | -| `` | Select commit under cursor | -| `j/k` | Navigate up/down (synchronized) | -| `h/l` | Horizontal scroll (independent per pane) | -| `q` | Close dual-pane tab | +See the [Keymaps](#keymaps) section below for complete key bindings. ### Dual-Pane Configuration Options @@ -111,7 +130,7 @@ require('gitgraph').draw_dual({}, { all = true, max_count = 5000 }) For more details, see [DUAL_PANE.md](DUAL_PANE.md). -``` +```` ## View commit with [Diffview.nvim](https://github.com/sindrets/diffview.nvim) @@ -138,10 +157,12 @@ When in visual mode you get the `Diffview` for the selected range. }, }, }, -``` +```` ## Use custom symbols + For example, use **kitty** branch symbols [more detail](https://github.com/kovidgoyal/kitty/pull/7681) + ```lua symbols = { merge_commit = '', @@ -177,26 +198,45 @@ For example, use **kitty** branch symbols [more detail](https://github.com/kovid - `:GitGraph [args]` - Open single-pane git graph - `:GitGraphDual [args]` - Open dual-pane git graph +- `:GitGraphRefresh` - Refresh dual-pane git graph (same as ``) +- `:GitGraphRefreshSingle` - Refresh single-pane git graph (same as ``) # Keymaps -... more keymaps to come ... +### Single-Pane Mode + +| Key | Action | +|-----|--------| +| `` | Select commit under cursor | +| `` | Refresh single-pane gitgraph | + +### Dual-Pane Mode + +| Key | Action | +|-----|--------| +| `` | Switch between graph and text panes | +| `gs` | Toggle scroll synchronization | +| `` | Select commit under cursor | +| `j/k` | Navigate up/down (synchronized) | +| `h/l` | Horizontal scroll (independent per pane) | +| `` | Refresh dual-pane gitgraph | +| `q` | Close dual-pane tab | # Highlights Groups ## commit information - - 'GitGraphHash' - - 'GitGraphTimestamp' - - 'GitGraphAuthor' - - 'GitGraphBranchName' - - 'GitGraphBranchTag' - - 'GitGraphBranchMsg' +- 'GitGraphHash' +- 'GitGraphTimestamp' +- 'GitGraphAuthor' +- 'GitGraphBranchName' +- 'GitGraphBranchTag' +- 'GitGraphBranchMsg' ## branch colors - - 'GitGraphBranch1' - - 'GitGraphBranch2' - - 'GitGraphBranch3' - - 'GitGraphBranch4' - - 'GitGraphBranch5' +- 'GitGraphBranch1' +- 'GitGraphBranch2' +- 'GitGraphBranch3' +- 'GitGraphBranch4' +- 'GitGraphBranch5' diff --git a/lua/gitgraph.lua b/lua/gitgraph.lua index cc73c8a..65bf2ad 100644 --- a/lua/gitgraph.lua +++ b/lua/gitgraph.lua @@ -37,6 +37,18 @@ function M.draw_dual(options, args) return require('gitgraph.draw_dual').draw(M.config, options, args) end +--- Refreshes the gitgraph in dual-pane mode +---@return nil +function M.refresh_dual() + return require('gitgraph.draw_dual').refresh() +end + +--- Refreshes the gitgraph in single-pane mode +---@return nil +function M.refresh() + return require('gitgraph.draw').refresh() +end + --- Tests the gitgraph plugin function M.test() local lines, _failure = require('gitgraph.tests').run_tests(M.config.symbols, M.config.format.fields) diff --git a/lua/gitgraph/draw.lua b/lua/gitgraph/draw.lua index 9a2ac22..f372a50 100644 --- a/lua/gitgraph/draw.lua +++ b/lua/gitgraph/draw.lua @@ -10,6 +10,11 @@ local M = {} function M.draw(config, options, args) M.graph = {} + -- Store parameters for refresh + M.last_config = config + M.last_options = options + M.last_args = args + local so = os.clock() if utils.check_cmd('git --version') then @@ -34,11 +39,11 @@ function M.draw(config, options, args) assert(buf) vim.api.nvim_win_set_buf(0, buf) - vim.api.nvim_set_option_value('modifiable', true, { buf = buf }) -- make modifiable - vim.api.nvim_set_option_value('buflisted', false, { buf = buf }) -- unlisted + vim.api.nvim_set_option_value('modifiable', true, { buf = buf }) -- make modifiable + vim.api.nvim_set_option_value('buflisted', false, { buf = buf }) -- unlisted vim.api.nvim_set_option_value('wrap', false, { scope = 'local' }) -- turn off linewrap - vim.api.nvim_buf_clear_namespace(buf, -1, 0, -1) -- clear highlights + vim.api.nvim_buf_clear_namespace(buf, -1, 0, -1) -- clear highlights -- clear do @@ -92,4 +97,25 @@ function M.draw(config, options, args) log.info('total dur:', tot_dur * 1000, 'ms') end +-- Store last used parameters for refresh +M.last_config = nil +M.last_options = nil +M.last_args = nil + +-- Refresh function for single-pane mode +function M.refresh() + if not M.last_config or not M.last_options or not M.last_args then + print('GitGraph: No previous session to refresh') + return + end + + -- Check if buffer is still valid + if not M.buf or not vim.api.nvim_buf_is_valid(M.buf) then + print('GitGraph: Buffer is no longer valid') + return + end + + M.draw(M.last_config, M.last_options, M.last_args) +end + return M diff --git a/lua/gitgraph/draw_dual.lua b/lua/gitgraph/draw_dual.lua index 9425675..5748703 100644 --- a/lua/gitgraph/draw_dual.lua +++ b/lua/gitgraph/draw_dual.lua @@ -176,13 +176,40 @@ local function apply_dual_buffer_mappings(graph_buf, text_buf, graph_data, hooks vim.keymap.set('n', 'q', function() vim.cmd('tabclose') end, { buffer = buf, desc = 'close dual-pane tab' }) + + -- Refresh dual-pane + vim.keymap.set('n', '', function() + M.refresh() + end, { buffer = buf, desc = 'refresh dual-pane gitgraph' }) end end ----@param config I.GGConfig ----@param options I.DrawOptions ----@param args I.GitLogArgs -function M.draw(config, options, args) +-- Store last used parameters for refresh +M.last_config = nil +M.last_options = nil +M.last_args = nil + +-- Refresh function for dual-pane mode +function M.refresh() + if not M.last_config or not M.last_options or not M.last_args then + print('GitGraph: No previous dual-pane session to refresh') + return + end + + -- Check if windows and buffers are still valid + if not M.graph_win or not vim.api.nvim_win_is_valid(M.graph_win) or + not M.text_win or not vim.api.nvim_win_is_valid(M.text_win) or + not M.graph_buf or not vim.api.nvim_buf_is_valid(M.graph_buf) or + not M.text_buf or not vim.api.nvim_buf_is_valid(M.text_buf) then + print('GitGraph: Dual-pane windows/buffers are no longer valid') + return + end + + M.draw_content(M.last_config, M.last_options, M.last_args) +end + +-- Helper function to draw content (separated from tab/window creation) +function M.draw_content(config, options, args) if utils.check_cmd('git --version') then log.error('git command not found, please install it') return @@ -193,24 +220,10 @@ function M.draw(config, options, args) return end - -- Open in new tab for dual-pane mode - vim.cmd('tabnew') - - -- Create new buffers for this tab - M.graph_buf = vim.api.nvim_create_buf(false, true) - M.text_buf = vim.api.nvim_create_buf(false, true) - - -- Create vertical split layout - vim.cmd('vsplit') - - -- Set up graph window (left) - M.graph_win = vim.api.nvim_get_current_win() - vim.api.nvim_win_set_buf(M.graph_win, M.graph_buf) - - -- Set up text window (right) - vim.cmd('wincmd l') - M.text_win = vim.api.nvim_get_current_win() - vim.api.nvim_win_set_buf(M.text_win, M.text_buf) + -- Store parameters for refresh + M.last_config = config + M.last_options = options + M.last_args = args -- Make buffers modifiable for editing vim.api.nvim_set_option_value('modifiable', true, { buf = M.graph_buf }) @@ -238,7 +251,7 @@ function M.draw(config, options, args) -- Apply highlights asynchronously local function apply_highlights() vim.api.nvim_buf_clear_namespace(M.graph_buf, NS, 0, -1) - vim.api.nvim_buf_clear_namespace(M.text_buf, NS, 0, -1) + vim.api.nvim_buf_clear_namespace(M.text_buf, NS, 0, -1) -- Graph highlights for _, hl in ipairs(graph_highlights) do if hl.start and hl.stop and hl.start >= 0 and hl.stop >= hl.start then @@ -290,16 +303,43 @@ function M.draw(config, options, args) apply_graph_buffer_options(M.graph_buf, M.graph_win) apply_text_buffer_options(M.text_buf, M.text_win) - -- Apply mappings - apply_dual_buffer_mappings(M.graph_buf, M.text_buf, graph, config.hooks) - - -- Set up scroll synchronization - setup_scroll_sync() - -- Set focus to graph window if M.graph_win and vim.api.nvim_win_is_valid(M.graph_win) then vim.api.nvim_set_current_win(M.graph_win) end end +---@param config I.GGConfig +---@param options I.DrawOptions +---@param args I.GitLogArgs +function M.draw(config, options, args) + -- Open in new tab for dual-pane mode + vim.cmd('tabnew') + + -- Create new buffers for this tab + M.graph_buf = vim.api.nvim_create_buf(false, true) + M.text_buf = vim.api.nvim_create_buf(false, true) + + -- Create vertical split layout + vim.cmd('vsplit') + + -- Set up graph window (left) + M.graph_win = vim.api.nvim_get_current_win() + vim.api.nvim_win_set_buf(M.graph_win, M.graph_buf) + + -- Set up text window (right) + vim.cmd('wincmd l') + M.text_win = vim.api.nvim_get_current_win() + vim.api.nvim_win_set_buf(M.text_win, M.text_buf) + + -- Draw content using helper function + M.draw_content(config, options, args) + + -- Apply mappings + apply_dual_buffer_mappings(M.graph_buf, M.text_buf, M.graph, config.hooks) + + -- Set up scroll synchronization + setup_scroll_sync() +end + return M diff --git a/lua/gitgraph/utils.lua b/lua/gitgraph/utils.lua index e800154..471310a 100644 --- a/lua/gitgraph/utils.lua +++ b/lua/gitgraph/utils.lua @@ -68,7 +68,7 @@ function M.resolve_bi_crossing(prev_commit_row, prev_connector_row, commit_row, -- B A ⓚ │ -- a A ⓶─────────╯ -- A ⓚ - local prev_prev_row = prev_connector_row -- graph[#graph - 2] + local prev_prev_row = prev_connector_row -- graph[#graph - 2] local prev_prev_prev_row = prev_commit_row -- graph[#graph - 3] assert(prev_prev_row and prev_prev_prev_row) do @@ -332,6 +332,10 @@ function M.apply_buffer_mappings(buf_id, graph, hooks) hooks.on_select_range_commit(from_commit, to_commit) end end, { buffer = buf_id, desc = 'select range of commit' }) + + vim.keymap.set('n', '', function() + require('gitgraph.draw').refresh() + end, { buffer = buf_id, desc = 'refresh gitgraph' }) end ---@param cmd string diff --git a/plugin/gitgraph.lua b/plugin/gitgraph.lua index b7441f5..767e7c6 100644 --- a/plugin/gitgraph.lua +++ b/plugin/gitgraph.lua @@ -55,6 +55,22 @@ local function setup_commands() end }) end + + -- Register GitGraphRefresh command for dual-pane + vim.api.nvim_create_user_command('GitGraphRefresh', function() + local gitgraph = require('gitgraph') + gitgraph.refresh_dual() + end, { + desc = 'Refresh GitGraph dual-pane mode', + }) + + -- Register GitGraphRefreshSingle command for single-pane + vim.api.nvim_create_user_command('GitGraphRefreshSingle', function() + local gitgraph = require('gitgraph') + gitgraph.refresh() + end, { + desc = 'Refresh GitGraph single-pane mode', + }) end -- Setup commands when plugin loads