diff --git a/README.md b/README.md
index 3b2b5bcb..64424633 100644
--- a/README.md
+++ b/README.md
@@ -1,14 +1,13 @@
-# VGit :zap:
+# VGit
+
- Visual Git Plugin for Neovim to enhance your git experience.
+ Visual Git Plugin for Neovim to enhance your git experience
|
-
-
@@ -18,40 +17,27 @@
-
-
-## Highlighted Features
-- Gutter changes annotation to highlight any local (unpublished) changes or lines changed by the most recent commit
-- Current line blame as virtual text
-- See the details of a blame related to the current line (`:VGit buffer_blame_preview`)
-- See the blames of a buffer in a VGit preview (`:VGit buffer_gutter_blame_preview`)
-- See all hunks in a VGit preview (`:VGit buffer_hunk_preview`)
-- See the buffer changes in a VGit preview (`:VGit buffer_diff_preview`)
-- See the buffer changes that were staged in a VGit preview (`:VGit buffer_staged_diff_preview`)
-- See all the history of a buffer in a VGit preview (`:VGit buffer_history`)
-- See changes in your project in a VGit diff preview (`:VGit project_diff_preview`)
-- See changes in your project in a quickfix list (`:VGit project_hunks_qf`)
-- Enhance your workflow by using VGit's buffer navigation `:VGit hunk_up` and `:VGit hunk_down` that can be used on any VGit previews with changes.
-If you have Telescope feel free to run `:VGit actions` to quickly checkout your execution options.
-
-
-
+
+
-## Supported Neovim Versions:
+## Requirements
- Neovim **>=** 0.5
-
-## Supported Opperating System:
-- linux-gnu*
-- Darwin
+- Git **>=** 2.18.0
+- Operating System:
+ - linux-gnu*
+ - Darwin
## Prerequisites
- [Git](https://git-scm.com/)
- [plenary.nvim](https://github.com/nvim-lua/plenary.nvim)
## Recommended Settings
-- `vim.o.updatetime = 100` (see :help updatetime).
-- `vim.wo.signcolumn = 'yes'` (see :help signcolumn)
+```lua
+vim.o.updatetime = 300
+vim.o.incsearch = false
+vim.wo.signcolumn = 'yes'
+```
## Installation
Default installation via Packer.
@@ -64,197 +50,225 @@ use {
}
```
-Lazy loading via Packer.
-```lua
-use({
- 'tanvirtin/vgit.nvim',
- event = 'BufWinEnter',
- requires = {
- 'nvim-lua/plenary.nvim',
- },
- config = function()
- require('vgit').setup()
- end,
-})
-```
-
## Setup
You must instantiate the plugin in order for the features to work.
```lua
require('vgit').setup()
```
-To embed the above code snippet in a .vim file wrap it in lua << EOF code-snippet EOF:
+To embed the above code snippet in a .vim file wrap it in lua << EOF code-snippet EOF.
```lua
lua << EOF
require('vgit').setup()
EOF
```
-## Themes
-Predefined supported themes:
-- [tokyonight](https://github.com/folke/tokyonight.nvim)
-- [monokai](https://github.com/tanvirtin/monokai.nvim)
-
-Colorscheme definitions can be found in `lua/vgit/themes/`, feel free to open a pull request with your own colorscheme!
-
-## Layouts
-Predefined supported layouts:
-- default (Full screen previews)
-
-Layout definitions can be found in `lua/vgit/layouts/`, feel free to open a pull request with your own layout!
-
-## API
-| Function Name | Description |
-|---------------|-------------|
-| setup | Sets up the plugin for success |
-| toggle_buffer_hunks | Shows hunk signs on buffers/Hides hunk signs on buffers |
-| toggle_buffer_blames | Enables blames feature on buffers/Disables blames feature on buffers |
-| toggle_diff_preference | Switches between "horizontal" and "vertical" layout for previews |
-| buffer_stage | Stages a buffer you are currently on |
-| buffer_unstage | Unstages a buffer you are currently on |
-| buffer_diff_preview | Opens a diff preview of the changes in the current buffer |
-| buffer_staged_diff_preview | Shows staged changes in a preview window |
-| buffer_hunk_preview | Gives you a view through which you can navigate and see the current hunk or other hunks |
-| buffer_history_preview | Opens a buffer preview along with a table of logs, enabling users to see different iterations of the buffer in the git history |
-| buffer_blame_preview | Opens a preview detailing the blame of the line that the user is currently on |
-| buffer_gutter_blame_preview | Opens a preview which shows the blames related to all the lines of a buffer |
-| buffer_reset | Resets the current buffer to HEAD |
-| buffer_hunk_stage | Stages a hunk, if cursor is over it |
-| buffer_hunk_reset | Removes the hunk from the buffer, if cursor is over it |
-| project_hunks_qf | Opens a populated quickfix window with all the hunks of the project |
-| project_diff_preview | Opens a preview listing all the files that have been changed |
-| hunk_down | Navigate downward through a hunk, this works on any view with diff highlights |
-| hunk_up | Navigate upwards through a hunk, this works on any view with diff highlights |
-| get_diff_base | Returns the current diff base that all diff and hunks are being compared for all buffers |
-| get_diff_preference | Returns the current diff preference of the diff, the value will either be "horizontal" or "vertical" |
-| get_diff_strategy | Returns the current diff strategy used to compute hunk signs and buffer preview, the value will either be "remote" or "index" |
-| set_diff_base | Sets the current diff base to a different commit, going forward all future hunks and diffs for a given buffer will be against this commit |
-| set_diff_strategy | Sets the diff strategy that will be used to show hunk signs and buffer preview, the value can only be "remote" or "index" |
-| show_debug_logs | Shows all errors that has occured during program execution |
-
-## Advanced Setup
+Highlights, signs, keymappings are few examples of what can be configured in VGit. Advanced setup below shows you all configurable parameters in VGit.
```lua
-local vgit = require('vgit')
-local utils = require('vgit.utils')
-
-vgit.setup({
- debug = false, -- Only enable this to trace issues related to the app,
- keymaps = {
- ['n '] = 'hunk_up',
- ['n '] = 'hunk_down',
- ['n g'] = 'actions',
- ['n gs'] = 'buffer_hunk_stage',
- ['n gr'] = 'buffer_hunk_reset',
- ['n gp'] = 'buffer_hunk_preview',
- ['n gb'] = 'buffer_blame_preview',
- ['n gf'] = 'buffer_diff_preview',
- ['n gh'] = 'buffer_history_preview',
- ['n gu'] = 'buffer_reset',
- ['n gg'] = 'buffer_gutter_blame_preview',
- ['n gd'] = 'project_diff_preview',
- ['n gq'] = 'project',
- ['n gx'] = 'toggle_diff_preference',
+require('vgit').setup({
+ keymaps = {
+ ['n '] = 'hunk_up',
+ ['n '] = 'hunk_down',
+ ['n g'] = 'actions',
+ ['n gs'] = 'buffer_hunk_stage',
+ ['n gr'] = 'buffer_hunk_reset',
+ ['n gp'] = 'buffer_hunk_preview',
+ ['n gb'] = 'buffer_blame_preview',
+ ['n gf'] = 'buffer_diff_preview',
+ ['n gh'] = 'buffer_history_preview',
+ ['n gu'] = 'buffer_reset',
+ ['n gg'] = 'buffer_gutter_blame_preview',
+ ['n gl'] = 'project_hunks_preview',
+ ['n gd'] = 'project_diff_preview',
+ ['n gq'] = 'project_hunks_qf',
+ ['n gx'] = 'toggle_diff_preference',
+ },
+ settings = {
+ hls = {
+ GitBackgroundPrimary = 'NormalFloat',
+ GitBackgroundSecondary = {
+ gui = nil,
+ fg = nil,
+ bg = nil,
+ sp = nil,
+ override = false,
+ },
+ GitBorder = 'LineNr',
+ GitLineNr = 'LineNr',
+ GitComment = 'Comment',
+ GitSignsAdd = {
+ gui = nil,
+ fg = '#d7ffaf',
+ bg = nil,
+ sp = nil,
+ override = false,
+ },
+ GitSignsChange = {
+ gui = nil,
+ fg = '#7AA6DA',
+ bg = nil,
+ sp = nil,
+ override = false,
+ },
+ GitSignsDelete = {
+ gui = nil,
+ fg = '#e95678',
+ bg = nil,
+ sp = nil,
+ override = false,
+ },
+ GitSignsAddLn = 'DiffAdd',
+ GitSignsDeleteLn = 'DiffDelete',
+ GitWordAdd = {
+ gui = nil,
+ fg = nil,
+ bg = '#5d7a22',
+ sp = nil,
+ override = false,
+ },
+ GitWordDelete = {
+ gui = nil,
+ fg = nil,
+ bg = '#960f3d',
+ sp = nil,
+ override = false,
+ },
+ },
+ live_blame = {
+ enabled = true,
+ debounce_ms = 300,
+ format = function(blame, git_config)
+ local config_author = git_config['user.name']
+ local author = blame.author
+ if config_author == author then
+ author = 'You'
+ end
+ local function round(x)
+ return x >= 0 and math.floor(x + 0.5) or math.floor(x - 0.5)
+ end
+ local time = os.difftime(os.time(), blame.author_time) / (24 * 60 * 60)
+ local time_format = string.format('%s days ago', round(time))
+ local time_divisions = {
+ { 24, 'hours' },
+ { 60, 'minutes' },
+ { 60, 'seconds' },
+ }
+ local division_counter = 1
+ while time < 1 and division_counter ~= #time_divisions do
+ local division = time_divisions[division_counter]
+ time = time * division[1]
+ time_format = string.format('%s %s ago', round(time), division[2])
+ division_counter = division_counter + 1
+ end
+ local commit_message = blame.commit_message
+ if not blame.committed then
+ author = 'You'
+ commit_message = 'Uncommitted changes'
+ local info = string.format('%s • %s', author, commit_message)
+ return string.format(' %s', info)
+ end
+ local max_commit_message_length = 255
+ if #commit_message > max_commit_message_length then
+ commit_message = commit_message:sub(1, max_commit_message_length) .. '...'
+ end
+ local info = string.format(
+ '%s, %s • %s',
+ author,
+ time_format,
+ commit_message
+ )
+ return string.format(' %s', info)
+ end,
},
- controller = {
- hunks_enabled = true,
- blames_enabled = true,
- diff_strategy = 'index',
- diff_preference = 'horizontal',
- predict_hunk_signs = true,
- predict_hunk_throttle_ms = 300,
- predict_hunk_max_lines = 50000,
- blame_line_throttle_ms = 150,
- action_delay_ms = 300,
+ live_gutter = {
+ enabled = true,
+ debounce_ms = 50,
+ },
+ scene = {
+ diff_preference = 'unified',
},
- hls = vgit.themes.tokyonight,
signs = {
- VGitViewSignAdd = {
- name = 'DiffAdd',
- line_hl = 'DiffAdd',
- text_hl = nil,
- num_hl = nil,
- icon = nil,
- text = '',
+ priority = 10,
+ definitions = {
+ GitSignsAddLn = {
+ linehl = 'GitSignsAddLn',
+ texthl = nil,
+ numhl = nil,
+ icon = nil,
+ text = '',
},
- VGitViewSignRemove = {
- name = 'DiffDelete',
- line_hl = 'DiffDelete',
- text_hl = nil,
- num_hl = nil,
- icon = nil,
- text = '',
+ GitSignsDeleteLn = {
+ linehl = 'GitSignsDeleteLn',
+ texthl = nil,
+ numhl = nil,
+ icon = nil,
+ text = '',
},
- VGitSignAdd = {
- name = 'VGitSignAdd',
- text_hl = 'VGitSignAdd',
- num_hl = nil,
- icon = nil,
- line_hl = nil,
- text = '┃',
+ GitSignsAdd = {
+ texthl = 'GitSignsAdd',
+ numhl = nil,
+ icon = nil,
+ linehl = nil,
+ text = '┃',
},
- VGitSignRemove = {
- name = 'VGitSignRemove',
- text_hl = 'VGitSignRemove',
- num_hl = nil,
- icon = nil,
- line_hl = nil,
- text = '┃',
+ GitSignsDelete = {
+ texthl = 'GitSignsDelete',
+ numhl = nil,
+ icon = nil,
+ linehl = nil,
+ text = '┃',
},
- VGitSignChange = {
- name = 'VGitSignChange',
- text_hl = 'VGitSignChange',
- num_hl = nil,
- icon = nil,
- line_hl = nil,
- text = '┃',
+ GitSignsChange = {
+ texthl = 'GitSignsChange',
+ numhl = nil,
+ icon = nil,
+ linehl = nil,
+ text = '┃',
},
- },
- render = {
- layout = vgit.layouts.default,
- sign = {
- priority = 10,
- hls = {
- add = 'VGitSignAdd',
- remove = 'VGitSignRemove',
- change = 'VGitSignChange',
- },
+ },
+ usage = {
+ scene = {
+ add = 'GitSignsAddLn',
+ remove = 'GitSignsDeleteLn',
},
- line_blame = {
- hl = 'Comment',
- format = function(blame, git_config)
- local config_author = git_config['user.name']
- local author = blame.author
- if config_author == author then
- author = 'You'
- end
- local time = os.difftime(os.time(), blame.author_time) / (24 * 60 * 60)
- local time_format = string.format('%s days ago', utils.round(time))
- local time_divisions = { { 24, 'hours' }, { 60, 'minutes' }, { 60, 'seconds' } }
- local division_counter = 1
- while time < 1 and division_counter ~= #time_divisions do
- local division = time_divisions[division_counter]
- time = time * division[1]
- time_format = string.format('%s %s ago', utils.round(time), division[2])
- division_counter = division_counter + 1
- end
- local commit_message = blame.commit_message
- if not blame.committed then
- author = 'You'
- commit_message = 'Uncommitted changes'
- local info = string.format('%s • %s', author, commit_message)
- return string.format(' %s', info)
- end
- local max_commit_message_length = 255
- if #commit_message > max_commit_message_length then
- commit_message = commit_message:sub(1, max_commit_message_length) .. '...'
- end
- local info = string.format('%s, %s • %s', author, time_format, commit_message)
- return string.format(' %s', info)
- end,
+ main = {
+ add = 'GitSignsAdd',
+ remove = 'GitSignsDelete',
+ change = 'GitSignsChange',
},
+ },
},
+ symbols = {
+ void = '⣿',
+ },
+ }
})
```
-
+## API
+| Function Name | Description |
+|---------------|-------------|
+| setup | Sets VGit up for you. This plugin cannot be used before this function has been called. |
+| version | Returns a table with VGit's current version information |
+| buffer_hunk_preview | Opens a diff preview showing the diff of the current buffer in comparison to that found in index. This preview will open up in a smaller window relative to where your cursor is. |
+| buffer_diff_preview | Opens a diff preview showing the diff of the current buffer in comparison to that found in index. If the command is called while being on a hunk, the window will open focused on the diff of that hunk. |
+| buffer_history_preview | Opens a diff preview along with a table of logs, enabling users to see different iterations of the file through it's lifecycle in git. |
+| buffer_blame_preview | Opens a preview detailing the blame of the line that based on the cursor position within the buffer. |
+| buffer_gutter_blame_preview | Opens a preview which shows all the blames related to the lines of the buffer. |
+| buffer_diff_staged_preview | Opens a diff preview showing the diff of the staged changes in the current buffer. |
+| buffer_hunk_staged_preview | Opens a diff preview showing the diff of the staged changes in the current buffer. This preview will open up in a smaller window relative to where your cursor is. |
+| project_diff_preview | Opens a diff preview along with a table of all the files that have been changed, enabling users to see all the files that were changed in the current project. |
+| project_hunks_preview | Opens a diff preview along with a table of all the current hunks in the project. Users can use this preview to cycle through all the hunks. |
+| project_hunks_qf | Populate the quickfix list with hunks. Automatically opens the quickfix window. |
+| buffer_hunk_stage | Stages a hunk, if a cursor is on the hunk. |
+| buffer_hunk_reset | Removes all changes made in the buffer on the hunk the cursor is currently on to what exists in HEAD. |
+| buffer_stage | Stages all changes in the current buffer. |
+| buffer_unstage | Unstages all changes in the current buffer. |
+| buffer_reset | Removes all current changes in the buffer and resets it to the version in HEAD. |
+| hunk_up | Moves the cursor to the hunk above the current cursor position. |
+| hunk_down | Moves the cursor to the hunk below the current cursor position. |
+| toggle_diff_preference | Used to switch between "split" and "unified" diff. |
+| toggle_buffer_hunks | Enables/disables git gutter signs. |
+| toggle_buffer_blames | Used to switch between "split" and "unified" diff. |
+| enable_tracing | Enables debug logs that are used internally by VGit to make suppressed logs visible. |
+| disable_tracing | Disables debug logs that are used internally by VGit to make suppressed logs visible. |
diff --git a/doc/vgit.txt b/doc/vgit.txt
index e65fd56b..6dd79692 100644
--- a/doc/vgit.txt
+++ b/doc/vgit.txt
@@ -2,125 +2,210 @@
Supported Neovim Versions: >= 0.5.0
+Supported Git Versions >= 2.18.0
+
Author: Tanvir Islam
License: MIT license
==============================================================================
-INTRODUCTION *vgit*
+INTRODUCTION *vgit*
-VGit is a git integration plugin written in Lua for Neovim. It aims to
-beautify and enhance your git experience.
+VGit is a git integration plugin written for Neovim. The goal of this plugin
+is to visually enhance your git experience.
==============================================================================
-USAGE *vgit-usage*
+USAGE *vgit-usage*
For a basic setup with no configuration:
>
- require('vgit').setup()
+ require('vgit').setup()
-VGit can also be customized to your heart's content through it's advanced
-configuration:
+More advanced configuration:
>
- local vgit = require('vgit')
- local utils = require('vgit.utils')
-
- vgit.setup({
- debug = false,
- keymaps = {
- ['n '] = 'hunk_up',
- ['n '] = 'hunk_down',
- ['n g'] = 'actions',
- ['n gs'] = 'buffer_hunk_stage',
- ['n gr'] = 'buffer_hunk_reset',
- ['n gp'] = 'buffer_hunk_preview',
- ['n gb'] = 'buffer_blame_preview',
- ['n gf'] = 'buffer_diff_preview',
- ['n gh'] = 'buffer_history_preview',
- ['n gu'] = 'buffer_reset',
- ['n gg'] = 'buffer_gutter_blame_preview',
- ['n gd'] = 'project_diff_preview',
- ['n gq'] = 'project',
- ['n gx'] = 'toggle_diff_preference',
+ require('vgit').setup({
+ keymaps = {
+ ['n '] = 'hunk_up',
+ ['n '] = 'hunk_down',
+ ['n g'] = 'actions',
+ ['n gs'] = 'buffer_hunk_stage',
+ ['n gr'] = 'buffer_hunk_reset',
+ ['n gp'] = 'buffer_hunk_preview',
+ ['n gb'] = 'buffer_blame_preview',
+ ['n gf'] = 'buffer_diff_preview',
+ ['n gh'] = 'buffer_history_preview',
+ ['n gu'] = 'buffer_reset',
+ ['n gg'] = 'buffer_gutter_blame_preview',
+ ['n gl'] = 'project_hunks_preview',
+ ['n gd'] = 'project_diff_preview',
+ ['n gq'] = 'project_hunks_qf',
+ ['n gx'] = 'toggle_diff_preference',
+ },
+ settings = {
+ hls = {
+ GitBackgroundPrimary = 'NormalFloat',
+ GitBackgroundSecondary = {
+ gui = nil,
+ fg = nil,
+ bg = nil,
+ sp = nil,
+ override = false,
+ },
+ GitBorder = 'LineNr',
+ GitLineNr = 'LineNr',
+ GitComment = 'Comment',
+ GitSignsAdd = {
+ gui = nil,
+ fg = '#d7ffaf',
+ bg = nil,
+ sp = nil,
+ override = false,
+ },
+ GitSignsChange = {
+ gui = nil,
+ fg = '#7AA6DA',
+ bg = nil,
+ sp = nil,
+ override = false,
+ },
+ GitSignsDelete = {
+ gui = nil,
+ fg = '#e95678',
+ bg = nil,
+ sp = nil,
+ override = false,
+ },
+ GitSignsAddLn = 'DiffAdd',
+ GitSignsDeleteLn = 'DiffDelete',
+ GitWordAdd = {
+ gui = nil,
+ fg = nil,
+ bg = '#5d7a22',
+ sp = nil,
+ override = false,
+ },
+ GitWordDelete = {
+ gui = nil,
+ fg = nil,
+ bg = '#960f3d',
+ sp = nil,
+ override = false,
+ },
+ },
+ live_blame = {
+ enabled = true,
+ debounce_ms = 300,
+ format = function(blame, git_config)
+ local config_author = git_config['user.name']
+ local author = blame.author
+ if config_author == author then
+ author = 'You'
+ end
+ local function round(x)
+ return x >= 0 and math.floor(x + 0.5) or math.floor(x - 0.5)
+ end
+ local time = os.difftime(os.time(), blame.author_time) / (24 * 60 * 60)
+ local time_format = string.format('%s days ago', round(time))
+ local time_divisions = {
+ { 24, 'hours' },
+ { 60, 'minutes' },
+ { 60, 'seconds' },
+ }
+ local division_counter = 1
+ while time < 1 and division_counter ~= #time_divisions do
+ local division = time_divisions[division_counter]
+ time = time * division[1]
+ time_format = string.format('%s %s ago', round(time), division[2])
+ division_counter = division_counter + 1
+ end
+ local commit_message = blame.commit_message
+ if not blame.committed then
+ author = 'You'
+ commit_message = 'Uncommitted changes'
+ local info = string.format('%s • %s', author, commit_message)
+ return string.format(' %s', info)
+ end
+ local max_commit_message_length = 255
+ if #commit_message > max_commit_message_length then
+ commit_message = commit_message:sub(1, max_commit_message_length) .. '...'
+ end
+ local info = string.format(
+ '%s, %s • %s',
+ author,
+ time_format,
+ commit_message
+ )
+ return string.format(' %s', info)
+ end,
},
- controller = {
- hunks_enabled = true,
- blames_enabled = true,
- diff_strategy = 'index',
- diff_preference = 'horizontal',
- predict_hunk_signs = true,
- predict_hunk_throttle_ms = 300,
- predict_hunk_max_lines = 50000,
- blame_line_throttle_ms = 150,
- action_delay_ms = 300,
+ live_gutter = {
+ enabled = true,
+ debounce_ms = 50,
},
- hls = vgit.themes.tokyonight,
- sign = {
- VGitViewSignAdd = {
- name = 'DiffAdd',
- line_hl = 'DiffAdd',
- text_hl = nil,
- num_hl = nil,
- icon = nil,
- text = '',
+ scene = {
+ diff_preference = 'unified',
+ },
+ signs = {
+ priority = 10,
+ definitions = {
+ GitSignsAddLn = {
+ linehl = 'GitSignsAddLn',
+ texthl = nil,
+ numhl = nil,
+ icon = nil,
+ text = '',
},
- VGitViewSignRemove = {
- name = 'DiffDelete',
- line_hl = 'DiffDelete',
- text_hl = nil,
- num_hl = nil,
- icon = nil,
- text = '',
+ GitSignsDeleteLn = {
+ linehl = 'GitSignsDeleteLn',
+ texthl = nil,
+ numhl = nil,
+ icon = nil,
+ text = '',
},
- VGitSignAdd = {
- name = 'VGitSignAdd',
- text_hl = 'VGitSignAdd',
- num_hl = nil,
- icon = nil,
- line_hl = nil,
- text = '┃',
+ GitSignsAdd = {
+ texthl = 'GitSignsAdd',
+ numhl = nil,
+ icon = nil,
+ linehl = nil,
+ text = '┃',
},
- VGitSignRemove = {
- name = 'VGitSignRemove',
- text_hl = 'VGitSignRemove',
- num_hl = nil,
- icon = nil,
- line_hl = nil,
- text = '┃',
+ GitSignsDelete = {
+ texthl = 'GitSignsDelete',
+ numhl = nil,
+ icon = nil,
+ linehl = nil,
+ text = '┃',
},
- VGitSignChange = {
- name = 'VGitSignChange',
- text_hl = 'VGitSignChange',
- num_hl = nil,
- icon = nil,
- line_hl = nil,
- text = '┃',
+ GitSignsChange = {
+ texthl = 'GitSignsChange',
+ numhl = nil,
+ icon = nil,
+ linehl = nil,
+ text = '┃',
},
- },
- render = {
- layout = vgit.layouts.default,
- sign = {
- priority = 10,
- hls = {
- add = 'VGitSignAdd',
- remove = 'VGitSignRemove',
- change = 'VGitSignChange',
- },
+ },
+ usage = {
+ scene = {
+ add = 'GitSignsAddLn',
+ remove = 'GitSignsDeleteLn',
},
- line_blame = {
- hl = 'Comment',
- format = function(blame, git_config)
- -- Return your own blame string to show
- return ''
- end,
+ main = {
+ add = 'GitSignsAdd',
+ remove = 'GitSignsDelete',
+ change = 'GitSignsChange',
},
+ },
},
- })
+ symbols = {
+ void = '⣿',
+ },
+ }
+ })
==============================================================================
-COMMAND *vgit-command*
+COMMAND *vgit-command*
- *:VGit*
+ *:VGit*
:VGit {subcommand} {arguments} Runs a command exposed by the plugin. Typing
VGit followed by tab will show you all the |vgit-functions| available to you.
@@ -128,81 +213,85 @@ VGit followed by tab will show you all the |vgit-functions| available to you.
`:lua require('vgit').{subcommand}({arguments})`
==============================================================================
-FUNCTIONS *vgit-functions*
+FUNCTIONS *vgit-functions*
-setup({config}) *vgit.setup()*
- Sets VGit up for you. This command needs to run before any
- VGit functionality can be used.
+setup({config}) *vgit.setup()*
+ Sets VGit up for you. This plugin cannot be used before this
+ function has been called.
Parameters: ~
{config} Table object containing configuration. See
|vgit-usage| for more details.
-buffer_hunk_preview() *vgit.buffer_hunk_preview()*
- Opens a diff preview showing the horizontal diff of the
- current buffer. If the command is executed when the cursor is
- on top of a hunk sign, the preview will focus on that hunk
- when opened. Users can use the navigation commands to travel
- through the hunks. Please see |vgit-navigation|. This preview
- is also dependent on the the diff strategy and diff
- base, please refer to |vgit-diff-base|.
-
-buffer_diff_preview() *vgit.buffer_diff_preview()*
- Opens a diff preview showing the diff of the
- current buffer. The changes seen in the preview is based on
- If the command is executed when the cursor is
- on top of a hunk sign, the preview will focus on that hunk
- when opened. Users can use the navigation commands to travel
- through the hunks. Please see |vgit-navigation|. This preview
- is also dependent on the the diff strategy and diff
- base, please refer to |vgit-diff-base|.
-
-buffer_history_preview() *vgit.buffer_history_preview()*
+version() *vgit.version()*
+ Returns a table with VGit's current version information.
+
+buffer_hunk_preview() *vgit.buffer_hunk_preview()*
+ Opens a diff preview showing the diff of the current buffer
+ in comparison to that found in index. This preview will open up in
+ a smaller window relative to where your cursor is. If the
+ command is called while being on a hunk, the window will open
+ focused on the diff of that hunk.
+
+buffer_diff_preview() *vgit.buffer_diff_preview()*
+ Opens a diff preview showing the diff of the current buffer in
+ comparison to that found in index. If the command is called
+ while being on a hunk, the window will open focused on the
+ diff of that hunk.
+
+buffer_history_preview() *vgit.buffer_history_preview()*
Opens a diff preview along with a table of logs, enabling
users to see different iterations of the file through it's
- lifecycle in git. Users can use the navigation commands to
- travel through the hunks. Please refer to |vgit-navigation|.
+ lifecycle in git.
buffer_blame_preview() *vgit.buffer_blame_preview()*
Opens a preview detailing the blame of the line that
based on the cursor position within the buffer.
-buffer_gutter_blame_preview() *vgit.gutter_blame_preview()*
+buffer_gutter_blame_preview() *vgit.buffer_gutter_blame_preview()*
Opens a preview which shows all the blames related to the
lines of the buffer.
-buffer_staged_diff_preview() *vgit.buffer_staged_diff_preview()*
+buffer_diff_staged_preview() *vgit.buffer_staged_diff_preview()*
Opens a diff preview showing the diff of the staged changes in
- the current buffer. Users can use the navigation commands to
- travel through the hunks. Please see |vgit-navigation|. This
- preview only works when diff strategy is "index". Please refer
- to *vgit.set_diff_preference*
+ the current buffer.
-project_diff_preview() *vgit.project_diff_preview()*
+buffer_hunk_staged_preview() *vgit.buffer_staged_hunk_preview()*
+ Opens a diff preview showing the diff of the staged changes in
+ the current buffer. This preview will open up in a smaller
+ window relative to where your cursor is.
+
+project_diff_preview() *vgit.project_diff_preview()*
Opens a diff preview along with a table of all the files that
have been changed, enabling users to see all the files that
- were changed in the current project. Users can use the
- navigation commands to travel through the hunks. Please refer
- to |vgit-navigation|.
+ were changed in the current project. Users can use this view
+ to stage and unstage all files using stage_all and
+ unstage_all. Users can also trigger changes on individual
+ files using |buffer_stage|, |buffer_unstage| and
+ |buffer_reset| while being on the cursor that corresponds to
+ the file.
+
+project_hunks_preview() *vgit.project_hunks_preview()*
+ Opens a diff preview along with a table of all the current
+ hunks in the project. Useers can use this preview to cycle
+ through all the hunks. Pressing enter on a hunk will open the
+ file and focus on the corresponding hunk.
project_hunks_qf() *vgit.project_hunks_qf()*
Populate the quickfix list with hunks. Automatically opens the
quickfix window.
buffer_hunk_stage() *vgit.buffer_hunk_stage()*
- Stages a hunk, if a cursor is on the hunk sign. You can see
- the staged changes using *vgit.buffer_staged_diff_preview* .
- This function only works when diff strategy is "index". Please
- refer to *vgit.set_diff_preference* .
+ Stages a hunk, if a cursor is on the hunk.
buffer_hunk_reset({target}, {opts}) *vgit.buffer_hunk_reset()*
Removes all changes made in the buffer on the hunk the cursor
is currently on to what exists in HEAD.
-buffer_stage() *vgit.buffer_stage()*
+buffer_stage() *vgit.buffer_stage()*
Stages all changes in the current buffer.
-buffer_unstage() *vgit.buffer_unstage()*
+buffer_unstage() *vgit.buffer_unstage()*
Unstages all changes in the current buffer.
buffer_reset() *vgit.buffer_reset()*
@@ -211,102 +300,48 @@ buffer_reset() *vgit.buffer_reset()*
hunk_up() *vgit.hunk_up()*
Moves the cursor to the hunk above the current cursor
- position. This can be used on any buffer with changes in it.
+ position.
hunk_down() *vgit.hunk_down()*
Moves the cursor to the hunk below the current cursor
- position. This can be used on any buffer with changes in it.
+ position.
-toggle_buffer_hunks() *vgit.toggler_buffer_hunks()*
- Enables/disables showing signs on the current buffer related
- to git.
+toggle_diff_preference() *vgit.toggle_diff_preference()*
+ Used to switch between "split" and "unified" diff.
-toggle_diff_preference() *vgit.toggle_diff_preference()*
- Used to switch between "vertical" and "horizontal" diff.
+toggle_buffer_hunks() *vgit.toggle_buffer_hunks()*
+ Enables/disables git gutter signs.
toggle_buffer_blames() *vgit.toggle_buffer_blames()*
Enables/disables current line blame functionality that is seen
in the form of virtual texts.
-apply_highlights() *vgit.apply_highlights()*
- Applies all the highlight required for VGit to work. This
- comes in handy when you switch colorschemes and the highlights
- dissapear.
-
-show_debug_logs() *vgit.show_debug_logs()*
- If the "debug" flag is set to true using *vgit.setup* then
- executing this command will show all the logs that VGit is
- keeping track of.
-
-get_diff_preference() *vgit.get_diff_preference()*
- Returns the current diff preference, this value will either be
- "remote" or "index".
-
-get_diff_base() *vgit.get_diff_base()*
- Returns the current diff base. This value is not very useful
- if the diff preference is not "remote".
-
-get_diff_strategy() *vgit.get_diff_strategy()*
- Returns the current diff strategy, this value will either be
- "horizontal" or "vertical".
-
-set_diff_strategy({strategy}) *vgit.set_diff_strategy()*
- Diff strategy by VGit to determine how it's going to compare
- changes to retrieve the hunk. This value can either be "index"
- or "remote". Setting it to index will only show you changes
- relative to the current filesystem, where "remote" will
- compare changes to the remote repository. NOTE When the diff
- strategy is set to "remote" the base to compare it against
- will be HEAD.
-
- Parameters: ~
- {strategy} (string):
- The strategy that VGit sets and uses to compare
- diff against.
- Possible values.
- • `"index"`: Against changes in the file system.
- • `"remote"`: Against changes in remote
- repository.
-
-set_diff_base({base}) *vgit.set_diff_base()*
- This is a codependent function on the diff strategy meaning,
- it will only work if the diff strategy is "remote". Please
- refer to *set_diff_preference* . Once "remote" is set users
- can then set the base to any acceptable git commit hash and
- VGit will compare changes within the project relative to that
- base.
+enable_tracing()
+ Enables debug logs that are used internally by VGit to make
+ suppressed logs visible.
- Parameters: ~
- {base} (string):
- Any valid git commit hash.
-
-==============================================================================
-COMMAND *vgit-diff-base*
+disable_tracing()
+ Disables debug logs that are used internally by VGit to make
+ suppressed logs visible.
- *:VGit*
-VGit allows you to customize how you want to see the changes in your project.
-Please refer to:
-• *vgit.get_diff_strategy*
-• *vgit.set_diff_strategy*
-• *vgit.set_diff_base*
-• *vgit.get_diff_base*
==============================================================================
-COMMAND *vgit-navigation*
+COMMAND *vgit-navigation*
- *:VGit*
+ *:VGit*
Any VGit preview that contains changes with highlights is navigatable,
-enhancing your git workflow drastically. Please refer to *vgit.hunk_up* and
-*vgit.hunk_down* mapping these functions to "" and "" is a personal
-recommendation.
+enhancing your git workflow drastically. Please refer to |hunk_up| and
+|hunk_down| mapping these functions to "" and "" is a personal
+recommendation. Executing these commands while being on a table associated
+with a diff will also enable hunk navigations.
==============================================================================
-COMMAND *vgit-diff-preference*
+COMMAND *vgit-diff-preference*
- *:VGit*
-Diffs can be seen visually in all previews except hunk preview in two ways,
-horizontally and vertically. This is equivalent to Github's unified and split
-functionality. Please refer to *vgit.toggle_diff_preference*
+ *:VGit*
+Any VGit preview with a diff can be seen visually in two different ways,
+unified and split. Users can switch between these two styles anytime using
+|toggle_diff_preference|.
------------------------------------------------------------------------------
diff --git a/lua/vgit.lua b/lua/vgit.lua
index 0d0b4285..26035af5 100644
--- a/lua/vgit.lua
+++ b/lua/vgit.lua
@@ -1,1735 +1,367 @@
-local git = require('vgit.git')
-local themes = require('vgit.themes')
-local layouts = require('vgit.layouts')
-local renderer = require('vgit.renderer')
-local highlight = require('vgit.highlight')
-local autocmd = require('vgit.autocmd')
-local sign = require('vgit.sign')
-local key_mapper = require('vgit.key_mapper')
-local controller_store = require('vgit.stores.controller_store')
-local render_store = require('vgit.stores.render_store')
-local logger = require('vgit.logger')
-local dimensions = require('vgit.dimensions')
-local fs = require('vgit.fs')
-local preview_store = require('vgit.stores.preview_store')
-local buffer = require('vgit.buffer')
-local navigation = require('vgit.navigation')
-local Patch = require('vgit.Patch')
-local void = require('plenary.async.async').void
-local scheduler = require('plenary.async.util').scheduler
-local debounce_trailing = require('vgit.defer').debounce_trailing
-local Hunk = require('vgit.Hunk')
-local utils = require('vgit.utils')
-local change = require('vgit.change')
-local wrap = require('plenary.async.async').wrap
-
-local M = {}
-
-local store_buf =
- function(buf, filename, tracked_filename, tracked_remote_filename)
- buffer.store.add(buf)
- local filetype = fs.filetype(buf)
- if not filetype or filetype == '' then
- filetype = fs.detect_filetype(filename)
- end
- buffer.store.set(buf, 'filetype', filetype)
- buffer.store.set(buf, 'filename', filename)
- if tracked_filename and tracked_filename ~= '' then
- buffer.store.set(buf, 'tracked_filename', tracked_filename)
- buffer.store.set(buf, 'tracked_remote_filename', tracked_remote_filename)
- return
- end
- buffer.store.set(buf, 'untracked', true)
- end
-
-local attach_blames_autocmd = function(buf)
- autocmd.buf.on(
- buf,
- 'CursorHold',
- string.format(':lua _G.package.loaded.vgit._blame_line(%s)', buf)
- )
- autocmd.buf.on(
- buf,
- 'CursorMoved',
- string.format(':lua _G.package.loaded.vgit._unblame_line(%s)', buf)
- )
-end
-
-local detach_blames_autocmd = function(buf)
- autocmd.buf.off(buf, 'CursorHold')
- autocmd.buf.off(buf, 'CursorMoved')
-end
-
-local get_hunk_calculator = function()
- return (
- controller_store.get('diff_strategy') == 'remote' and git.remote_hunks
- ) or git.index_hunks
-end
-
-local calculate_hunks = function(buf)
- return get_hunk_calculator()(buffer.store.get(buf, 'tracked_filename'))
-end
-
-local get_current_hunk = function(hunks, lnum)
- for i = 1, #hunks do
- local hunk = hunks[i]
- if lnum == 1 and hunk.start == 0 and hunk.finish == 0 then
- return hunk
- end
- if lnum >= hunk.start and lnum <= hunk.finish then
- return hunk
- end
- end
-end
-
-local function ext_hunk_generation(buf, original_lines, current_lines)
- scheduler()
- if
- controller_store.get('disabled')
- or not buffer.is_valid(buf)
- or not buffer.store.contains(buf)
- then
- return
- end
- local temp_filename_b = fs.tmpname()
- local temp_filename_a = fs.tmpname()
- fs.write_file(temp_filename_a, original_lines)
- scheduler()
- fs.write_file(temp_filename_b, current_lines)
- scheduler()
- local hunks_err, hunks = git.file_hunks(temp_filename_a, temp_filename_b)
- scheduler()
- if not hunks_err then
- if not buffer.store.contains(buf) then
- fs.remove_file(temp_filename_a)
- scheduler()
- fs.remove_file(temp_filename_b)
- scheduler()
- return
- end
- buffer.store.set(buf, 'hunks', hunks)
- renderer.hide_hunk_signs(buf)
- renderer.render_hunk_signs(buf, hunks)
- else
- logger.debug(hunks_err, debug.traceback())
+local env = require('vgit.core.env')
+local hls_setting = require('vgit.settings.hls')
+local live_blame_setting = require('vgit.settings.live_blame')
+local live_gutter_setting = require('vgit.settings.live_gutter')
+local scene_setting = require('vgit.settings.scene')
+local signs_setting = require('vgit.settings.signs')
+local loop = require('vgit.core.loop')
+local symbols_setting = require('vgit.settings.symbols')
+local keymap = require('vgit.core.keymap')
+local highlighter = require('vgit.core.highlighter')
+local sign = require('vgit.core.sign')
+local Command = require('vgit.Command')
+local Navigation = require('vgit.Navigation')
+local Marker = require('vgit.Marker')
+local GitStore = require('vgit.GitStore')
+local autocmd = require('vgit.core.autocmd')
+local LiveGutter = require('vgit.features.LiveGutter')
+local LiveBlame = require('vgit.features.LiveBlame')
+local ProjectHunksList = require('vgit.features.ProjectHunksList')
+local BufferHunks = require('vgit.features.BufferHunks')
+local Git = require('vgit.cli.Git')
+local Versioning = require('vgit.core.Versioning')
+local active_scene = require('vgit.ui.active_scene')
+
+local versioning = Versioning:new()
+local git = Git:new()
+local command = Command:new()
+local navigation = Navigation:new()
+local marker = Marker:new()
+local git_store = GitStore:new()
+local live_gutter = LiveGutter:new(git_store)
+local live_blame = LiveBlame:new(git_store)
+local buffer_hunks = BufferHunks:new(git_store, navigation, marker)
+local project_hunks_list = ProjectHunksList:new()
+
+active_scene.inject(buffer_hunks, navigation, git_store)
+
+local function prevent_default() end
+
+local on_enter = loop.async(function()
+ if active_scene.exists() then
+ return active_scene.on_enter()
end
- fs.remove_file(temp_filename_a)
- scheduler()
- fs.remove_file(temp_filename_b)
- scheduler()
-end
-
-local function int_hunk_generation(buf, original_lines, current_lines)
- scheduler()
- if controller_store.get('disabled') then
- return
- end
- if not buffer.is_valid(buf) then
- return
- end
- if not buffer.store.contains(buf) then
- return
- end
- local o_lines_str = ''
- local c_lines_str = ''
- local num_lines = math.max(#original_lines, #current_lines)
- for i = 1, num_lines do
- local o_line = original_lines[i]
- local c_line = current_lines[i]
- if o_line then
- o_lines_str = o_lines_str .. original_lines[i] .. '\n'
- end
- if c_line then
- c_lines_str = c_lines_str .. current_lines[i] .. '\n'
- end
- end
- local hunks = {}
- vim.diff(o_lines_str, c_lines_str, {
- on_hunk = void(function(start_o, count_o, start_c, count_c)
- scheduler()
- local hunk = Hunk:new({ { start_o, count_o }, { start_c, count_c } })
- hunks[#hunks + 1] = hunk
- if count_o > 0 then
- for i = start_o, start_o + count_o - 1 do
- hunk.diff[#hunk.diff + 1] = '-' .. (original_lines[i] or '')
- end
- end
- if count_c > 0 then
- for i = start_c, start_c + count_c - 1 do
- hunk.diff[#hunk.diff + 1] = '+' .. (current_lines[i] or '')
- end
- end
- end),
- algorithm = 'myers',
- })
- buffer.store.set(buf, 'hunks', hunks)
- renderer.hide_hunk_signs(buf)
- renderer.render_hunk_signs(buf, hunks)
-end
-
-local generate_tracked_hunk_signs = debounce_trailing(
- void(function(buf)
- scheduler()
- if controller_store.get('disabled') then
- return
- end
- if not buffer.is_valid(buf) then
- return
- end
- if not buffer.store.contains(buf) then
- return
- end
- local max_lines_limit = controller_store.get('predict_hunk_max_lines')
- if vim.api.nvim_buf_line_count(buf) > max_lines_limit then
- return
- end
- local tracked_filename = buffer.store.get(buf, 'tracked_filename')
- local tracked_remote_filename = buffer.store.get(
- buf,
- 'tracked_remote_filename'
- )
- local show_err, original_lines
- if controller_store.get('diff_strategy') == 'remote' then
- show_err, original_lines = git.show(
- tracked_remote_filename,
- git.get_diff_base()
- )
- else
- show_err, original_lines = git.show(tracked_remote_filename, '')
- end
- scheduler()
- if
- show_err
- and vim.startswith(
- show_err[1],
- string.format('fatal: path \'%s\' exists on disk', tracked_filename)
- )
- then
- original_lines = {}
- show_err = nil
- end
- if show_err then
- return logger.debug(show_err, debug.traceback())
- end
- local current_lines = buffer.get_lines(buf)
- buffer.store.set(buf, 'temp_lines', current_lines)
- if vim.diff then
- int_hunk_generation(buf, original_lines, current_lines)
- else
- ext_hunk_generation(buf, original_lines, current_lines)
- end
- end),
- controller_store.get('predict_hunk_throttle_ms')
-)
-
-local generate_untracked_hunk_signs = debounce_trailing(
- void(function(buf)
- scheduler()
- if controller_store.get('disabled') then
- return
- end
- if not buffer.is_valid(buf) then
- return
- end
- if not buffer.store.contains(buf) then
- return
- end
- local hunks = git.untracked_hunks(buffer.get_lines(buf))
- scheduler()
- if not buffer.store.contains(buf) then
- return
- end
- buffer.store.set(buf, 'hunks', hunks)
- renderer.hide_hunk_signs(buf)
- renderer.render_hunk_signs(buf, hunks)
- end),
- controller_store.get('predict_hunk_throttle_ms')
-)
+end)
-local function buf_attach_tracked(buf)
- scheduler()
- if controller_store.get('disabled') then
- return
- end
- if not buffer.is_valid(buf) then
- return
- end
- if not buffer.store.contains(buf) then
- return
- end
- if controller_store.get('blames_enabled') then
- attach_blames_autocmd(buf)
+local on_j = loop.async(function()
+ if active_scene.exists() then
+ return active_scene.on_j()
end
- vim.api.nvim_buf_attach(buf, false, {
- on_lines = void(function(_, cbuf, _, _, p_lnum, n_lnum, byte_count)
- scheduler()
- if
- not controller_store.get('predict_hunk_signs')
- or (p_lnum == n_lnum and byte_count == 0)
- or not controller_store.get('hunks_enabled')
- then
- return
- end
- generate_tracked_hunk_signs(cbuf)
- end),
- on_detach = void(function()
- scheduler()
- buf_attach_tracked(buf)
- end),
- })
- if not controller_store.get('hunks_enabled') then
- return
- end
- local err, hunks = calculate_hunks(buf)
- scheduler()
- if err then
- logger.debug(err, debug.traceback())
- return
- end
- if not buffer.store.contains(buf) then
- return
- end
- buffer.store.set(buf, 'hunks', hunks)
- renderer.render_hunk_signs(buf, hunks)
-end
+end)
-local function buf_attach_untracked(buf)
- if controller_store.get('disabled') then
- return
- end
- if not buffer.is_valid(buf) then
- return
- end
- if not buffer.store.contains(buf) then
- return
- end
- vim.api.nvim_buf_attach(buf, false, {
- on_lines = void(function(_, cbuf, _, _, p_lnum, n_lnum, byte_count)
- scheduler()
- if
- not controller_store.get('predict_hunk_signs')
- or (p_lnum == n_lnum and byte_count == 0)
- or not controller_store.get('hunks_enabled')
- or not buffer.store.contains(cbuf)
- then
- return
- end
- if not buffer.store.get(cbuf, 'untracked') then
- return generate_tracked_hunk_signs(cbuf)
- end
- generate_untracked_hunk_signs(cbuf)
- end),
- on_detach = void(function()
- scheduler()
- buf_attach_untracked(buf)
- end),
- })
- if not controller_store.get('hunks_enabled') then
- return
+local on_k = loop.async(function()
+ if active_scene.exists() then
+ return active_scene.on_k()
end
- local hunks = git.untracked_hunks(buffer.get_lines(buf))
- scheduler()
- if not buffer.store.contains(buf) then
- return
- end
- buffer.store.set(buf, 'hunks', hunks)
- renderer.render_hunk_signs(buf, hunks)
-end
+end)
-M._buf_attach = void(function(buf)
- scheduler()
- buf = buf or buffer.current()
- if not buffer.is_valid(buf) then
- return
+local win_enter = loop.async(function()
+ if not active_scene.exists() and live_blame_setting:get('enabled') then
+ live_blame:desync_all()
end
- if buffer.store.contains(buf) then
+ if active_scene.exists() then
+ active_scene.keep_focused()
return
end
- local filename = fs.filename(buf)
- scheduler()
- if not filename or filename == '' then
- return
- end
- if not fs.exists(filename) then
- return
- end
- local is_inside_work_tree = git.is_inside_work_tree()
- scheduler()
- if not is_inside_work_tree then
- controller_store.set('disabled', true)
- return
- end
- if controller_store.get('disabled') == true then
- controller_store.set('disabled', false)
- end
- local tracked_filename = git.tracked_filename(filename)
- scheduler()
- local tracked_remote_filename = git.tracked_remote_filename(filename)
- scheduler()
- if tracked_filename and tracked_filename ~= '' then
- store_buf(buf, filename, tracked_filename, tracked_remote_filename)
- return buf_attach_tracked(buf)
- end
- if controller_store.get('diff_strategy') == 'index' then
- local is_ignored = git.check_ignored(filename)
- scheduler()
- if not is_ignored then
- store_buf(buf, filename, tracked_filename, tracked_remote_filename)
- buf_attach_untracked(buf)
- end
- end
end)
-M._rerender_history = void(function(buf)
- if controller_store.get('disabled') then
- return
- end
- if not buffer.is_valid(buf) then
- return
- end
- if not buffer.store.contains(buf) then
- return
- end
- if buffer.store.get(buf, 'untracked') then
- return
- end
- if buffer.is_being_edited(buf) then
- return
- end
- local selected_log = vim.api.nvim_win_get_cursor(0)[1]
- local diff_preference = controller_store.get('diff_preference')
- local calculate_change = (
- diff_preference == 'horizontal' and change.horizontal
- ) or change.vertical
- renderer.rerender_history_preview(
- wrap(function()
- local tracked_filename = buffer.store.get(buf, 'tracked_filename')
- local logs = buffer.store.get(buf, 'logs')
- local log = logs[selected_log]
- local err, hunks, lines, commit_hash, computed_hunks
- if not log then
- return { 'Failed to access logs' }, nil
- end
- err, computed_hunks = git.remote_hunks(
- tracked_filename,
- log.parent_hash,
- log.commit_hash
- )
- scheduler()
- if err then
- logger.debug(err, debug.traceback())
- return err, nil
- end
- hunks = computed_hunks
- commit_hash = log.commit_hash
- if commit_hash and not lines then
- err, lines = git.show(
- buffer.store.get(buf, 'tracked_remote_filename'),
- commit_hash
- )
- scheduler()
- elseif not lines then
- err, lines = fs.read_file(tracked_filename)
- scheduler()
- end
- if err then
- logger.debug(err, debug.traceback())
- return err, nil
- end
- local data = calculate_change(lines, hunks)
- return nil,
- utils.readonly({
- filename = tracked_filename,
- filetype = buffer.store.get(buf, 'filetype'),
- logs = logs,
- diff_change = data,
- })
- end, 0),
- selected_log
- )
+local buf_enter = loop.async(function()
+ live_gutter:resync()
end)
-M._rerender_project_diff = void(function()
- if controller_store.get('disabled') then
- return
+local buf_win_enter = loop.async(function()
+ if active_scene.exists() then
+ active_scene.destroy()
end
- local selected_file = vim.api.nvim_win_get_cursor(0)[1]
- local diff_preference = controller_store.get('diff_preference')
- local calculate_change = (
- diff_preference == 'horizontal' and change.horizontal
- ) or change.vertical
- renderer.rerender_project_diff_preview(
- wrap(function()
- local changed_files_err, changed_files = git.ls_changed()
- scheduler()
- if changed_files_err then
- logger.debug(changed_files_err, debug.traceback())
- return changed_files_err, nil
- end
- local file = changed_files[selected_file]
- if not file then
- return { 'File not found' },
- utils.readonly({
- changed_files = changed_files,
- })
- end
- local filename = file.filename
- local hunk_calculator = get_hunk_calculator()
- local hunks_err, hunks = hunk_calculator(filename)
- if hunks_err then
- logger.debug(hunks_err, debug.traceback())
- return hunks_err, nil
- end
- local files_err, lines = fs.read_file(filename)
- if files_err then
- logger.debug(files_err, debug.traceback())
- return files_err,
- utils.readonly({
- changed_files = changed_files,
- })
- end
- local data = calculate_change(lines, hunks)
- return nil,
- utils.readonly({
- filename = filename,
- filetype = fs.detect_filetype(filename),
- changed_files = changed_files,
- diff_change = data,
- })
- end, 0),
- selected_file
- )
+ live_gutter:attach()
end)
-M._select_project_diff = function()
- local preview = preview_store.get()
- if not preview:is_mounted() then
- return
+local buf_win_leave = loop.async(function()
+ -- Running the loop.await_fast_event to create a bit of time in between
+ -- old buffer leaving the window and new buffer entering.
+ loop.await_fast_event()
+ -- After this call buf_win_enter should fire, where window will be destroyed.
+ if active_scene.exists() then
+ active_scene.destroy()
end
- local file = preview.data.changed_files[preview.selected + 1]
- renderer.hide_preview()
- vim.cmd(string.format('e %s', file.filename))
-end
+end)
-M._buf_update = void(function(buf)
- scheduler()
- buf = buf or buffer.current()
- if not buffer.is_valid(buf) then
- return
- end
- if not buffer.store.contains(buf) then
- return
- end
- buffer.store.set(buf, 'temp_lines', {})
- if controller_store.get('hunks_enabled') then
- if
- buffer.store.get(buf, 'untracked')
- and controller_store.get('diff_strategy') == 'index'
- then
- local hunks = git.untracked_hunks(buffer.get_lines(buf))
- scheduler()
- if not buffer.store.contains(buf) then
- return
- end
- buffer.store.set(buf, 'hunks', hunks)
- renderer.hide_hunk_signs(buf)
- renderer.render_hunk_signs(buf, hunks)
- return
- end
- local err, hunks = calculate_hunks(buf)
- scheduler()
- if err then
- return logger.debug(err, debug.traceback())
- end
- if not buffer.store.contains(buf) then
- return
- end
- buffer.store.set(buf, 'hunks', hunks)
- renderer.hide_hunk_signs(buf)
- renderer.render_hunk_signs(buf, hunks)
- end
+local buf_wipeout = loop.async(function()
+ live_gutter:detach()
end)
-M._buf_detach = function(buf)
- buf = buf or buffer.current()
- if not buffer.store.contains(buf) then
- return
+local cursor_hold = loop.async(function()
+ if live_blame_setting:get('enabled') then
+ live_blame:sync()
end
- buffer.store.remove(buf)
- detach_blames_autocmd(buf)
-end
+end)
-M._mark_current_navigated_hunk = void(function(selected, num_hunks)
- if controller_store.get('disabled') then
- return
- end
- renderer.render_current_hunk_mark(buffer.current(), selected, num_hunks)
+local cursor_moved = loop.async(function()
+ live_blame:desync()
end)
-M._blame_line = debounce_trailing(
- void(function(buf)
- scheduler()
- if controller_store.get('disabled') then
- return
- end
- if not buffer.is_valid(buf) then
- return
- end
- if not buffer.store.contains(buf) then
- return
- end
- if buffer.store.get(buf, 'untracked') then
- return
- end
- if buffer.is_being_edited(buf) then
- return
- end
- local win = vim.api.nvim_get_current_win()
- local last_lnum_blamed = buffer.store.get(buf, 'last_lnum_blamed')
- local lnum = vim.api.nvim_win_get_cursor(win)[1]
- if last_lnum_blamed == lnum then
- return
- end
- local err, blame = git.blame_line(
- buffer.store.get(buf, 'tracked_filename'),
- lnum
- )
- scheduler()
- if err then
- return logger.debug(err, debug.traceback())
- end
- if not buffer.store.contains(buf) then
- return
- end
- renderer.hide_blame_line(buf)
- scheduler()
- if
- vim.api.nvim_win_get_cursor(vim.api.nvim_get_current_win())[1] == lnum
- then
- renderer.render_blame_line(buf, blame, lnum, git.state:get('config'))
- scheduler()
- if not buffer.store.contains(buf) then
- return
- end
- buffer.store.set(buf, 'last_lnum_blamed', lnum)
- end
- scheduler()
- end),
- controller_store.get('blame_line_throttle_ms')
-)
+local insert_enter = loop.async(function()
+ live_blame:desync(true)
+end)
-M._unblame_line = void(function(buf, override)
- if not buffer.is_valid(buf) then
- return
- end
- if not buffer.store.contains(buf) then
- return
- end
- if buffer.store.get(buf, 'untracked') then
- return
- end
- if override then
- return renderer.hide_blame_line(buf)
- end
- local win = vim.api.nvim_get_current_win()
- local lnum = vim.api.nvim_win_get_cursor(win)[1]
- local last_lnum_blamed = buffer.store.get(buf, 'last_lnum_blamed')
- if lnum ~= last_lnum_blamed then
- renderer.hide_blame_line(buf)
- end
+local color_scheme = loop.async(function()
+ highlighter.create_default_theme()
end)
-M._keep_focused = function()
- if not preview_store.exists() then
- return
- end
- local preview = preview_store.get()
- if not preview:is_mounted() then
+local hunk_up = loop.async(function()
+ if active_scene.exists() then
+ active_scene.navigate('up')
return
end
- preview:keep_focused()
-end
+ buffer_hunks:move_up()
+end)
-M._run_command = function(command, ...)
- if controller_store.get('disabled') then
+local hunk_down = loop.async(function()
+ if active_scene.exists() then
+ active_scene.navigate('down')
return
end
- local vgit = require('vgit')
- if not command then
- return
- end
- local starts_with = command:sub(1, 1)
- if
- starts_with == '_'
- or not vgit[command]
- or not type(vgit[command]) == 'function'
- then
- logger.error(string.format('Invalid command', command))
- return
- end
- return vgit[command](...)
-end
+ buffer_hunks:move_down()
+end)
-M._command_autocompletes = function(arglead, line)
- local vgit = require('vgit')
- local parsed_line = #vim.split(line, '%s+')
- local matches = {}
- if parsed_line == 2 then
- for name, func in pairs(vgit) do
- if
- not vim.startswith(name, '_')
- and vim.startswith(name, arglead)
- and type(func) == 'function'
- then
- matches[#matches + 1] = name
- end
- end
- end
- return matches
-end
+local hunk_reset = loop.async(function()
+ buffer_hunks:cursor_reset()
+end)
-M.buffer_reset = void(function()
- scheduler()
- local buf = buffer.current()
- if controller_store.get('disabled') then
+local buffer_reset = loop.async(function()
+ if active_scene.exists() then
+ active_scene.git_reset()
return
end
- if not buffer.is_valid(buf) then
- return
- end
- if not buffer.store.contains(buf) then
- return
- end
- if buffer.store.get(buf, 'untracked') then
- return
- end
- local hunks = buffer.store.get(buf, 'hunks')
- if #hunks ~= 0 then
- local tracked_remote_filename = buffer.store.get(
- buf,
- 'tracked_remote_filename'
- )
- if controller_store.get('diff_strategy') == 'remote' then
- local err, lines = git.show(tracked_remote_filename, 'HEAD')
- scheduler()
- if not err then
- logger.debug(err, debug.traceback())
- return
- end
- buffer.set_lines(buf, lines)
- vim.cmd('update')
- return
- end
- local err, lines = git.show(tracked_remote_filename, '')
- scheduler()
- if err then
- return logger.debug(err, debug.traceback())
- end
- buffer.set_lines(buf, lines)
- vim.cmd('update')
- end
+ buffer_hunks:reset_all()
end)
-M.buffer_hunk_stage = void(function()
- scheduler()
- local buf = buffer.current()
- local win = vim.api.nvim_get_current_win()
- if controller_store.get('disabled') then
- return
- end
- if not buffer.store.contains(buf) then
- return
- end
- if not buffer.is_valid(buf) then
- return
- end
- if buffer.is_being_edited(buf) then
- return
- end
- if controller_store.get('diff_strategy') ~= 'index' then
+local buffer_stage = loop.async(function()
+ if active_scene.exists() then
+ active_scene.git_stage()
return
end
- -- If buffer is untracked then, the whole file is the hunk.
- if buffer.store.get(buf, 'untracked') then
- local filename = buffer.store.get(buf, 'filename')
- local err = git.stage_file(filename)
- scheduler()
- if err then
- logger.debug(err, debug.traceback())
- return
- end
- local tracked_filename = git.tracked_filename(filename)
- scheduler()
- local tracked_remote_filename = git.tracked_remote_filename(filename)
- scheduler()
- if not buffer.store.contains(buf) then
- return
- end
- buffer.store.set(buf, 'tracked_filename', tracked_filename)
- buffer.store.set(buf, 'tracked_remote_filename', tracked_remote_filename)
- buffer.store.set(buf, 'hunks', {})
- buffer.store.set(buf, 'untracked', false)
- renderer.hide_hunk_signs(buf)
- renderer.render_hunk_signs(buf, {})
- return
- end
- local lnum = vim.api.nvim_win_get_cursor(win)[1]
- local hunks = buffer.store.get(buf, 'hunks')
- local selected_hunk = get_current_hunk(hunks, lnum)
- if not selected_hunk then
- return
- end
- local tracked_filename = buffer.store.get(buf, 'tracked_filename')
- local tracked_remote_filename = buffer.store.get(
- buf,
- 'tracked_remote_filename'
- )
- local patch = Patch:new(tracked_remote_filename, selected_hunk)
- local patch_filename = fs.tmpname()
- fs.write_file(patch_filename, patch)
- scheduler()
- local err = git.stage_hunk_from_patch(patch_filename)
- scheduler()
- fs.remove_file(patch_filename)
- scheduler()
- if err then
- logger.debug(err, debug.traceback())
- return
- end
- local hunks_err, calculated_hunks = git.index_hunks(tracked_filename)
- scheduler()
- if hunks_err then
- logger.debug(err, debug.traceback())
- return
- end
- if not buffer.store.contains(buf) then
- return
- end
- buffer.store.set(buf, 'hunks', calculated_hunks)
- renderer.hide_hunk_signs(buf)
- renderer.render_hunk_signs(buf, calculated_hunks)
+ buffer_hunks:stage_all()
end)
-M.buffer_stage = void(function()
- scheduler()
- local buf = buffer.current()
- if controller_store.get('disabled') then
- return
- end
- if not buffer.store.contains(buf) then
- return
- end
- if not buffer.is_valid(buf) then
- return
- end
- if buffer.is_being_edited(buf) then
- return
- end
- if controller_store.get('diff_strategy') ~= 'index' then
- return
- end
- local filename = buffer.store.get(buf, 'filename')
- local tracked_filename = buffer.store.get(buf, 'tracked_filename')
- local err = git.stage_file(
- (tracked_filename and tracked_filename ~= '' and tracked_filename)
- or filename
- )
- scheduler()
- if err then
- logger.debug(err, debug.traceback())
- return
- end
- if not buffer.store.contains(buf) then
+local buffer_unstage = loop.async(function()
+ if active_scene.exists() then
+ active_scene.git_unstage()
return
end
- if buffer.store.get(buf, 'untracked') then
- tracked_filename = git.tracked_filename(filename)
- scheduler()
- local tracked_remote_filename = git.tracked_remote_filename(filename)
- scheduler()
- if not buffer.store.contains(buf) then
- return
- end
- buffer.store.set(buf, 'tracked_filename', tracked_filename)
- buffer.store.set(buf, 'tracked_remote_filename', tracked_remote_filename)
- buffer.store.set(buf, 'untracked', false)
- end
- buffer.store.set(buf, 'hunks', {})
- renderer.hide_hunk_signs(buf)
- renderer.render_hunk_signs(buf, {})
+ buffer_hunks:unstage_all()
end)
-M.buffer_unstage = void(function()
- scheduler()
- local buf = buffer.current()
- if controller_store.get('disabled') then
- return
- end
- if not buffer.store.contains(buf) then
- return
- end
- if not buffer.is_valid(buf) then
- return
- end
- if buffer.is_being_edited(buf) then
- return
- end
- if controller_store.get('diff_strategy') ~= 'index' then
- return
- end
- if buffer.store.get(buf, 'untracked') then
- return
- end
- local filename = buffer.store.get(buf, 'filename')
- local tracked_filename = buffer.store.get(buf, 'tracked_filename')
- local err = git.unstage_file(tracked_filename)
- scheduler()
- if err then
- logger.debug(err, debug.traceback())
- return
- end
- tracked_filename = git.tracked_filename(filename)
- scheduler()
- local tracked_remote_filename = git.tracked_remote_filename(filename)
- scheduler()
- if not buffer.store.contains(buf) then
+local stage_all = loop.async(function()
+ git:stage()
+ if active_scene.exists() then
+ active_scene.refresh()
return
end
- buffer.store.set(buf, 'tracked_filename', tracked_filename)
- buffer.store.set(buf, 'tracked_remote_filename', tracked_remote_filename)
- if tracked_filename and tracked_filename ~= '' then
- buffer.store.set(buf, 'untracked', false)
- local hunks_err, calculated_hunks = git.index_hunks(tracked_filename)
- scheduler()
- if not hunks_err then
- buffer.store.set(buf, 'hunks', calculated_hunks)
- renderer.hide_hunk_signs(buf)
- renderer.render_hunk_signs(buf, calculated_hunks)
- else
- logger.debug(err, debug.traceback())
- end
- else
- buffer.store.set(buf, 'untracked', true)
- local hunks = git.untracked_hunks(buffer.get_lines(buf))
- scheduler()
- if not buffer.store.contains(buf) then
- return
- end
- buffer.store.set(buf, 'hunks', hunks)
- renderer.hide_hunk_signs(buf)
- renderer.render_hunk_signs(buf, hunks)
- end
end)
-M.buffer_hunk_reset = void(function()
- local buf = buffer.current()
- local win = vim.api.nvim_get_current_win()
- if controller_store.get('disabled') then
- return
- end
- if not buffer.store.contains(buf) then
- return
- end
- if not buffer.is_valid(buf) then
- return
- end
- if not controller_store.get('hunks_enabled') then
- return
- end
- if buffer.store.get(buf, 'untracked') then
+local unstage_all = loop.async(function()
+ git:unstage()
+ if active_scene.exists() then
+ active_scene.refresh()
return
end
- local hunks = buffer.store.get(buf, 'hunks')
- local lnum = vim.api.nvim_win_get_cursor(win)[1]
- if lnum == 1 then
- local current_lines = buffer.get_lines(buf)
- if #hunks > 0 and #current_lines == 1 and current_lines[1] == '' then
- local all_removes = true
- for i = 1, #hunks do
- local hunk = hunks[i]
- if hunk.type ~= 'remove' then
- all_removes = false
- break
- end
- end
- if all_removes then
- return M.buffer_reset(buf)
- end
- end
- end
- local selected_hunk = nil
- local selected_hunk_index = nil
- for i = 1, #hunks do
- local hunk = hunks[i]
- if
- (lnum >= hunk.start and lnum <= hunk.finish)
- or (
- hunk.start == 0
- and hunk.finish == 0
- and lnum - 1 == hunk.start
- and lnum - 1 == hunk.finish
- )
- then
- selected_hunk = hunk
- selected_hunk_index = i
- break
- end
- end
- if selected_hunk then
- local replaced_lines = {}
- for i = 1, #selected_hunk.diff do
- local line = selected_hunk.diff[i]
- local is_line_removed = vim.startswith(line, '-')
- if is_line_removed then
- replaced_lines[#replaced_lines + 1] = string.sub(line, 2, -1)
- end
- end
- local start = selected_hunk.start
- local finish = selected_hunk.finish
- if start and finish then
- if selected_hunk.type == 'remove' then
- vim.api.nvim_buf_set_lines(buf, start, finish, false, replaced_lines)
- else
- vim.api.nvim_buf_set_lines(
- buf,
- start - 1,
- finish,
- false,
- replaced_lines
- )
- end
- local new_lnum = start
- if new_lnum < 1 then
- new_lnum = 1
- end
- navigation.set_cursor(win, { new_lnum, 0 })
- vim.cmd('update')
- table.remove(hunks, selected_hunk_index)
- renderer.hide_hunk_signs(buf)
- renderer.render_hunk_signs(buf, hunks)
- end
- end
end)
-M.toggle_buffer_hunks = void(function()
- scheduler()
- if not controller_store.get('disabled') then
- if controller_store.get('hunks_enabled') then
- controller_store.set('hunks_enabled', false)
- buffer.store.for_each(function(buf, bcache)
- if buffer.is_valid(buf) then
- bcache:set('hunks', {})
- renderer.hide_hunk_signs(buf)
- end
- end)
- return controller_store.get('hunks_enabled')
- else
- controller_store.set('hunks_enabled', true)
- end
- buffer.store.for_each(function(buf, bcache)
- if buffer.is_valid(buf) then
- local hunks_err, hunks = calculate_hunks(buf)
- scheduler()
- if not hunks_err then
- controller_store.set('hunks_enabled', true)
- bcache:set('hunks', hunks)
- renderer.hide_hunk_signs(buf)
- renderer.render_hunk_signs(buf, hunks)
- else
- logger.debug(hunks_err, debug.traceback())
- end
- end
- end)
- end
- return controller_store.get('hunks_enabled')
+local buffer_hunk_stage = loop.async(function()
+ buffer_hunks:cursor_stage()
end)
-M.toggle_buffer_blames = void(function()
- scheduler()
- if controller_store.get('disabled') then
- return
- end
- if controller_store.get('blames_enabled') then
- controller_store.set('blames_enabled', false)
- buffer.store.for_each(function(buf, bcache)
- if buffer.is_valid(buf) then
- detach_blames_autocmd(buf)
- bcache:set('blames', {})
- M._unblame_line(buf, true)
- end
- end)
- return controller_store.get('blames_enabled')
- end
- controller_store.set('blames_enabled', true)
- buffer.store.for_each(function(buf)
- if buffer.is_valid(buf) then
- attach_blames_autocmd(buf)
- end
- end)
- return controller_store.get('blames_enabled')
+local buffer_hunk_preview = loop.async(function()
+ active_scene.hunk_scene()
end)
-M.toggle_diff_preference = function()
- local allowed_preference = {
- horizontal = 'vertical',
- vertical = 'horizontal',
- }
- controller_store.set(
- 'diff_preference',
- allowed_preference[controller_store.get('diff_preference')]
- )
-end
-
-M.hunk_down = void(function()
- scheduler()
- local buf = buffer.current()
- local win = vim.api.nvim_get_current_win()
- if controller_store.get('disabled') then
- return
- end
- if preview_store.exists() then
- local preview = preview_store.get()
- return preview:navigate_code('down')
- end
- if buffer.is_valid(buf) then
- if not buffer.store.contains(buf) then
- return
- end
- local hunks = buffer.store.get(buf, 'hunks')
- if #hunks ~= 0 then
- local hunk_index = navigation.hunk_down(
- win,
- vim.api.nvim_win_get_cursor(0),
- hunks
- )
- M._mark_current_navigated_hunk(hunk_index, #hunks)
- scheduler()
- end
- end
+local buffer_hunk_staged_preview = loop.async(function()
+ active_scene.staged_hunk_scene()
end)
-M.hunk_up = void(function()
- scheduler()
- local buf = buffer.current()
- local win = vim.api.nvim_get_current_win()
- if controller_store.get('disabled') then
- return
- end
- if preview_store.exists() then
- local preview = preview_store.get()
- return preview:navigate_code('up')
- end
- if buffer.is_valid(buf) then
- if not buffer.store.contains(buf) then
- return
- end
- local hunks = buffer.store.get(buf, 'hunks')
- if #hunks ~= 0 then
- local hunk_index = navigation.hunk_up(
- win,
- vim.api.nvim_win_get_cursor(0),
- hunks
- )
- M._mark_current_navigated_hunk(hunk_index, #hunks)
- scheduler()
- end
- end
+local buffer_diff_preview = loop.async(function()
+ active_scene.diff_scene()
end)
-M.apply_highlights = function()
- highlight.setup(controller_store.get('config'), true)
-end
-
-M.show_debug_logs = function()
- if logger.state:get('debug') then
- local debug_logs = logger.state:get('debug_logs')
- for i = 1, #debug_logs do
- local log = debug_logs[i]
- logger.error(log)
- end
- end
-end
-
-M.get_diff_base = function()
- return git.get_diff_base()
-end
+local buffer_diff_staged_preview = loop.async(function()
+ active_scene.staged_diff_scene()
+end)
-M.get_diff_strategy = function()
- return controller_store.get('diff_strategy')
-end
+local buffer_history_preview = loop.async(function()
+ active_scene.history_scene()
+end)
-M.get_diff_preference = function()
- return controller_store.get('diff_preference')
-end
+local buffer_blame_preview = loop.async(function()
+ active_scene.line_blame_scene()
+end)
-M.set_diff_base = void(function(diff_base)
- scheduler()
- if not diff_base or type(diff_base) ~= 'string' then
- logger.error(
- string.format(
- 'Failed to set diff base, the commit "%s" is invalid',
- diff_base
- )
- )
- return
- end
- if git.controller_store.get('diff_base') == diff_base then
- return
- end
- local is_commit_valid = git.is_commit_valid(diff_base)
- scheduler()
- if not is_commit_valid then
- logger.error(
- string.format(
- 'Failed to set diff base, the commit "%s" is invalid',
- diff_base
- )
- )
- return
- end
- git.set_diff_base(diff_base)
- if controller_store.get('diff_strategy') ~= 'remote' then
- return
- end
- local data = buffer.store.get_data()
- for buf, bcache in pairs(data) do
- local hunks_err, hunks = git.remote_hunks(bcache:get('tracked_filename'))
- scheduler()
- if hunks_err then
- logger.debug(hunks_err, debug.traceback())
- else
- bcache:set('hunks', hunks)
- renderer.hide_hunk_signs(buf)
- renderer.render_hunk_signs(buf, hunks)
- end
- end
+local project_diff_preview = loop.async(function()
+ active_scene.project_diff_scene()
end)
-M.set_diff_strategy = void(function(strategy)
- scheduler()
- if strategy ~= 'remote' and strategy ~= 'index' then
- return logger.error(
- string.format('Failed to set diff strategy, "%s" is invalid', strategy)
- )
- end
- local current_strategy = controller_store.get('diff_strategy')
- if current_strategy == strategy then
- return
- end
- controller_store.set('diff_strategy', strategy)
- buffer.store.for_each(function(buf, bcache)
- if buffer.is_valid(buf) then
- local hunks_err, hunks = calculate_hunks(buf)
- scheduler()
- if hunks_err then
- logger.debug(hunks_err, debug.traceback())
- else
- controller_store.set('hunks_enabled', true)
- bcache:set('hunks', hunks)
- renderer.hide_hunk_signs(buf)
- renderer.render_hunk_signs(buf, hunks)
- end
- end
- end)
+local project_hunks_preview = loop.async(function()
+ active_scene.project_hunks_scene()
end)
-M.buffer_gutter_blame_preview = void(function()
- local buf = buffer.current()
- if controller_store.get('disabled') then
- return
- end
- if not buffer.store.contains(buf) then
- return
- end
- if not buffer.is_valid(buf) then
- return
- end
- if buffer.store.get(buf, 'untracked') then
- return
- end
- renderer.render_gutter_blame_preview(
- wrap(function()
- local filename = buffer.store.get(buf, 'tracked_filename')
- local read_file_err, lines = fs.read_file(filename)
- scheduler()
- if read_file_err then
- logger.debug(read_file_err, debug.traceback())
- return read_file_err, nil
- end
- local blames_err, blames = git.blames(filename)
- scheduler()
- if blames_err then
- logger.debug(blames_err, debug.traceback())
- return blames_err, nil
- end
- local hunk_calculator = get_hunk_calculator()
- local hunks_err, hunks = hunk_calculator(filename)
- scheduler()
- if hunks_err then
- logger.debug(hunks_err, debug.traceback())
- return hunks_err, nil
- end
- return nil,
- {
- blames = blames,
- lines = lines,
- hunks = hunks,
- }
- end, 0),
- buffer.store.get(buf, 'filetype')
- )
+local buffer_gutter_blame_preview = loop.async(function()
+ active_scene.gutter_blame_scene()
end)
-M.buffer_blame_preview = void(function()
- local buf = buffer.current()
- if not buffer.store.contains(buf) then
- return
- end
- if not buffer.is_valid(buf) then
- return
- end
- if buffer.store.get(buf, 'untracked') then
- return
- end
- local has_commits = git.has_commits()
- scheduler()
- if not has_commits then
- return
- end
- local win = vim.api.nvim_get_current_win()
- local lnum = vim.api.nvim_win_get_cursor(win)[1]
- renderer.render_blame_preview(wrap(function()
- local err, blame = git.blame_line(
- buffer.store.get(buf, 'tracked_filename'),
- lnum
- )
- scheduler()
- return err, blame
- end, 0))
+local toggle_diff_preference = loop.async(function()
+ active_scene.toggle_diff_preference()
end)
-M.buffer_history_preview = void(function()
- local buf = buffer.current()
- if not buffer.store.contains(buf) then
- return
- end
- if not buffer.is_valid(buf) then
- return
- end
- if buffer.store.get(buf, 'untracked') then
- return
- end
- local diff_preference = controller_store.get('diff_preference')
- local calculate_change = (
- diff_preference == 'horizontal' and change.horizontal
- ) or change.vertical
- renderer.render_history_preview(
- wrap(function()
- local tracked_filename = buffer.store.get(buf, 'tracked_filename')
- local logs_err, logs = git.logs(tracked_filename)
- scheduler()
- if logs_err then
- logger.debug(logs_err, debug.traceback())
- return logs_err, nil
- end
- buffer.store.set(buf, 'logs', logs)
- local log = logs[1]
- local err, hunks, lines, commit_hash, computed_hunks
- if not log then
- return { 'Failed to access logs' }, nil
- end
- err, computed_hunks = git.remote_hunks(
- tracked_filename,
- log.parent_hash,
- log.commit_hash
- )
- scheduler()
- if err then
- logger.debug(err, debug.traceback())
- return err, nil
- end
- hunks = computed_hunks
- commit_hash = log.commit_hash
- if commit_hash and not lines then
- err, lines = git.show(
- buffer.store.get(buf, 'tracked_remote_filename'),
- commit_hash
- )
- scheduler()
- elseif not lines then
- err, lines = fs.read_file(tracked_filename)
- scheduler()
- end
- if err then
- logger.debug(err, debug.traceback())
- return err, nil
- end
- local data = calculate_change(lines, hunks)
- return nil,
- utils.readonly({
- filename = tracked_filename,
- filetype = buffer.store.get(buf, 'filetype'),
- logs = logs,
- diff_change = data,
- })
- end, 0),
- buffer.store.get(buf, 'filetype'),
- diff_preference
- )
+local project_hunks_qf = loop.async(function()
+ project_hunks_list:show_as_quickfix(project_hunks_list:fetch())
end)
-M.buffer_hunk_preview = void(function()
- local buf = buffer.current()
- local win = vim.api.nvim_get_current_win()
- if controller_store.get('disabled') then
- return
- end
- if not buffer.store.contains(buf) then
- return
- end
- if not controller_store.get('hunks_enabled') then
- return
- end
- if not buffer.is_valid(buf) then
- return
- end
- if buffer.store.get(buf, 'untracked') then
- return
- end
- local hunks = buffer.store.get(buf, 'hunks')
- if #hunks == 0 then
- logger.info('No changes found')
- return
+local toggle_buffer_blames = loop.async(function()
+ local blames_enabled = live_blame_setting:get('enabled')
+ if blames_enabled then
+ live_blame:desync_all()
+ else
+ live_blame:sync()
end
- local lnum = vim.api.nvim_win_get_cursor(win)[1]
- renderer.render_hunk_preview(
- wrap(function()
- local tracked_filename = buffer.store.get(buf, 'tracked_filename')
- local read_file_err, lines = fs.read_file(tracked_filename)
- scheduler()
- if read_file_err then
- logger.debug(read_file_err, debug.traceback())
- return read_file_err, nil
- end
- local data = change.horizontal(lines, hunks)
- return nil,
- {
- filename = tracked_filename,
- filetype = buffer.store.get(buf, 'filetype'),
- diff_change = data,
- selected_hunk = get_current_hunk(hunks, lnum) or Hunk:new(),
- }
- end, 0),
- buffer.store.get(buf, 'filetype')
- )
+ live_blame_setting:set('enabled', not blames_enabled)
end)
-M.buffer_diff_preview = void(function()
- local buf = buffer.current()
- if not buffer.store.contains(buf) then
- return
- end
- if not buffer.is_valid(buf) then
- return
- end
- if buffer.store.get(buf, 'untracked') then
- return
- end
- local diff_preference = controller_store.get('diff_preference')
- local calculate_change = (
- diff_preference == 'horizontal' and change.horizontal
- ) or change.vertical
- renderer.render_diff_preview(
- wrap(function()
- local tracked_filename = buffer.store.get(buf, 'tracked_filename')
- local hunks_err, hunks = calculate_hunks(buf)
- scheduler()
- if hunks_err then
- logger.debug(hunks_err, debug.traceback())
- return hunks_err, nil
- end
- local temp_lines = buffer.store.get(buf, 'temp_lines')
- local read_file_err, lines
- if #temp_lines ~= 0 then
- lines = temp_lines
- else
- read_file_err, lines = fs.read_file(tracked_filename)
- scheduler()
- if read_file_err then
- logger.debug(read_file_err, debug.traceback())
- return read_file_err, nil
- end
- end
- local data = calculate_change(lines, hunks)
- scheduler()
- return nil,
- {
- filename = tracked_filename,
- filetype = buffer.store.get(buf, 'filetype'),
- diff_change = data,
- }
- end, 0),
- buffer.store.get(buf, 'filetype'),
- diff_preference
- )
+local toggle_buffer_hunks = loop.async(function()
+ local hunks_enabled = live_gutter_setting:get('enabled')
+ live_gutter_setting:set('enabled', not hunks_enabled)
+ live_gutter:resync()
end)
-M.buffer_staged_diff_preview = void(function()
- local buf = buffer.current()
- if controller_store.get('disabled') then
- return
- end
- if not buffer.is_valid(buf) then
- return
- end
- if not buffer.store.contains(buf) then
- return
- end
- if buffer.store.get(buf, 'untracked') then
- return
- end
- if controller_store.get('diff_strategy') ~= 'index' then
- return
- end
- local diff_preference = controller_store.get('diff_preference')
- local calculate_change = (
- diff_preference == 'horizontal' and change.horizontal
- ) or change.vertical
- renderer.render_diff_preview(
- wrap(function()
- local tracked_filename = buffer.store.get(buf, 'tracked_filename')
- local hunks_err, hunks = git.staged_hunks(tracked_filename)
- scheduler()
- if hunks_err then
- logger.debug(hunks_err, debug.traceback())
- return hunks_err, nil
- end
- scheduler()
- local show_err, lines = git.show(
- buffer.store.get(buf, 'tracked_remote_filename')
- )
- scheduler()
- if show_err then
- logger.debug(show_err, debug.traceback())
- return show_err, nil
- end
- local data = calculate_change(lines, hunks)
- scheduler()
- return nil,
- {
- filename = tracked_filename,
- filetype = buffer.store.get(buf, 'filetype'),
- diff_change = data,
- }
- end, 0),
- buffer.store.get(buf, 'filetype'),
- diff_preference
- )
+local enable_tracing = loop.async(function()
+ env.set('DEBUG', true)
end)
-M.project_diff_preview = void(function()
- if controller_store.get('disabled') then
- return
- end
- local diff_preference = controller_store.get('diff_preference')
- local calculate_change = (
- diff_preference == 'horizontal' and change.horizontal
- ) or change.vertical
- local changed_files_err, changed_files = git.ls_changed()
- scheduler()
- if changed_files_err then
- return logger.debug(changed_files_err, debug.traceback())
- end
- if #changed_files == 0 then
- logger.info('No changes found')
- return
- end
- renderer.render_project_diff_preview(
- wrap(function()
- local selected_file = 1
- local file = changed_files[selected_file]
- if not file then
- return { 'File not found' },
- utils.readonly({
- changed_files = changed_files,
- })
- end
- local filename = file.filename
- local hunk_calculator = get_hunk_calculator()
- local hunks_err, hunks = hunk_calculator(filename)
- if hunks_err then
- logger.debug(hunks_err, debug.traceback())
- return hunks_err, nil
- end
- local files_err, lines = fs.read_file(filename)
- if files_err then
- logger.debug(files_err, debug.traceback())
- return files_err,
- utils.readonly({
- changed_files = changed_files,
- })
- end
- local data = calculate_change(lines, hunks)
- return nil,
- utils.readonly({
- filename = filename,
- filetype = fs.detect_filetype(filename),
- changed_files = changed_files,
- diff_change = data,
- })
- end, 0),
- diff_preference
- )
+local disable_tracing = loop.async(function()
+ env.set('DEBUG', false)
end)
-M.project_hunks_qf = void(function()
- if controller_store.get('disabled') then
- return
- end
- local changed_files_err, changed_files = git.ls_changed()
- scheduler()
- if changed_files_err then
- return logger.debug(changed_files_err, debug.traceback())
- end
- if #changed_files == 0 then
- logger.info('No changes found')
- return
- end
- local qf_entries = {}
- for i = 1, #changed_files do
- local file = changed_files[i]
- local filename = file.filename
- local status = file.status
- local hunks_err, hunks
- if status == '??' then
- local show_err, lines = fs.read_file(filename)
- if not show_err then
- hunks = git.untracked_hunks(lines)
- else
- logger.debug(show_err, debug.traceback())
- end
- else
- local hunk_calculator = get_hunk_calculator()
- hunks_err, hunks = hunk_calculator(filename)
- end
- scheduler()
- if not hunks_err then
- for j = 1, #hunks do
- local hunk = hunks[j]
- qf_entries[#qf_entries + 1] = {
- text = string.format('[%s..%s]', hunk.start, hunk.finish),
- filename = filename,
- lnum = hunk.start,
- col = 0,
- }
- end
- else
- logger.debug(hunks_err, debug.traceback())
- end
- end
- if #qf_entries == 0 then
- return logger.info('No changes found')
+local initialize_necessary_features = loop.async(function()
+ live_gutter:attach()
+ if live_blame_setting:get('enabled') then
+ live_blame:sync()
end
- vim.fn.setqflist(qf_entries, 'r')
- vim.cmd('copen')
end)
-M.actions = function()
- if not pcall(require, 'telescope') then
- logger.info(
- 'Please install https://github.com/nvim-telescope/telescope.nvim to use the command palette'
- )
- return
- end
- local actions = {
- 'project_diff_preview | Opens preview of all the changes in your current project',
- 'project_hunks_qf | Opens quickfix list with all the changes as hunks in your current project',
- 'buffer_diff_preview | Opens preview of the changes in the current buffer',
- 'buffer_staged_diff_preview | Opens preview of all the staged changes for your current buffer',
- 'buffer_hunk_preview | Opens preview of the changes in the current buffer hunk',
- 'buffer_history_preview | Opens preview of all the changes throughout time for the current buffer',
- 'buffer_blame_preview | Opens preview of showing the blame details of the current line for the current buffer',
- 'buffer_gutter_blame_preview | Opens preview of showing all blame details for the current buffer',
- 'buffer_reset | Reset all the changes on the current buffer',
- 'buffer_hunk_stage | Stage the current hunk the cursor is currently on in your current buffer',
- 'buffer_stage | Stage the current buffer',
- 'buffer_unstage | Unstage the current buffer',
- 'buffer_hunk_reset | Reset the current hunk the cursor is onin your current buffer',
- 'toggle_buffer_hunks | Enables buffer signs on/Disables buffer signs off',
- 'toggle_buffer_blames | Enables current line blames/Disables current buffer line blames',
- 'toggle_diff_preference | Toggles between "Horizontal" and "Vertical" diff preference',
- 'hunk_up | Navigates up on to a change on any buffer or preview',
- 'hunk_down | Navigates down on to a change on any buffer or preview',
- 'apply_highlights | Applies all the current highlights, useful when changing colorschemes',
- }
- local pickers = require('telescope.pickers')
- local finders = require('telescope.finders')
- local conf = require('telescope.config').values
- local telescope_actions = require('telescope.actions')
- local action_state = require('telescope.actions.state')
- pickers.new(
- { layout_strategy = 'bottom_pane', layout_config = { height = #actions } },
- {
- prompt_title = 'VGit',
- finder = finders.new_table(actions),
- sorter = conf.generic_sorter(),
- attach_mappings = function(buf, map)
- map(
- 'i',
- '',
- void(function()
- local selected = action_state.get_selected_entry()
- local value = selected.value
- local command = vim.trim(vim.split(value, '|')[1])
- telescope_actions.close(buf)
- scheduler()
- require('vgit')[command]()
- end)
- )
- return true
- end,
- }
- ):find()
+local function command_list(...)
+ return command:list(...)
end
-M.help = function()
- vim.cmd('help vgit.nvim')
+local function execute_command(...)
+ command:execute(...)
end
-M.renderer = renderer
-M.autocmd = autocmd
-M.highlight = highlight
-M.themes = themes
-M.layouts = layouts
-M.dimensions = dimensions
-M.utils = utils
+local function version()
+ return versioning:current()
+end
-M.setup = function(config)
- controller_store.setup(config)
- render_store.setup(config)
- autocmd.setup()
- highlight.setup(config)
- sign.setup(config)
- logger.setup(config)
- git.setup(config)
- key_mapper.setup(config)
- autocmd.on('BufWinEnter', ':lua _G.package.loaded.vgit._buf_attach()')
- autocmd.on('BufWinLeave', ':lua _G.package.loaded.vgit._buf_detach()')
- autocmd.on('BufWritePost', ':lua _G.package.loaded.vgit._buf_update()')
- autocmd.on('WinEnter', ':lua _G.package.loaded.vgit._keep_focused()')
+local function setup_commands()
vim.cmd(
string.format(
'command! -nargs=* -range %s %s',
- '-complete=customlist,v:lua.package.loaded.vgit._command_autocompletes',
- 'VGit lua _G.package.loaded.vgit._run_command()'
+ '-complete=customlist,v:lua.package.loaded.vgit.command_list',
+ 'VGit lua _G.package.loaded.vgit.execute_command()'
)
)
- M._buf_attach()
- vim.cmd('echohl WarningMsg')
- vim.cmd(
- 'echo "[VGit] Breaking changes will be introduced in an upcoming update. Please checkout issue #159 for more details."'
- )
- vim.cmd('echohl NONE')
end
-return M
+local function register_modules()
+ highlighter.register_module(function()
+ sign.register_module()
+ end)
+ autocmd.register_module()
+end
+
+local function register_autocmds()
+ autocmd.on('BufEnter', 'buf_enter()')
+ autocmd.on('WinEnter', 'win_enter()')
+ autocmd.on('BufWinEnter', 'buf_win_enter()')
+ autocmd.on('BufWinLeave', 'buf_win_leave()')
+ autocmd.on('BufWipeout', 'buf_wipeout()')
+ autocmd.on('CursorHold', 'cursor_hold()')
+ autocmd.on('CursorMoved', 'cursor_moved()')
+ autocmd.on('InsertEnter', 'insert_enter()')
+ autocmd.on('ColorScheme', 'color_scheme()')
+end
+
+local function configure_settings(config)
+ local settings = config and config.settings or {}
+ hls_setting:assign(settings.hls)
+ live_blame_setting:assign(settings.live_blame)
+ live_gutter_setting:assign(settings.live_gutter)
+ scene_setting:assign(settings.scene)
+ signs_setting:assign(settings.signs)
+ symbols_setting:assign(settings.symbols)
+end
+
+local function define_keymaps(config)
+ local keymaps = config and config.keymaps or {}
+ keymap.define(keymaps)
+end
+
+local setup = function(config)
+ if not versioning:is_neovim_compatible() then
+ return
+ end
+ define_keymaps(config)
+ configure_settings(config)
+ setup_commands()
+ register_modules()
+ register_autocmds()
+ initialize_necessary_features()
+end
+
+return {
+ setup = setup,
+ version = version,
+ prevent_default = prevent_default,
+ buf_enter = buf_enter,
+ win_enter = win_enter,
+ buf_win_enter = buf_win_enter,
+ buf_win_leave = buf_win_leave,
+ buf_wipeout = buf_wipeout,
+ execute_command = execute_command,
+ command_list = command_list,
+ color_scheme = color_scheme,
+ hunk_up = hunk_up,
+ hunk_down = hunk_down,
+ stage_all = stage_all,
+ unstage_all = unstage_all,
+ buffer_hunk_reset = hunk_reset,
+ buffer_reset = buffer_reset,
+ buffer_stage = buffer_stage,
+ buffer_unstage = buffer_unstage,
+ buffer_hunk_stage = buffer_hunk_stage,
+ buffer_hunk_staged_preview = buffer_hunk_staged_preview,
+ buffer_hunk_preview = buffer_hunk_preview,
+ buffer_diff_preview = buffer_diff_preview,
+ buffer_diff_staged_preview = buffer_diff_staged_preview,
+ buffer_history_preview = buffer_history_preview,
+ buffer_blame_preview = buffer_blame_preview,
+ buffer_gutter_blame_preview = buffer_gutter_blame_preview,
+ project_hunks_qf = project_hunks_qf,
+ project_diff_preview = project_diff_preview,
+ project_hunks_preview = project_hunks_preview,
+ enable_tracing = enable_tracing,
+ disable_tracing = disable_tracing,
+ toggle_buffer_blames = toggle_buffer_blames,
+ toggle_buffer_hunks = toggle_buffer_hunks,
+ toggle_diff_preference = toggle_diff_preference,
+ cursor_hold = cursor_hold,
+ cursor_moved = cursor_moved,
+ insert_enter = insert_enter,
+ on_enter = on_enter,
+ on_j = on_j,
+ on_k = on_k,
+ settings = {
+ scene = scene_setting,
+ hls = hls_setting,
+ symbols = symbols_setting,
+ signs = signs_setting,
+ live_blame = live_blame_setting,
+ },
+}
diff --git a/lua/vgit/Command.lua b/lua/vgit/Command.lua
new file mode 100644
index 00000000..f7ebe39d
--- /dev/null
+++ b/lua/vgit/Command.lua
@@ -0,0 +1,45 @@
+local console = require('vgit.core.console')
+local Object = require('vgit.core.Object')
+
+local Command = Object:extend()
+
+function Command:new()
+ return setmetatable({}, Command)
+end
+
+function Command:execute(command, ...)
+ local vgit = require('vgit')
+ if not command then
+ return
+ end
+ local starts_with = command:sub(1, 1)
+ if
+ starts_with == '_'
+ or not vgit[command]
+ or not type(vgit[command]) == 'function'
+ then
+ console.error(string.format('Invalid VGit command %s', command))
+ return
+ end
+ return vgit[command](...)
+end
+
+function Command:list(arglead, line)
+ local vgit = require('vgit')
+ local parsed_line = #vim.split(line, '%s+')
+ local matches = {}
+ if parsed_line == 2 then
+ for name, func in pairs(vgit) do
+ if
+ not vim.startswith(name, '_')
+ and vim.startswith(name, arglead)
+ and type(func) == 'function'
+ then
+ matches[#matches + 1] = name
+ end
+ end
+ end
+ return matches
+end
+
+return Command
diff --git a/lua/vgit/Component.lua b/lua/vgit/Component.lua
deleted file mode 100644
index 9df20de6..00000000
--- a/lua/vgit/Component.lua
+++ /dev/null
@@ -1,592 +0,0 @@
-local Object = require('plenary.class')
-local dimensions = require('vgit.dimensions')
-local render_store = require('vgit.stores.render_store')
-local navigation = require('vgit.navigation')
-local virtual_text = require('vgit.virtual_text')
-local sign = require('vgit.sign')
-local autocmd = require('vgit.autocmd')
-local assert = require('vgit.assertion').assert
-local buffer = require('vgit.buffer')
-local VirtualLineNrDecorator = require('vgit.decorators.VirtualLineNrDecorator')
-local void = require('plenary.async.async').void
-local scheduler = require('plenary.async.util').scheduler
-local Interface = require('vgit.Interface')
-
-local Component = Object:extend()
-
-Component.state = Interface:new({
- loading = {
- frame_rate = 60,
- frames = {
- '∙∙∙',
- '●∙∙',
- '∙●∙',
- '∙∙●',
- '∙∙∙',
- },
- },
- error = '✖✖✖',
-})
-
-function Component:setup(config)
- Component.state:assign(config)
-end
-
-function Component:new(options)
- assert(
- options == nil or type(options) == 'table',
- 'type error :: expected table or nil'
- )
- options = options or {}
- local height = self:get_min_height()
- local width = self:get_min_width()
- return setmetatable({
- anim_id = nil,
- timer_id = nil,
- state = {
- buf = nil,
- win_id = nil,
- ns_id = nil,
- virtual_line_nr = nil,
- loading = false,
- error = false,
- mounted = false,
- cache = {
- lines = {},
- cursor = nil,
- },
- paint_count = 0,
- },
- config = Interface
- :new({
- filetype = '',
- border = {
- enabled = false,
- hl = 'FloatBorder',
- chars = { '', '', '', '', '', '', '', '' },
- },
- buf_options = {
- ['modifiable'] = false,
- ['buflisted'] = false,
- ['bufhidden'] = 'wipe',
- },
- win_options = {
- ['wrap'] = false,
- ['number'] = false,
- ['winhl'] = 'Normal:',
- ['cursorline'] = false,
- ['cursorbind'] = false,
- ['scrollbind'] = false,
- ['signcolumn'] = 'auto',
- },
- window_props = {
- style = 'minimal',
- relative = 'editor',
- height = height,
- width = width,
- row = 1,
- col = 0,
- focusable = true,
- zindex = 50,
- },
- virtual_line_nr = {
- enabled = false,
- width = render_store.get('preview').virtual_line_nr_width,
- },
- static = false,
- })
- :assign(options),
- }, Component)
-end
-
-function Component:is_virtual_line_nr_enabled()
- return self.config:get('virtual_line_nr').enabled
-end
-
-function Component:has_virtual_line_nr()
- return self:get_virtual_line_nr() and self:is_virtual_line_nr_enabled()
-end
-
-function Component:is_border_enabled()
- return self.config:get('border').enabled
-end
-
-function Component:is_static()
- return self.config:get('static')
-end
-
-function Component:is_hover()
- return self.config:get('window_props').relative == 'cursor'
-end
-
-function Component:is_focused()
- return vim.api.nvim_get_current_win() == self:get_win_id()
-end
-
-function Component:has_lines()
- return self:get_paint_count() > 0
-end
-
-function Component:get_paint_count()
- return self.state.paint_count
-end
-
-function Component:get_win_ids()
- return { self:get_win_id(), self:get_virtual_line_nr_win_id() }
-end
-
-function Component:get_bufs()
- return { self:get_buf(), self:get_virtual_line_nr_buf() }
-end
-
-function Component:get_win_id()
- return self.state.win_id
-end
-
-function Component:get_buf()
- return self.state.buf
-end
-
-function Component:get_ns_id()
- return self.state.ns_id
-end
-
-function Component:get_virtual_line_nr_buf()
- return self:get_virtual_line_nr() and self:get_virtual_line_nr():get_buf()
- or nil
-end
-
-function Component:get_virtual_line_nr_win_id()
- return self:get_virtual_line_nr()
- and self:get_virtual_line_nr():get_win_id()
- or nil
-end
-
-function Component:get_buf_option(key)
- return buffer.get_option(self:get_win_id(), key)
-end
-
-function Component:get_win_option(key)
- return vim.api.nvim_win_get_option(self:get_win_id(), key)
-end
-
-function Component:get_lines()
- return buffer.get_lines(self:get_buf())
-end
-
-function Component:get_height()
- return vim.api.nvim_win_get_height(self:get_win_id())
-end
-
-function Component:get_width()
- return vim.api.nvim_win_get_width(self:get_win_id())
-end
-
-function Component:get_min_height()
- return 20
-end
-
-function Component:get_min_width()
- return 70
-end
-
-function Component:get_cached_lines()
- return self.state.cache.lines
-end
-
-function Component:get_cached_cursor()
- return self.state.cache.cursor
-end
-
-function Component:get_loading()
- return self.state.loading
-end
-
-function Component:get_error()
- return self.state.error
-end
-
-function Component:get_virtual_line_nr()
- return self.state.virtual_line_nr
-end
-
-function Component:is_mounted()
- return self.state.mounted
-end
-
-function Component:set_virtual_line_nr(virtual_line_nr)
- assert(type(virtual_line_nr) == 'table', 'type error :: expected table')
- self.state.virtual_line_nr = virtual_line_nr
- return self
-end
-
-function Component:set_ns_id(value)
- assert(type(value) == 'number', 'type error :: expected number')
- self.state.ns_id = value
- return self
-end
-
-function Component:set_buf(value)
- assert(type(value) == 'number', 'type error :: expected number')
- self.state.buf = value
- return self
-end
-
-function Component:set_win_id(value)
- assert(type(value) == 'number', 'type error :: expected number')
- self.state.win_id = value
- return self
-end
-
-function Component:set_cached_lines(value)
- assert(vim.tbl_islist(value), 'type error :: expected list table')
- self.state.cache.lines = value
- return self
-end
-
-function Component:set_cached_cursor(value)
- assert(vim.tbl_islist(value), 'type error :: expected list table')
- self.state.cache.cursor = value
- return self
-end
-
-function Component:set_height(value)
- assert(type(value) == 'number', 'type error :: expected number')
- vim.api.nvim_win_set_height(self:get_win_id(), value)
- return self
-end
-
-function Component:set_width(value)
- assert(type(value) == 'number', 'type error :: expected number')
- vim.api.nvim_win_set_width(self:get_win_id(), value)
- return self
-end
-
-function Component:add_syntax_highlights()
- local filetype = self.config:get('filetype')
- if not filetype or filetype == '' then
- return self
- end
- local buf = self:get_buf()
- local has_ts = false
- local ts_highlight = nil
- local ts_parsers = nil
- if not has_ts then
- has_ts, _ = pcall(require, 'nvim-treesitter')
- if has_ts then
- _, ts_highlight = pcall(require, 'nvim-treesitter.highlight')
- _, ts_parsers = pcall(require, 'nvim-treesitter.parsers')
- end
- end
- if has_ts and filetype and filetype ~= '' then
- local lang = ts_parsers.ft_to_lang(filetype)
- if ts_parsers.has_parser(lang) then
- pcall(ts_highlight.attach, buf, lang)
- else
- buffer.set_option(buf, 'syntax', filetype)
- end
- end
- return self
-end
-
-function Component:clear_syntax_highlights()
- local buf = self:get_buf()
- local has_ts = false
- if not has_ts then
- has_ts, _ = pcall(require, 'nvim-treesitter')
- end
- if has_ts then
- local active_buf = vim.treesitter.highlighter.active[buf]
- if active_buf then
- active_buf:destroy()
- else
- buffer.set_option(buf, 'syntax', '')
- end
- end
- return self
-end
-
-function Component:increment_paint_count()
- self.state.paint_count = self.state.paint_count + 1
- return self
-end
-
-function Component:set_filetype(filetype)
- assert(type(filetype) == 'string', 'type error :: expected string')
- self.config:set('filetype', filetype)
- local buf = self:get_buf()
- self:clear_syntax_highlights()
- self:add_syntax_highlights()
- buffer.set_option(buf, 'syntax', filetype)
- return self
-end
-
-function Component:set_cursor(row, col)
- assert(type(row) == 'number', 'type error :: expected number')
- assert(type(col) == 'number', 'type error :: expected number')
- navigation.set_cursor(self:get_win_id(), { row, col })
- if self:has_virtual_line_nr() then
- navigation.set_cursor(self:get_virtual_line_nr_win_id(), { row, col })
- end
- return self
-end
-
-function Component:set_buf_option(option, value)
- vim.api.nvim_buf_set_option(self:get_buf(), option, value)
- return self
-end
-
-function Component:set_win_option(option, value)
- vim.api.nvim_win_set_option(self:get_win_id(), option, value)
- return self
-end
-
-function Component:set_lines(lines, force)
- if self:is_static() and self:has_lines() and not force then
- return self
- end
- assert(type(lines) == 'table', 'type error :: expected table')
- self:increment_paint_count()
- self:clear_timers()
- buffer.set_lines(self:get_buf(), lines)
- return self
-end
-
-function Component:set_virtual_line_nr_lines(lines, hls)
- assert(type(lines) == 'table', 'type error :: expected table')
- assert(
- self:has_virtual_line_nr(),
- 'cannot set virtual number lines -- virtual number is disabled'
- )
- assert(self:get_virtual_line_nr(), 'VirtualLineNrDecorator not created')
- local virtual_line_nr = self:get_virtual_line_nr()
- virtual_line_nr:unmount()
- self:set_virtual_line_nr(
- VirtualLineNrDecorator:new(
- self.config:get('virtual_line_nr'),
- self.config:get('window_props'),
- self:get_buf()
- )
- )
- virtual_line_nr = self:get_virtual_line_nr()
- virtual_line_nr:mount()
- virtual_line_nr:set_lines(lines)
- virtual_line_nr:set_hls(hls)
- return self
-end
-
-function Component:set_centered_animated_text(
- frame_rate,
- frames,
- force,
- callback
-)
- assert(type(frame_rate) == 'number', 'type error :: expected number')
- assert(vim.tbl_islist(frames), 'type error :: expected list table')
- self:clear_timers()
- self:set_centered_text(frames[1], true, force)
- local frame_count = 1
- self.anim_id = vim.fn.timer_start(
- frame_rate,
- void(function()
- scheduler()
- if buffer.is_valid(self:get_buf()) then
- frame_count = frame_count + 1
- local selected_frame = frame_count % #frames
- selected_frame = selected_frame == 0 and 1 or selected_frame
- self:set_centered_text(
- string.format('%s', frames[selected_frame]),
- true
- )
- if callback then
- callback(frame_rate, frames, self.anim_id)
- end
- else
- self:clear_timers()
- end
- end),
- {
- ['repeat'] = -1,
- }
- )
- return self
-end
-
-function Component:set_loading(value, force)
- if self:is_static() and self:has_lines() and not force then
- return self
- end
- assert(type(value) == 'boolean', 'type error :: expected boolean')
- self:clear_timers()
- if value == self:get_loading() then
- return self
- end
- if value then
- self:set_cached_cursor(vim.api.nvim_win_get_cursor(self:get_win_id()))
- self.state.loading = value
- local animation_configuration = Component.state:get('loading')
- self:set_centered_animated_text(
- animation_configuration.frame_rate,
- animation_configuration.frames,
- force
- )
- else
- self:add_syntax_highlights()
- self.state.loading = value
- buffer.set_lines(self:get_buf(), self:get_cached_lines())
- self:set_win_option('cursorline', self.config:get('win_options').cursorline)
- navigation.set_cursor(self:get_win_id(), self:get_cached_cursor())
- self:set_cached_lines({})
- self.state.cursor = nil
- end
- return self
-end
-
-function Component:set_error(value, force)
- if self:is_static() and self:has_lines() and not force then
- return self
- end
- assert(type(value) == 'boolean', 'type error :: expected boolean')
- self:clear_timers()
- if value == self:get_error() then
- return self
- end
- if value then
- self.state.error = value
- self:set_centered_text(Component.state:get('error'))
- else
- self:add_syntax_highlights()
- self.state.error = value
- buffer.set_lines(self:get_buf(), self:get_cached_lines())
- self:set_win_option('cursorline', self.config:get('win_options').cursorline)
- self:set_cached_lines({})
- end
- return self
-end
-
-function Component:set_centered_text(text, in_animation, force)
- if self:is_static() and self:has_lines() and not force then
- return self
- end
- assert(type(text) == 'string', 'type error :: expected string')
- if not in_animation then
- self:clear_timers()
- end
- self:clear_syntax_highlights()
- local lines = {}
- local win_id = self:get_win_id()
- local height = vim.api.nvim_win_get_height(win_id)
- local width = vim.api.nvim_win_get_width(win_id)
- for _ = 1, height do
- lines[#lines + 1] = ''
- end
- lines[math.ceil(height / 2)] = string.rep(
- ' ',
- dimensions.calculate_text_center(text, width)
- ) .. text
- self:set_win_option('cursorline', false)
- self:set_cached_lines(buffer.get_lines(self:get_buf()))
- buffer.set_lines(self:get_buf(), lines)
- if self:has_virtual_line_nr() then
- buffer.set_lines(self:get_virtual_line_nr_buf(), {})
- end
- return self
-end
-
-function Component:set_mounted(value)
- assert(type(value) == 'boolean', 'type error :: expected boolean')
- self.state.mounted = value
- return self
-end
-
-function Component:on(cmd, handler, options)
- autocmd.buf.on(self:get_buf(), cmd, handler, options)
- return self
-end
-
-function Component:add_keymap(key, action)
- buffer.add_keymap(self:get_buf(), key, action)
- return self
-end
-
-function Component:remove_keymap(key)
- buffer.remove_keymap(self:get_buf(), key)
- return self
-end
-
-function Component:transpose_text(text, row, col)
- assert(vim.tbl_islist(text), 'type error :: expected list table')
- assert(#text == 2, 'invalid number of text entries')
- assert(type(row) == 'number', 'type error :: expected number')
- assert(type(col) == 'number', 'type error :: expected number')
- virtual_text.transpose_text(
- self:get_buf(),
- text[1],
- self:get_ns_id(),
- text[2],
- row,
- col
- )
-end
-
-function Component:transpose_line(texts, row)
- assert(vim.tbl_islist(texts), 'type error :: expected list table')
- assert(type(row) == 'number', 'type error :: expected number')
- virtual_text.transpose_line(self:get_buf(), texts, self:get_ns_id(), row)
-end
-
-function Component:call(fn)
- assert(type(fn) == 'function', 'type error :: expected function')
- vim.api.nvim_buf_call(self:get_buf(), fn)
- return self
-end
-
-function Component:focus()
- vim.api.nvim_set_current_win(self:get_win_id())
- return self
-end
-
-function Component:clear_timers()
- if self.anim_id then
- vim.fn.timer_stop(self.anim_id)
- end
-end
-
-function Component:make_border(config)
- if config.hl then
- local new_border = {}
- for _, char in pairs(config.chars) do
- if type(char) == 'table' then
- char[2] = config.hl
- new_border[#new_border + 1] = char
- else
- new_border[#new_border + 1] = { char, config.hl }
- end
- end
- return new_border
- end
- return config.chars
-end
-
-function Component:clear(force)
- sign.unplace(self:get_buf())
- virtual_text.clear(self:get_buf(), self:get_ns_id())
- if self:is_static() and not force then
- self:clear_timers()
- return
- end
- self:set_loading(false)
- self:set_error(false)
- self:set_lines({}, force)
- return self
-end
-
-function Component:mount()
- error('Component must implement mount method')
-end
-
-function Component:unmount()
- error('Component must implement unmount method')
-end
-
-return Component
diff --git a/lua/vgit/change.lua b/lua/vgit/Diff.lua
similarity index 67%
rename from lua/vgit/change.lua
rename to lua/vgit/Diff.lua
index 91536af4..79fb0fee 100644
--- a/lua/vgit/change.lua
+++ b/lua/vgit/Diff.lua
@@ -1,57 +1,128 @@
-local utils = require('vgit.utils')
-local dmp = require('vgit.lib.dmp')
-local scheduler = require('plenary.async.util').scheduler
+local CodeDTO = require('vgit.core.CodeDTO')
+local Object = require('vgit.core.Object')
+local dmp = require('vgit.vendor.dmp')
-local M = {}
+local Diff = Object:extend()
-M.constants = utils.readonly({ word_diff_max_lines = 4 })
+function Diff:new(hunks)
+ return setmetatable({
+ hunks = hunks,
+ max_lines = 4,
+ }, Diff)
+end
-local function create_change(opts)
- opts = opts or {}
- return {
- lines = opts.lines or {},
- current_lines = opts.current_lines or {},
- previous_lines = opts.previous_lines or {},
- lnum_changes = opts.lnum_changes or {},
- hunks = opts.hunks or {},
- marks = opts.marks or {},
- }
+function Diff:deleted_unified(lines)
+ local hunks = self.hunks
+ local hunk = hunks[1]
+ local type = hunk.type
+ local diff = hunk.diff
+ local start = hunk.start
+ local finish = hunk.finish
+ local lnum_changes = {}
+ local s = start
+ for _ = 1, #diff do
+ lnum_changes[#lnum_changes + 1] = {
+ lnum = s,
+ type = 'remove',
+ buftype = 'current',
+ }
+ s = s + 1
+ end
+ return CodeDTO:new({
+ lines = lines,
+ lnum_changes = lnum_changes,
+ hunks = hunks,
+ marks = {
+ {
+ type = type,
+ start = start,
+ finish = finish,
+ },
+ },
+ stat = hunk.stat,
+ })
end
-M.horizontal = function(lines, hunks)
+function Diff:deleted_split(lines)
+ local hunks = self.hunks
+ local hunk = hunks[1]
+ local type = hunk.type
+ local diff = hunk.diff
+ local start = hunk.start
+ local finish = hunk.finish
+ local lnum_changes = {}
+ local s = start
+ local current_lines = {}
+ for _ = 1, #diff do
+ current_lines[#current_lines + 1] = ''
+ lnum_changes[#lnum_changes + 1] = {
+ lnum = s,
+ buftype = 'previous',
+ type = 'remove',
+ }
+ lnum_changes[#lnum_changes + 1] = {
+ lnum = s,
+ buftype = 'current',
+ type = 'void',
+ }
+ s = s + 1
+ end
+ return CodeDTO:new({
+ previous_lines = lines,
+ current_lines = current_lines,
+ lnum_changes = lnum_changes,
+ hunks = hunks,
+ marks = {
+ {
+ type = type,
+ start = start,
+ finish = finish,
+ },
+ },
+ stat = hunk.stat,
+ })
+end
+
+function Diff:unified(lines)
+ local hunks = self.hunks
if #hunks == 0 then
- return utils.readonly(create_change({
+ return CodeDTO:new({
lines = lines,
hunks = hunks,
- }))
+ })
end
local new_lines = {}
local lnum_changes = {}
local marks = {}
+ local stat = {
+ added = 0,
+ removed = 0,
+ }
for key, value in pairs(lines) do
- scheduler()
new_lines[key] = value
end
local new_lines_added = 0
for i = 1, #hunks do
- scheduler()
local hunk = hunks[i]
local type = hunk.type
local diff = hunk.diff
local start = hunk.start + new_lines_added
local finish = hunk.finish + new_lines_added
+ local hunk_stat = hunk.stat
+ stat.added = stat.added + hunk_stat.added
+ stat.removed = stat.removed + hunk_stat.removed
if type == 'add' then
- marks[#marks + 1] = utils.readonly({
+ marks[#marks + 1] = {
type = type,
start = start,
finish = finish,
- })
+ }
for j = start, finish do
- scheduler()
- lnum_changes[#lnum_changes + 1] = utils.readonly({
+ lnum_changes[#lnum_changes + 1] = {
lnum = j,
type = 'add',
- })
+ buftype = 'current',
+ }
end
elseif type == 'remove' then
marks[#marks + 1] = {
@@ -61,18 +132,18 @@ M.horizontal = function(lines, hunks)
}
local s = start
for j = 1, #diff do
- scheduler()
local line = diff[j]
s = s + 1
new_lines_added = new_lines_added + 1
table.insert(new_lines, s, line:sub(2, #line))
- lnum_changes[#lnum_changes + 1] = utils.readonly({
+ lnum_changes[#lnum_changes + 1] = {
lnum = s,
type = 'remove',
- })
+ buftype = 'current',
+ }
end
marks[#marks].finish = start + #diff
- marks[#marks] = utils.readonly(marks[#marks])
+ marks[#marks] = marks[#marks]
elseif type == 'change' then
local removed_lines, added_lines = hunk:parse_diff()
marks[#marks + 1] = {
@@ -82,7 +153,6 @@ M.horizontal = function(lines, hunks)
}
local s = start
for j = 1, #diff do
- scheduler()
local line = diff[j]
local cleaned_line = line:sub(2, #line)
local line_type = line:sub(1, 1)
@@ -92,7 +162,7 @@ M.horizontal = function(lines, hunks)
local word_diff = nil
if
#removed_lines == #added_lines
- and #added_lines < M.constants.word_diff_max_lines
+ and #added_lines < self.max_lines
then
local d = dmp.diff_main(
cleaned_line,
@@ -101,16 +171,17 @@ M.horizontal = function(lines, hunks)
dmp.diff_cleanupSemantic(d)
word_diff = d
end
- lnum_changes[#lnum_changes + 1] = utils.readonly({
+ lnum_changes[#lnum_changes + 1] = {
lnum = s,
type = 'remove',
+ buftype = 'current',
word_diff = word_diff,
- })
+ }
elseif line_type == '+' then
local word_diff = nil
if
#removed_lines == #added_lines
- and #added_lines < M.constants.word_diff_max_lines
+ and #added_lines < self.max_lines
then
local d = dmp.diff_main(
cleaned_line,
@@ -119,42 +190,48 @@ M.horizontal = function(lines, hunks)
dmp.diff_cleanupSemantic(d)
word_diff = d
end
- lnum_changes[#lnum_changes + 1] = utils.readonly({
+ lnum_changes[#lnum_changes + 1] = {
lnum = s,
type = 'add',
+ buftype = 'current',
word_diff = word_diff,
- })
+ }
end
s = s + 1
end
marks[#marks].finish = start + #diff - 1
- marks[#marks] = utils.readonly(marks[#marks])
+ marks[#marks] = marks[#marks]
end
end
- return utils.readonly(create_change({
+ return CodeDTO:new({
lines = new_lines,
- hunks = hunks,
lnum_changes = lnum_changes,
+ hunks = hunks,
marks = marks,
- }))
+ stat = stat,
+ })
end
-M.vertical = function(lines, hunks)
+function Diff:split(lines)
+ local hunks = self.hunks
if #hunks == 0 then
- return utils.readonly(create_change({
+ return CodeDTO:new({
current_lines = lines,
previous_lines = lines,
hunks = hunks,
- }))
+ })
end
local current_lines = {}
local previous_lines = {}
local lnum_changes = {}
local void_line = ''
local marks = {}
+ local stat = {
+ added = 0,
+ removed = 0,
+ }
-- shallow copy
for key, value in pairs(lines) do
- scheduler()
current_lines[key] = value
previous_lines[key] = value
end
@@ -162,32 +239,33 @@ M.vertical = function(lines, hunks)
-- previous data, which means, the offset needs to be added to our hunks.
local new_lines_added = 0
for i = 1, #hunks do
- scheduler()
local hunk = hunks[i]
local type = hunk.type
local start = hunk.start + new_lines_added
local finish = hunk.finish + new_lines_added
local diff = hunk.diff
+ local hunk_stat = hunk.stat
+ stat.added = stat.added + hunk_stat.added
+ stat.removed = stat.removed + hunk_stat.removed
if type == 'add' then
- marks[#marks + 1] = utils.readonly({
+ marks[#marks + 1] = {
type = type,
start = start,
finish = finish,
- })
+ }
-- Remove the line indicating that these lines were inserted in current_lines.
for j = start, finish do
- scheduler()
previous_lines[j] = void_line
- lnum_changes[#lnum_changes + 1] = utils.readonly({
+ lnum_changes[#lnum_changes + 1] = {
lnum = j,
buftype = 'previous',
type = 'void',
- })
- lnum_changes[#lnum_changes + 1] = utils.readonly({
+ }
+ lnum_changes[#lnum_changes + 1] = {
lnum = j,
buftype = 'current',
type = 'add',
- })
+ }
end
elseif type == 'remove' then
local current_new_lines_added = 0
@@ -197,26 +275,25 @@ M.vertical = function(lines, hunks)
finish = nil,
}
for j = 1, #diff do
- scheduler()
local line = diff[j]
start = start + 1
current_new_lines_added = current_new_lines_added + 1
table.insert(current_lines, start, void_line)
table.insert(previous_lines, start, line:sub(2, #line))
- lnum_changes[#lnum_changes + 1] = utils.readonly({
+ lnum_changes[#lnum_changes + 1] = {
lnum = start,
buftype = 'current',
type = 'void',
- })
- lnum_changes[#lnum_changes + 1] = utils.readonly({
+ }
+ lnum_changes[#lnum_changes + 1] = {
lnum = start,
buftype = 'previous',
type = 'remove',
- })
+ }
end
new_lines_added = new_lines_added + current_new_lines_added
marks[#marks].finish = finish + current_new_lines_added
- marks[#marks] = utils.readonly(marks[#marks])
+ marks[#marks] = marks[#marks]
elseif type == 'change' then
marks[#marks + 1] = {
type = type,
@@ -235,7 +312,6 @@ M.vertical = function(lines, hunks)
-- Hunk finish index does not indicate the total number of lines that may have a diff.
-- Which is why I am inserting empty lines into both the current and previous data arrays.
for j = finish + 1, (start + max_lines) - 1 do
- scheduler()
new_lines_added = new_lines_added + 1
table.insert(current_lines, j, void_line)
table.insert(previous_lines, j, void_line)
@@ -243,7 +319,6 @@ M.vertical = function(lines, hunks)
-- With the new calculated range I simply loop over and add the removed
-- and added lines to their corresponding arrays that contain a buffer lines.
for j = start, start + max_lines - 1 do
- scheduler()
local recalculated_index = (j - start) + 1
local added_line = added_lines[recalculated_index]
local removed_line = removed_lines[recalculated_index]
@@ -251,7 +326,7 @@ M.vertical = function(lines, hunks)
local word_diff = nil
if
#removed_lines == #added_lines
- and #added_lines < M.constants.word_diff_max_lines
+ and #added_lines < self.max_lines
then
local d = dmp.diff_main(
removed_line,
@@ -260,18 +335,18 @@ M.vertical = function(lines, hunks)
dmp.diff_cleanupSemantic(d)
word_diff = d
end
- lnum_changes[#lnum_changes + 1] = utils.readonly({
+ lnum_changes[#lnum_changes + 1] = {
lnum = j,
buftype = 'previous',
type = 'remove',
word_diff = word_diff,
- })
+ }
end
if added_line then
local word_diff = nil
if
#removed_lines == #added_lines
- and #added_lines < M.constants.word_diff_max_lines
+ and #added_lines < self.max_lines
then
local d = dmp.diff_main(
added_line,
@@ -280,26 +355,26 @@ M.vertical = function(lines, hunks)
dmp.diff_cleanupSemantic(d)
word_diff = d
end
- lnum_changes[#lnum_changes + 1] = utils.readonly({
+ lnum_changes[#lnum_changes + 1] = {
lnum = j,
buftype = 'current',
type = 'add',
word_diff = word_diff,
- })
+ }
end
if added_line and not removed_line then
- lnum_changes[#lnum_changes + 1] = utils.readonly({
+ lnum_changes[#lnum_changes + 1] = {
lnum = j,
buftype = 'previous',
type = 'void',
- })
+ }
end
if removed_line and not added_line then
- lnum_changes[#lnum_changes + 1] = utils.readonly({
+ lnum_changes[#lnum_changes + 1] = {
lnum = j,
buftype = 'current',
type = 'void',
- })
+ }
end
previous_lines[j] = removed_line or void_line
current_lines[j] = added_line or void_line
@@ -309,16 +384,17 @@ M.vertical = function(lines, hunks)
else
marks[#marks].finish = finish
end
- marks[#marks] = utils.readonly(marks[#marks])
+ marks[#marks] = marks[#marks]
end
end
- return utils.readonly(create_change({
+ return CodeDTO:new({
current_lines = current_lines,
previous_lines = previous_lines,
- hunks = hunks,
lnum_changes = lnum_changes,
+ hunks = hunks,
marks = marks,
- }))
+ stat = stat,
+ })
end
-return M
+return Diff
diff --git a/lua/vgit/Feature.lua b/lua/vgit/Feature.lua
new file mode 100644
index 00000000..c4b228b2
--- /dev/null
+++ b/lua/vgit/Feature.lua
@@ -0,0 +1,92 @@
+local fs = require('vgit.core.fs')
+local loop = require('vgit.core.loop')
+local Object = require('vgit.core.Object')
+local console = require('vgit.core.console')
+
+local Feature = Object:extend()
+
+function Feature:new(git_store)
+ return setmetatable({ git_store = git_store }, Feature)
+end
+
+function Feature:is_buffer_valid(buffer)
+ loop.await_fast_event()
+ if not buffer:is_valid() then
+ console.debug(string.format('The buffer %s is invalid', buffer.filename))
+ return false
+ end
+ return true
+end
+
+function Feature:is_buffer_in_git_store(buffer)
+ loop.await_fast_event()
+ if not self.git_store:contains(buffer) then
+ console.debug(
+ string.format('The buffer %s not in git store', buffer.filename)
+ )
+ return false
+ end
+ return true
+end
+
+function Feature:is_buffer_in_disk(buffer)
+ loop.await_fast_event()
+ local filename = buffer.filename
+ if not filename or filename == '' then
+ console.debug(
+ string.format('The buffer #%s does not have a filename', buffer.bufnr)
+ )
+ return false
+ end
+ if not fs.exists(filename) then
+ console.debug(
+ string.format('The buffer %s does exist in disk', buffer.filename)
+ )
+ return
+ end
+ return true
+end
+
+function Feature:is_inside_git_dir(buffer)
+ loop.await_fast_event()
+ local is_inside_git_dir = buffer.git_object:is_inside_git_dir()
+ loop.await_fast_event()
+ if not is_inside_git_dir then
+ console.debug(
+ 'Live gutter feature is disabled, we are not in a git repository'
+ )
+ return false
+ end
+ return true
+end
+
+function Feature:is_buffer_ignored(buffer)
+ loop.await_fast_event()
+ local is_ignored = buffer.git_object:is_ignored()
+ loop.await_fast_event()
+ if is_ignored then
+ console.debug(
+ string.format(
+ 'The buffer %s will be ignored, match found in .gitignore',
+ buffer.filename
+ )
+ )
+ return true
+ end
+ return false
+end
+
+function Feature:is_buffer_tracked(buffer)
+ loop.await_fast_event()
+ local tracked_filename = buffer.git_object:tracked_filename()
+ loop.await_fast_event()
+ if tracked_filename == '' then
+ console.debug(
+ string.format('The buffer %s is not tracked', buffer.filename)
+ )
+ return false
+ end
+ return true
+end
+
+return Feature
diff --git a/lua/vgit/GitStore.lua b/lua/vgit/GitStore.lua
new file mode 100644
index 00000000..eac1c2e7
--- /dev/null
+++ b/lua/vgit/GitStore.lua
@@ -0,0 +1,64 @@
+local Object = require('vgit.core.Object')
+
+local GitStore = Object:extend()
+
+function GitStore:new()
+ return setmetatable({ buffers = {} }, GitStore)
+end
+
+function GitStore:add(buffer)
+ self.buffers[buffer.bufnr] = buffer
+end
+
+function GitStore:contains(buffer)
+ return self.buffers[buffer.bufnr] ~= nil
+end
+
+function GitStore:remove(buffer)
+ buffer = self.buffers[buffer.bufnr]
+ self.buffers[buffer.bufnr] = nil
+ return buffer
+end
+
+function GitStore:get(buffer)
+ return self.buffers[buffer.bufnr]
+end
+
+function GitStore:clean(callback)
+ local bufnrs = vim.api.nvim_list_bufs()
+ local bufnr_map = {}
+ for i = 1, #bufnrs do
+ local bufnr = bufnrs[i]
+ bufnr_map[bufnr] = true
+ end
+ local buffers = {}
+ for bufnr, buffer in pairs(self.buffers) do
+ if not bufnr_map[bufnr] then
+ buffers[#buffers + 1] = buffer
+ self.buffers[bufnr] = nil
+ if callback then
+ callback(buffer)
+ end
+ end
+ end
+ return buffers
+end
+
+function GitStore:current()
+ local bufnr = vim.api.nvim_get_current_buf()
+ return self.buffers[bufnr]
+end
+
+function GitStore:size()
+ local count = 0
+ for _, _ in pairs(self.buffers) do
+ count = count + 1
+ end
+ return count
+end
+
+function GitStore:is_empty()
+ return self:size() == 0
+end
+
+return GitStore
diff --git a/lua/vgit/Interface.lua b/lua/vgit/Interface.lua
deleted file mode 100644
index df6e141e..00000000
--- a/lua/vgit/Interface.lua
+++ /dev/null
@@ -1,64 +0,0 @@
-local Object = require('plenary.class')
-local assert = require('vgit.assertion').assert
-
-local Interface = Object:extend()
-
-function Interface:new(state)
- assert(
- type(state) == 'nil' or type(state) == 'table',
- 'type error :: expected table or nil'
- )
- return setmetatable(
- { data = type(state) == 'table' and state or {} },
- Interface
- )
-end
-
-function Interface:get(key)
- assert(type(key) == 'string', 'type error :: expected string')
- assert(self.data[key] ~= nil, string.format('key "%s" does not exist', key))
- return self.data[key]
-end
-
-function Interface:set(key, value)
- assert(self.data[key] ~= nil, string.format('key "%s" does not exist', key))
- assert(
- type(self.data[key]) == type(value),
- string.format('type error :: expected %s', key)
- )
- self.data[key] = value
-end
-
-function Interface:assign(config)
- if not config then
- return self
- end
- local function assign(state_segment, config_segment)
- local state_segment_type = type(state_segment)
- local config_segment_type = type(config_segment)
- assert(state_segment_type == config_segment_type, 'invalid config')
- if
- config_segment_type == 'table' and not vim.tbl_islist(config_segment)
- then
- for key, state_value in pairs(state_segment) do
- local config_value = config_segment[key]
- if config_value ~= nil then
- local state_value_type = type(state_value)
- local config_value_type = type(config_value)
- if
- config_value_type == 'table' and not vim.tbl_islist(config_value)
- then
- assign(state_segment[key], config_segment[key])
- else
- assert(state_value_type == config_value_type, 'invalid config')
- state_segment[key] = config_value
- end
- end
- end
- end
- end
- assign(self.data, config)
- return self
-end
-
-return Interface
diff --git a/lua/vgit/Job.lua b/lua/vgit/Job.lua
deleted file mode 100644
index d9053fcb..00000000
--- a/lua/vgit/Job.lua
+++ /dev/null
@@ -1,319 +0,0 @@
-local vim = vim
-local uv = vim.loop
-
-local Object = require('plenary.class')
-local F = require('plenary.functional')
-
-local Job = Object:extend()
-
-local function close_safely(j, key)
- local handle = j[key]
- if not handle then
- return
- end
- if not handle:is_closing() then
- handle:close()
- end
-end
-
-local start_shutdown_check = function(child, options, code, signal)
- uv.check_start(child._shutdown_check, function()
- if not child:_pipes_are_closed(options) then
- return
- end
- uv.check_stop(child._shutdown_check)
- child._shutdown_check = nil
- child:_shutdown(code, signal)
- child = nil
- end)
-end
-
-local shutdown_factory = function(child, options)
- return function(code, signal)
- if uv.is_closing(child._shutdown_check) then
- return child:shutdown(code, signal)
- else
- start_shutdown_check(child, options, code, signal)
- end
- end
-end
-
-local function expand(path)
- if vim.in_fast_event() then
- return assert(
- uv.fs_realpath(path),
- string.format('Path must be valid: %s', path)
- )
- else
- return vim.fn.expand(path, true)
- end
-end
-
-function Job:new(o)
- if not o then
- error(debug.traceback('Options are required for Job:new'))
- end
- local command = o.command
- if not command then
- if o[1] then
- command = o[1]
- else
- error(debug.traceback('\'command\' is required for Job:new'))
- end
- elseif o[1] then
- error(debug.traceback('Cannot pass both \'command\' and array args'))
- end
- local args = o.args
- if not args then
- if #o > 1 then
- args = { select(2, unpack(o)) }
- end
- end
- local ok, is_exe = pcall(vim.fn.executable, command)
- if not o.skip_validation and ok and 1 ~= is_exe then
- error(debug.traceback(command .. ': Executable not found'))
- end
- local obj = {}
- obj.command = command
- obj.args = args
- obj._raw_cwd = o.cwd
- if o.env then
- if type(o.env) ~= 'table' then
- error('[plenary.job] env has to be a table')
- end
- local transform = {}
- for k, v in pairs(o.env) do
- if type(k) == 'number' then
- table.insert(transform, v)
- elseif type(k) == 'string' then
- table.insert(transform, k .. '=' .. tostring(v))
- end
- end
- obj.env = transform
- end
- if o.interactive == nil then
- obj.interactive = true
- else
- obj.interactive = o.interactive
- end
- obj.enable_handlers = F.if_nil(o.enable_handlers, true, o.enable_handlers)
- obj.enable_recording = F.if_nil(
- F.if_nil(o.enable_recording, o.enable_handlers, o.enable_recording),
- true,
- o.enable_recording
- )
- if not obj.enable_handlers and obj.enable_recording then
- error('[plenary.job] Cannot record items but disable handlers')
- end
- obj._user_on_start = o.on_start
- obj._user_on_stdout = o.on_stdout
- obj._user_on_stderr = o.on_stderr
- obj._user_on_exit = o.on_exit
- obj._maximum_results = o.maximum_results
- obj.user_data = {}
- self._reset(obj)
- return setmetatable(obj, self)
-end
-
-function Job:_reset()
- self.is_shutdown = nil
- if
- self._shutdown_check
- and uv.is_active(self._shutdown_check)
- and not uv.is_closing(self._shutdown_check)
- then
- vim.api.nvim_err_writeln(
- debug.traceback('We may be memory leaking here. Please report to TJ.')
- )
- end
- self._shutdown_check = uv.new_check()
- self.stdout = nil
- self.stderr = nil
- self._stdout_reader = nil
- self._stderr_reader = nil
- if self.enable_recording then
- self._stdout_results = {}
- self._stderr_results = {}
- else
- self._stdout_results = nil
- self._stderr_results = nil
- end
-end
-
-function Job:_stop()
- close_safely(self, 'stdin')
- close_safely(self, 'stderr')
- close_safely(self, 'stdout')
- close_safely(self, 'handle')
-end
-
-function Job:_pipes_are_closed(options)
- for _, pipe in ipairs({ options.stdin, options.stdout, options.stderr }) do
- if pipe and not uv.is_closing(pipe) then
- return false
- end
- end
- return true
-end
-
-function Job:shutdown(code, signal)
- if not uv.is_active(self._shutdown_check) then
- vim.wait(1000, function()
- return self:_pipes_are_closed(self) and self.is_shutdown
- end, 1, true)
- end
- self:_shutdown(code, signal)
-end
-
-function Job:_shutdown(code, signal)
- if self.is_shutdown then
- return
- end
- self.code = code
- self.signal = signal
- if self._stdout_reader then
- pcall(self._stdout_reader, nil, nil, true)
- end
- if self._stderr_reader then
- pcall(self._stderr_reader, nil, nil, true)
- end
- if self._user_on_exit then
- self:_user_on_exit(code, signal)
- end
- if self.stdout then
- self.stdout:read_stop()
- end
- if self.stderr then
- self.stderr:read_stop()
- end
- self:_stop()
- self.is_shutdown = true
- self._stdout_reader = nil
- self._stderr_reader = nil
-end
-
-function Job:_create_uv_options()
- local options = {}
- options.command = self.command
- options.args = self.args
- options.stdio = { self.stdin, self.stdout, self.stderr }
- if self._raw_cwd then
- options.cwd = expand(self._raw_cwd)
- end
- if self.env then
- options.env = self.env
- end
- return options
-end
-
-local on_output = function(self, result_key, cb)
- return coroutine.wrap(function(err, data, is_complete)
- local result_index = 1
- local line, start, result_line, found_newline
- while true do
- if data then
- data = data:gsub('\r', '')
- local processed_index = 1
- local data_length = #data + 1
- repeat
- start = string.find(data, '\n', processed_index, true) or data_length
- line = string.sub(data, processed_index, start - 1)
- found_newline = start ~= data_length
- if result_line then
- result_line = result_line .. line
- elseif start ~= processed_index or found_newline then
- result_line = line
- end
- if found_newline then
- if not result_line then
- return vim.api.nvim_err_writeln(
- 'Broken data thing due to: '
- .. tostring(result_line)
- .. ' '
- .. tostring(data)
- )
- end
- if self.enable_recording then
- self[result_key][result_index] = result_line
- end
- if cb then
- cb(err, result_line, self)
- end
- if
- self._maximum_results and result_index > self._maximum_results
- then
- vim.schedule(function()
- self:shutdown()
- end)
- return
- end
- result_index = result_index + 1
- result_line = nil
- end
- processed_index = start + 1
- until not found_newline
- end
- if self.enable_recording then
- self[result_key][result_index] = result_line
- end
- if cb and is_complete and not found_newline then
- cb(err, result_line, self)
- end
- if (data == nil and not result_line) or is_complete then
- return
- end
- err, data, is_complete = coroutine.yield()
- end
- end)
-end
-
-function Job:_prepare_pipes()
- self:_stop()
- self.stdout = uv.new_pipe(false)
- self.stderr = uv.new_pipe(false)
-end
-
-function Job:_execute()
- local options = self:_create_uv_options()
- if self._user_on_start then
- self:_user_on_start()
- end
- self.handle, self.pid = uv.spawn(
- options.command,
- options,
- shutdown_factory(self, options)
- )
- if not self.handle then
- error(debug.traceback('Failed to spawn process: ' .. vim.inspect(self)))
- end
- if self.enable_handlers then
- self._stdout_reader = on_output(
- self,
- '_stdout_results',
- self._user_on_stdout
- )
- self.stdout:read_start(self._stdout_reader)
- self._stderr_reader = on_output(
- self,
- '_stderr_results',
- self._user_on_stderr
- )
- self.stderr:read_start(self._stderr_reader)
- end
- return self
-end
-
-function Job:start()
- self:_reset()
- self:_prepare_pipes()
- self:_execute()
-end
-
-function Job.is_job(item)
- if type(item) ~= 'table' then
- return false
- end
- return getmetatable(item) == Job
-end
-
-return Job
diff --git a/lua/vgit/Marker.lua b/lua/vgit/Marker.lua
new file mode 100644
index 00000000..dd7d78f0
--- /dev/null
+++ b/lua/vgit/Marker.lua
@@ -0,0 +1,48 @@
+local virtual_text = require('vgit.core.virtual_text')
+local Object = require('vgit.core.Object')
+
+local Marker = Object:extend()
+
+function Marker:new()
+ return setmetatable({
+ timer_id = nil,
+ epoch = 1000,
+ ns_id = vim.api.nvim_create_namespace(''),
+ }, Marker)
+end
+
+function Marker:clear_timer()
+ if self.timer_id then
+ vim.fn.timer_stop(self.timer_id)
+ self.timer_id = nil
+ end
+end
+
+function Marker:mark_current_hunk(buffer, window, text)
+ self:unmark_current_hunk(buffer)
+ self:clear_timer()
+ virtual_text.transpose_text(
+ buffer,
+ text,
+ self.ns_id,
+ 'GitComment',
+ window:get_lnum() - 1,
+ 0,
+ 'right_align'
+ )
+ self.timer_id = vim.fn.timer_start(self.epoch, function()
+ if buffer:is_valid() then
+ virtual_text.clear(buffer, self.ns_id)
+ end
+ self:clear_timer()
+ end)
+end
+
+function Marker:unmark_current_hunk(buffer)
+ self:clear_timer()
+ if buffer:is_valid() then
+ virtual_text.clear(buffer, self.ns_id)
+ end
+end
+
+return Marker
diff --git a/lua/vgit/navigation.lua b/lua/vgit/Navigation.lua
similarity index 55%
rename from lua/vgit/navigation.lua
rename to lua/vgit/Navigation.lua
index 0425d430..c467daad 100644
--- a/lua/vgit/navigation.lua
+++ b/lua/vgit/Navigation.lua
@@ -1,8 +1,64 @@
-local M = {}
+local Object = require('vgit.core.Object')
-M.mark_up = function(win, cursor, marks)
- local lnum = cursor[1]
- local line_count = vim.api.nvim_buf_line_count(0)
+local Navigation = Object:extend()
+
+function Navigation:new()
+ return setmetatable({}, Navigation)
+end
+
+function Navigation:mark_select(component, selected, marks, position)
+ local position_window = function()
+ if position == 'top' then
+ vim.cmd('norm! zt')
+ elseif position == 'center' then
+ vim.cmd('norm! zz')
+ elseif position == 'bottom' then
+ vim.cmd('norm! zb')
+ end
+ end
+ local line_count = component:get_line_count()
+ local new_lnum = nil
+ local mark_index = 0
+ for i = #marks, 1, -1 do
+ local mark = marks[i]
+ if i == selected then
+ new_lnum = mark.start
+ mark_index = i
+ break
+ end
+ end
+ if not new_lnum or new_lnum < 1 or new_lnum > line_count then
+ if marks and marks[#marks] and marks[#marks].start then
+ new_lnum = marks[#marks].start
+ mark_index = #marks
+ else
+ new_lnum = 1
+ mark_index = 1
+ end
+ end
+ if new_lnum then
+ component:set_lnum(new_lnum)
+ component:call(position_window)
+ return mark_index
+ else
+ local start_hunks_lnum = marks[#marks].start
+ start_hunks_lnum = start_hunks_lnum
+ and (start_hunks_lnum >= 1 or new_lnum <= line_count)
+ and start_hunks_lnum
+ or 1
+ mark_index = start_hunks_lnum
+ and (start_hunks_lnum >= 1 or new_lnum <= line_count)
+ and #marks
+ or 1
+ component:set_lnum(start_hunks_lnum)
+ component:call(position_window)
+ return mark_index
+ end
+end
+
+function Navigation:mark_up(window, buffer, marks)
+ local lnum = window:get_lnum()
+ local line_count = buffer:get_line_count()
local new_lnum = nil
local mark_index = 0
for i = #marks, 1, -1 do
@@ -27,8 +83,9 @@ M.mark_up = function(win, cursor, marks)
end
end
if new_lnum and lnum ~= new_lnum then
- vim.api.nvim_win_set_cursor(win, { new_lnum, 0 })
- vim.cmd('norm! zz')
+ window:set_lnum(new_lnum):call(function()
+ vim.cmd('norm! zz')
+ end)
return mark_index
else
local finish_hunks_lnum = marks[#marks].finish
@@ -40,24 +97,22 @@ M.mark_up = function(win, cursor, marks)
and (finish_hunks_lnum >= 1 or new_lnum <= line_count)
and #marks
or 1
- vim.api.nvim_win_set_cursor(win, { finish_hunks_lnum, 0 })
- vim.cmd('norm! zz')
+ window:set_lnum(finish_hunks_lnum):call(function()
+ vim.cmd('norm! zz')
+ end)
return mark_index
end
end
-M.mark_down = function(win, cursor, marks)
- local lnum = cursor[1]
- local line_count = vim.api.nvim_buf_line_count(0)
+function Navigation:mark_down(window, buffer, marks)
+ local lnum = window:get_lnum()
+ local line_count = buffer:get_line_count()
local new_lnum = nil
local selected_mark = nil
local mark_index = 0
for i = 1, #marks do
local mark = marks[i]
local compare_lnum = lnum
- if mark.type == 'remove' then
- compare_lnum = lnum + 1
- end
if mark.start > compare_lnum then
new_lnum = mark.start
mark_index = i
@@ -66,6 +121,10 @@ M.mark_down = function(win, cursor, marks)
new_lnum = mark.finish
mark_index = i
break
+ elseif compare_lnum == mark.finish and compare_lnum == line_count then
+ new_lnum = mark.finish
+ mark_index = i
+ break
end
end
if not new_lnum or new_lnum < 1 or new_lnum > line_count then
@@ -82,8 +141,9 @@ M.mark_down = function(win, cursor, marks)
compare_lnum = compare_lnum + 1
end
if new_lnum and compare_lnum ~= new_lnum then
- vim.api.nvim_win_set_cursor(win, { new_lnum, 0 })
- vim.cmd('norm! zz')
+ window:set_lnum(new_lnum):call(function()
+ vim.cmd('norm! zz')
+ end)
return mark_index
else
local first_hunk_start_lnum = marks[1].start
@@ -95,14 +155,15 @@ M.mark_down = function(win, cursor, marks)
and (first_hunk_start_lnum >= 1 or new_lnum <= line_count)
and 1
or 1
- vim.api.nvim_win_set_cursor(win, { first_hunk_start_lnum, 0 })
- vim.cmd('norm! zz')
+ window:set_lnum(first_hunk_start_lnum):call(function()
+ vim.cmd('norm! zz')
+ end)
return mark_index
end
end
-M.hunk_up = function(win, cursor, hunks)
- local lnum = cursor[1]
+function Navigation:hunk_up(window, hunks)
+ local lnum = window:get_lnum()
local new_lnum = nil
local selected = nil
for i = #hunks, 1, -1 do
@@ -121,8 +182,9 @@ M.hunk_up = function(win, cursor, hunks)
new_lnum = 1
end
if new_lnum and lnum ~= new_lnum then
- vim.api.nvim_win_set_cursor(win, { new_lnum, 0 })
- vim.cmd('norm! zz')
+ window:set_lnum(new_lnum):call(function()
+ vim.cmd('norm! zz')
+ end)
return selected
else
local finish_hunks_lnum = hunks[#hunks].finish
@@ -131,14 +193,15 @@ M.hunk_up = function(win, cursor, hunks)
finish_hunks_lnum = 1
selected = 1
end
- vim.api.nvim_win_set_cursor(win, { finish_hunks_lnum, 0 })
- vim.cmd('norm! zz')
+ window:set_lnum(finish_hunks_lnum):call(function()
+ vim.cmd('norm! zz')
+ end)
return selected
end
end
-M.hunk_down = function(win, cursor, hunks)
- local lnum = cursor[1]
+function Navigation:hunk_down(window, hunks)
+ local lnum = window:get_lnum()
local new_lnum = nil
local selected = nil
for i = 1, #hunks do
@@ -157,8 +220,9 @@ M.hunk_down = function(win, cursor, hunks)
new_lnum = 1
end
if new_lnum then
- vim.api.nvim_win_set_cursor(win, { new_lnum, 0 })
- vim.cmd('norm! zz')
+ window:set_lnum(new_lnum):call(function()
+ vim.cmd('norm! zz')
+ end)
return selected
else
local first_hunk_start_lnum = hunks[1].start
@@ -167,14 +231,11 @@ M.hunk_down = function(win, cursor, hunks)
first_hunk_start_lnum = 1
selected = 1
end
- vim.api.nvim_win_set_cursor(win, { first_hunk_start_lnum, 0 })
- vim.cmd('norm! zz')
+ window:set_lnum(first_hunk_start_lnum):call(function()
+ vim.cmd('norm! zz')
+ end)
return selected
end
end
-M.set_cursor = function(...)
- return pcall(vim.api.nvim_win_set_cursor, ...)
-end
-
-return M
+return Navigation
diff --git a/lua/vgit/Preview.lua b/lua/vgit/Preview.lua
deleted file mode 100644
index 742a708f..00000000
--- a/lua/vgit/Preview.lua
+++ /dev/null
@@ -1,491 +0,0 @@
-local Object = require('plenary.class')
-local render_store = require('vgit.stores.render_store')
-local autocmd = require('vgit.autocmd')
-local logger = require('vgit.logger')
-local sign = require('vgit.sign')
-local virtual_text = require('vgit.virtual_text')
-local assert = require('vgit.assertion').assert
-local buffer = require('vgit.buffer')
-local scheduler = require('plenary.async.util').scheduler
-local navigation = require('vgit.navigation')
-local CodeComponent = require('vgit.components.CodeComponent')
-
-local Preview = Object:extend()
-
-function Preview:new(components, opts)
- assert(type(components) == 'table', 'type error :: expected table')
- assert(
- type(opts) == 'table' or type(opts) == 'nil',
- 'type error :: expected string or nil'
- )
- opts = opts or {}
- return setmetatable({
- components = components,
- state = {
- mounted = false,
- rendered = false,
- win_toggle_queue = {},
- },
- parent_buf = vim.api.nvim_get_current_buf(),
- parent_win = vim.api.nvim_get_current_win(),
- temporary = opts.temporary or false,
- layout_type = opts.layout_type or nil,
- selected = opts.selected or nil,
- data = nil,
- err = nil,
- }, Preview)
-end
-
-function Preview:notify(text)
- if self.layout_type == 'horizontal' then
- self.components.preview:notify(text)
- else
- self.components.previous:notify(text)
- end
-end
-
-function Preview:navigate_code(direction)
- if not self.data then
- return
- end
- if not self.data.diff_change then
- return
- end
- if not self.data.diff_change.marks then
- return
- end
- local components = self:get_components()
- if components.table and components.table:is_focused() then
- return
- end
- local marks = self.data.diff_change.marks
- if #marks == 0 then
- return self:notify('There are no changes')
- end
- local mark_index = nil
- local win = vim.api.nvim_get_current_win()
- if direction == 'up' then
- mark_index = navigation.mark_up(win, vim.api.nvim_win_get_cursor(0), marks)
- end
- if direction == 'down' then
- mark_index = navigation.mark_down(
- win,
- vim.api.nvim_win_get_cursor(0),
- marks
- )
- end
- if mark_index then
- scheduler()
- self:notify(
- string.format('%s%s/%s Changes', string.rep(' ', 1), mark_index, #marks)
- )
- end
-end
-
-function Preview:highlight_diff_change(data)
- local lnum_changes = data.lnum_changes
- local layout_type = self.layout_type or 'horizontal'
- local components = self:get_components()
- local ns_id = vim.api.nvim_create_namespace('tanvirtin/vgit.nvim/paint')
- scheduler()
- for i = 1, #lnum_changes do
- scheduler()
- local datum, component, buf = lnum_changes[i], nil, nil
- if layout_type == 'horizontal' then
- component = components.preview
- buf = component:get_buf()
- elseif layout_type == 'vertical' then
- component = components[datum.buftype]
- buf = component:get_buf()
- end
- if not buf or not component then
- logger.error('There are no component or buffer to highlight the changes')
- return
- end
- local type, lnum, word_diff = datum.type, datum.lnum, datum.word_diff
- local defined_sign = render_store.get('preview').sign.hls[type]
- if defined_sign then
- scheduler()
- sign.place(
- buf,
- lnum,
- defined_sign,
- render_store.get('preview').sign.priority
- )
- scheduler()
- end
- if type == 'void' then
- scheduler()
- local void_line = string.rep(
- render_store.get('preview').symbols.void,
- vim.api.nvim_win_get_width(component:get_win_id())
- )
- scheduler()
- virtual_text.add(buf, ns_id, lnum - 1, 0, {
- id = lnum,
- virt_text = { { void_line, 'LineNr' } },
- virt_text_pos = 'overlay',
- })
- scheduler()
- end
- local texts = {}
- if word_diff then
- local offset = 0
- for j = 1, #word_diff do
- scheduler()
- local segment = word_diff[j]
- local operation, fragment = unpack(segment)
- if operation == -1 then
- local hl = type == 'remove' and 'VGitViewWordRemove'
- or 'VGitViewWordAdd'
- texts[#texts + 1] = { fragment, hl }
- elseif operation == 0 then
- texts[#texts + 1] = {
- fragment,
- nil,
- }
- end
- if operation == 0 or operation == -1 then
- offset = offset + #fragment
- end
- end
- scheduler()
- virtual_text.transpose_line(buf, texts, ns_id, lnum - 1)
- scheduler()
- end
- end
-end
-
-function Preview:make_virtual_line_nr(data)
- local components = self:get_components()
- local line_nr_count = 1
- local virtual_nr_lines = {}
- local hls = {}
- local common_hl = 'LineNr'
- local layout_type = self.layout_type or 'horizontal'
- if layout_type == 'horizontal' then
- local component = components.preview
- local lnum_change_map = {}
- for i = 1, #data.lnum_changes do
- scheduler()
- local lnum_change = data.lnum_changes[i]
- lnum_change_map[lnum_change.lnum] = lnum_change
- end
- for i = 1, #data.lines do
- scheduler()
- local lnum_change = lnum_change_map[i]
- if lnum_change and lnum_change.type == 'remove' then
- virtual_nr_lines[#virtual_nr_lines + 1] = ''
- hls[#hls + 1] = common_hl
- else
- virtual_nr_lines[#virtual_nr_lines + 1] = string.format(
- '%s',
- line_nr_count
- )
- hls[#hls + 1] = common_hl
- if lnum_change and lnum_change.type == 'add' then
- hls[#hls] = render_store.get('sign').hls.add
- elseif lnum_change and lnum_change.type == 'remove' then
- hls[#hls] = render_store.get('sign').hls.remove
- end
- line_nr_count = line_nr_count + 1
- end
- end
- scheduler()
- component:set_virtual_line_nr_lines(virtual_nr_lines, hls)
- scheduler()
- for i = 1, #data.lines do
- scheduler()
- local lnum_change = lnum_change_map[i]
- if lnum_change then
- local type, lnum = lnum_change.type, lnum_change.lnum
- local defined_sign = render_store.get('preview').sign.hls[type]
- if defined_sign then
- scheduler()
- sign.place(
- component:get_virtual_line_nr_buf(),
- lnum,
- defined_sign,
- render_store.get('preview').sign.priority
- )
- scheduler()
- end
- end
- end
- elseif layout_type == 'vertical' then
- local previous_component = components.previous
- local current_component = components.current
- local current_lnum_change_map = {}
- local previous_lnum_change_map = {}
- for i = 1, #data.lnum_changes do
- scheduler()
- local lnum_change = data.lnum_changes[i]
- if lnum_change.buftype == 'current' then
- current_lnum_change_map[lnum_change.lnum] = lnum_change
- elseif lnum_change.buftype == 'previous' then
- previous_lnum_change_map[lnum_change.lnum] = lnum_change
- end
- end
- for i = 1, #data.current_lines do
- scheduler()
- local lnum_change = current_lnum_change_map[i]
- if
- lnum_change
- and (lnum_change.type == 'remove' or lnum_change.type == 'void')
- then
- virtual_nr_lines[#virtual_nr_lines + 1] = string.rep(
- render_store.get('preview').symbols.void,
- 6
- )
- hls[#hls + 1] = common_hl
- else
- virtual_nr_lines[#virtual_nr_lines + 1] = string.format(
- '%s',
- line_nr_count
- )
- hls[#hls + 1] = common_hl
- if lnum_change and lnum_change.type == 'add' then
- hls[#hls] = render_store.get('sign').hls.add
- elseif lnum_change and lnum_change.type == 'remove' then
- hls[#hls] = render_store.get('sign').hls.remove
- end
- line_nr_count = line_nr_count + 1
- end
- end
- scheduler()
- current_component:set_virtual_line_nr_lines(virtual_nr_lines, hls)
- scheduler()
- for i = 1, #data.current_lines do
- scheduler()
- local lnum_change = current_lnum_change_map[i]
- if lnum_change then
- local type, lnum = lnum_change.type, lnum_change.lnum
- local defined_sign = render_store.get('preview').sign.hls[type]
- if defined_sign then
- scheduler()
- sign.place(
- current_component:get_virtual_line_nr_buf(),
- lnum,
- defined_sign,
- render_store.get('preview').sign.priority
- )
- scheduler()
- end
- end
- end
- hls = {}
- virtual_nr_lines = {}
- line_nr_count = 1
- for i = 1, #data.previous_lines do
- scheduler()
- local lnum_change = previous_lnum_change_map[i]
- if
- lnum_change
- and (lnum_change.type == 'add' or lnum_change.type == 'void')
- then
- virtual_nr_lines[#virtual_nr_lines + 1] = string.rep(
- render_store.get('preview').symbols.void,
- 6
- )
- hls[#hls + 1] = common_hl
- else
- virtual_nr_lines[#virtual_nr_lines + 1] = string.format(
- '%s',
- line_nr_count
- )
- hls[#hls + 1] = common_hl
- if lnum_change and lnum_change.type == 'add' then
- hls[#hls] = render_store.get('sign').hls.add
- elseif lnum_change and lnum_change.type == 'remove' then
- hls[#hls] = render_store.get('sign').hls.remove
- end
- line_nr_count = line_nr_count + 1
- end
- end
- scheduler()
- previous_component:set_virtual_line_nr_lines(virtual_nr_lines, hls)
- scheduler()
- for i = 1, #data.current_lines do
- scheduler()
- local lnum_change = previous_lnum_change_map[i]
- if lnum_change then
- local type, lnum = lnum_change.type, lnum_change.lnum
- local defined_sign = render_store.get('preview').sign.hls[type]
- if defined_sign then
- scheduler()
- sign.place(
- previous_component:get_virtual_line_nr_buf(),
- lnum,
- defined_sign,
- render_store.get('preview').sign.priority
- )
- scheduler()
- end
- end
- end
- end
-end
-
-function Preview:set_mounted(value)
- assert(type(value) == 'boolean', 'type error :: expected boolean')
- self.state.mounted = value
-end
-
-function Preview:set_loading(value, force)
- assert(type(value) == 'boolean', 'type error :: expected boolean')
- if not self:is_mounted() then
- return self
- end
- for _, component in pairs(self.components) do
- component:set_loading(value, force)
- end
- return self
-end
-
-function Preview:set_centered_text(text, force)
- assert(type(text) == 'string', 'type error :: expected string')
- for _, component in pairs(self.components) do
- component:set_centered_text(text, force)
- end
- return self
-end
-
-function Preview:set_error(value, force)
- assert(type(value) == 'boolean', 'type error :: expected boolean')
- for _, component in pairs(self.components) do
- component:set_error(value, force)
- end
- return self
-end
-
-function Preview:get_components()
- return self.components
-end
-
-function Preview:get_parent_buf()
- return self.parent_buf
-end
-
-function Preview:get_parent_win()
- return self.parent_win
-end
-
-function Preview:get_win_ids()
- local win_ids = {}
- for _, component in pairs(self.components) do
- win_ids[#win_ids + 1] = component:get_win_id()
- end
- return win_ids
-end
-
-function Preview:get_bufs()
- local bufs = {}
- for _, component in pairs(self.components) do
- bufs[#bufs + 1] = component:get_buf()
- end
- return bufs
-end
-
-function Preview:keep_focused()
- local win_ids = self:get_win_ids()
- if #win_ids > 1 then
- local current_win_id = vim.api.nvim_get_current_win()
- if not vim.tbl_contains(win_ids, current_win_id) then
- if vim.tbl_isempty(self.state.win_toggle_queue) then
- self.state.win_toggle_queue = self:get_win_ids()
- end
- vim.api.nvim_set_current_win(table.remove(self.state.win_toggle_queue))
- else
- self.state.win_toggle_queue = self:get_win_ids()
- end
- end
-end
-
-function Preview:is_mounted()
- for _, component in pairs(self.components) do
- local win_ids = component:get_win_ids()
- local bufs = component:get_bufs()
- for i = 1, #win_ids do
- if not vim.api.nvim_win_is_valid(win_ids[i]) then
- return false
- end
- end
- for i = 1, #bufs do
- if not buffer.is_valid(bufs[i]) then
- return false
- end
- end
- end
- return self.state.mounted
-end
-
-function Preview:is_temporary()
- return self.temporary
-end
-
-function Preview:clear()
- for _, component in pairs(self.components) do
- component:clear()
- end
-end
-
-function Preview:mount()
- if self:is_mounted() then
- return self
- end
- for _, component in pairs(self.components) do
- component:mount()
- end
- local win_ids = {}
- for _, component in pairs(self.components) do
- win_ids[#win_ids + 1] = component:get_win_id()
- if component:is(CodeComponent) then
- win_ids[#win_ids + 1] = component:get_virtual_line_nr_win_id()
- end
- end
- for _, component in pairs(self.components) do
- component:on(
- 'BufWinLeave',
- string.format(
- ':lua require("vgit").renderer.hide_windows(%s)',
- vim.inspect(win_ids)
- ),
- { once = true }
- )
- end
- local bufs = buffer.list()
- scheduler()
- for i = 1, #bufs do
- local buf = bufs[i]
- local is_buf_listed = buffer.get_option(buf, 'buflisted') == true
- scheduler()
- if is_buf_listed and buffer.is_valid(buf) then
- local event = self.temporary and 'BufEnter' or 'BufWinEnter'
- autocmd.buf.on(
- buf,
- event,
- string.format(
- ':lua _G.package.loaded.vgit.renderer.hide_windows(%s)',
- vim.inspect(win_ids)
- ),
- { once = true }
- )
- end
- end
- self:set_mounted(true)
- return self
-end
-
-function Preview:unmount()
- local components = self:get_components()
- for _, component in pairs(components) do
- component:unmount()
- end
- self:set_mounted(false)
-end
-
-function Preview:render()
- error('Preview must implement render method')
-end
-
-return Preview
diff --git a/lua/vgit/assertion.lua b/lua/vgit/assertion.lua
deleted file mode 100644
index 6fe499d8..00000000
--- a/lua/vgit/assertion.lua
+++ /dev/null
@@ -1,9 +0,0 @@
-local M = {}
-
-M.assert = function(cond, msg)
- if not cond then
- error(debug.traceback(msg))
- end
-end
-
-return M
diff --git a/lua/vgit/autocmd.lua b/lua/vgit/autocmd.lua
deleted file mode 100644
index 1b81bc3b..00000000
--- a/lua/vgit/autocmd.lua
+++ /dev/null
@@ -1,58 +0,0 @@
-local M = {
- buf = {},
- namespace = 'VGit',
-}
-
-M.setup = function()
- vim.cmd(string.format('aug %s | autocmd! | aug END', M.namespace))
-end
-
-M.off = function()
- vim.cmd(string.format('aug %s | autocmd! | aug END', M.namespace))
-end
-
-M.on = function(cmd, handler, options)
- local once = (options and options.once) or false
- local override = (options and options.override) or true
- local nested = (options and options.nested) or false
- vim.api.nvim_exec(
- string.format(
- 'au%s %s %s * %s %s %s',
- override and '!' or '',
- M.namespace,
- cmd,
- nested and '++nested' or '',
- once and '++once' or '',
- handler
- ),
- false
- )
-end
-
-M.buf.on = function(buf, cmd, handler, options)
- local once = (options and options.once) or false
- local override = (options and options.override) or true
- local nested = (options and options.nested) or false
- vim.api.nvim_exec(
- string.format(
- 'au%s %s %s %s %s %s',
- override and '!' or '',
- M.namespace,
- cmd,
- buf,
- nested and '++nested' or '',
- once and '++once' or '',
- handler
- ),
- false
- )
-end
-
-M.buf.off = function(buf, cmd)
- vim.api.nvim_exec(
- string.format('au! %s %s ++once ', M.namespace, cmd, buf),
- false
- )
-end
-
-return M
diff --git a/lua/vgit/buffer.lua b/lua/vgit/buffer.lua
deleted file mode 100644
index e060ef79..00000000
--- a/lua/vgit/buffer.lua
+++ /dev/null
@@ -1,71 +0,0 @@
-local buffer_store = require('vgit.stores.buffer_store')
-
-local M = {}
-
-M.store = buffer_store
-
-M.current = function()
- return vim.api.nvim_get_current_buf()
-end
-
-M.add_keymap = function(buf, key, action)
- vim.api.nvim_buf_set_keymap(
- buf,
- 'n',
- key,
- string.format(':lua require("vgit").%s', action),
- {
- silent = true,
- noremap = true,
- }
- )
-end
-
-M.remove_keymap = function(buf, key)
- vim.api.nvim_buf_del_keymap(buf, 'n', key)
-end
-
-M.get_lines = function(buf, start, finish)
- start = start or 0
- finish = finish or -1
- return vim.api.nvim_buf_get_lines(buf, start, finish, false)
-end
-
-M.set_lines = function(buf, lines, start, finish)
- start = start or 0
- finish = finish or -1
- local modifiable = vim.api.nvim_buf_get_option(buf, 'modifiable')
- if modifiable then
- vim.api.nvim_buf_set_lines(buf, start, finish, false, lines)
- return
- end
- vim.api.nvim_buf_set_option(buf, 'modifiable', true)
- vim.api.nvim_buf_set_lines(buf, start, finish, false, lines)
- vim.api.nvim_buf_set_option(buf, 'modifiable', false)
-end
-
-M.set_option = function(buf, key, value)
- vim.api.nvim_buf_set_option(buf, key, value)
-end
-
-M.get_option = function(buf, key)
- return vim.api.nvim_buf_get_option(buf, key)
-end
-
-M.assign_options = function(buf, options)
- for key, value in pairs(options) do
- vim.api.nvim_buf_set_option(buf, key, value)
- end
-end
-
-M.is_being_edited = function(buf)
- return M.get_option(buf, 'modified')
-end
-
-M.is_valid = function(buf)
- return vim.api.nvim_buf_is_valid(buf) and vim.api.nvim_buf_is_loaded(buf)
-end
-
-M.list = vim.api.nvim_list_bufs
-
-return M
diff --git a/lua/vgit/cli/Git.lua b/lua/vgit/cli/Git.lua
new file mode 100644
index 00000000..9345ed69
--- /dev/null
+++ b/lua/vgit/cli/Git.lua
@@ -0,0 +1,830 @@
+local utils = require('vgit.core.utils')
+local Job = require('vgit.core.Job')
+local loop = require('vgit.core.loop')
+local Object = require('vgit.core.Object')
+local Hunk = require('vgit.cli.models.Hunk')
+local Log = require('vgit.cli.models.Log')
+local Status = require('vgit.cli.models.Status')
+local Blame = require('vgit.cli.models.Blame')
+
+local Git = Object:extend()
+
+function Git:new(cwd)
+ return setmetatable({
+ cwd = cwd or '',
+ diff_algorithm = 'myers',
+ empty_tree_hash = '4b825dc642cb6eb9a060e54bf8d69288fbee4904',
+ cache = {
+ config = nil,
+ },
+ }, Git)
+end
+
+function Git:set_cwd(cwd)
+ self.cwd = cwd
+end
+
+Git.is_commit_valid = loop.promisify(function(self, commit, callback)
+ local result = {}
+ local err = {}
+ local job = Job:new({
+ command = 'git',
+ args = {
+ '-C',
+ self.cwd,
+ 'show',
+ '--abbrev-commit',
+ '--oneline',
+ '--no-notes',
+ '--no-patch',
+ '--no-color',
+ commit,
+ },
+ on_stdout = function(line)
+ result[#result + 1] = line
+ end,
+ on_stderr = function(line)
+ err[#err + 1] = line
+ end,
+ on_exit = function()
+ if #err ~= 0 then
+ return callback(false)
+ end
+ if #result == 0 then
+ return callback(false)
+ end
+ callback(true)
+ end,
+ })
+ job:start()
+end, 3)
+
+Git.config = loop.promisify(function(self, callback)
+ if self.cache.config then
+ return callback(nil, self.cache.config)
+ end
+ local err = {}
+ local result = {}
+ local job = Job:new({
+ command = 'git',
+ args = {
+ '-C',
+ self.cwd,
+ 'config',
+ '--list',
+ },
+ on_stdout = function(line)
+ local line_chunks = vim.split(line, '=')
+ result[line_chunks[1]] = line_chunks[2]
+ end,
+ on_stderr = function(line)
+ err[#err + 1] = line
+ end,
+ on_exit = function()
+ if #err ~= 0 then
+ return callback(err, nil)
+ end
+ self.cache.config = result
+ callback(nil, result)
+ end,
+ })
+ job:start()
+end, 2)
+
+Git.has_commits = loop.promisify(function(self, callback)
+ local result = true
+ local job = Job:new({
+ command = 'git',
+ args = {
+ '-C',
+ self.cwd,
+ 'status',
+ },
+ on_stdout = function(line)
+ if line == 'No commits yet' then
+ result = false
+ end
+ end,
+ on_exit = function()
+ callback(result)
+ end,
+ })
+ job:start()
+end, 2)
+
+Git.is_inside_git_dir = loop.promisify(function(self, callback)
+ local err = {}
+ local job = Job:new({
+ command = 'git',
+ args = {
+ '-C',
+ self.cwd,
+ 'rev-parse',
+ '--is-inside-git-dir',
+ },
+ on_stderr = function(line)
+ err[#err + 1] = line
+ end,
+ on_exit = function()
+ if #err ~= 0 then
+ return callback(false)
+ end
+ callback(true)
+ end,
+ })
+ job:start()
+end, 2)
+
+Git.blames = loop.promisify(function(self, filename, callback)
+ local err = {}
+ local result = {}
+ local blame_info = {}
+ local job = Job:new({
+ command = 'git',
+ args = {
+ '-C',
+ self.cwd,
+ 'blame',
+ '--line-porcelain',
+ '--',
+ filename,
+ },
+ on_stdout = function(line)
+ if string.byte(line:sub(1, 3)) ~= 9 then
+ table.insert(blame_info, line)
+ else
+ local blame = Blame:new(blame_info)
+ if blame then
+ result[#result + 1] = blame
+ end
+ blame_info = {}
+ end
+ end,
+ on_stderr = function(line)
+ err[#err + 1] = line
+ end,
+ on_exit = function()
+ if #err ~= 0 then
+ return callback(err, nil)
+ end
+ callback(nil, result)
+ end,
+ })
+ job:start()
+end, 3)
+
+Git.blame_line = loop.promisify(function(self, filename, lnum, callback)
+ filename = utils.strip_substring(filename, self.cwd)
+ local err = {}
+ local result = {}
+ local job = Job:new({
+ command = 'git',
+ args = {
+ '-C',
+ self.cwd,
+ 'blame',
+ '-L',
+ string.format('%s,+1', lnum),
+ '--line-porcelain',
+ '--',
+ filename,
+ },
+ on_stdout = function(line)
+ result[#result + 1] = line
+ end,
+ on_stderr = function(line)
+ err[#err + 1] = line
+ end,
+ on_exit = function()
+ if #err ~= 0 then
+ return callback(err, nil)
+ end
+ callback(nil, Blame:new(result))
+ end,
+ })
+ job:start()
+end, 4)
+
+Git.logs = loop.promisify(function(self, filename, callback)
+ local err = {}
+ local logs = {}
+ local revision_count = 0
+ local job = Job:new({
+ command = 'git',
+ args = {
+ '-C',
+ self.cwd,
+ 'log',
+ '--color=never',
+ '--pretty=format:"%H-%P-%at-%an-%ae-%s"',
+ '--',
+ filename,
+ },
+ on_stdout = function(line)
+ revision_count = revision_count + 1
+ local log = Log:new(line, revision_count)
+ if log then
+ logs[#logs + 1] = log
+ end
+ end,
+ on_stderr = function(line)
+ err[#err + 1] = line
+ end,
+ on_exit = function()
+ if #err ~= 0 then
+ return callback(err, nil)
+ end
+ return callback(nil, logs)
+ end,
+ })
+ job:start()
+end, 3)
+
+Git.file_hunks = loop.promisify(function(self, filename_a, filename_b, callback)
+ local result = {}
+ local err = {}
+ local args = {
+ '-C',
+ self.cwd,
+ '--no-pager',
+ '-c',
+ 'core.safecrlf=false',
+ 'diff',
+ '--color=never',
+ string.format('--diff-algorithm=%s', self.diff_algorithm),
+ '--patch-with-raw',
+ '--unified=0',
+ '--no-index',
+ filename_a,
+ filename_b,
+ }
+ local job = Job:new({
+ command = 'git',
+ args = args,
+ on_stdout = function(line)
+ result[#result + 1] = line
+ end,
+ on_stderr = function(line)
+ err[#err + 1] = line
+ end,
+ on_exit = function()
+ if #err ~= 0 then
+ return callback(err, nil)
+ end
+ local hunks = {}
+ for i = 1, #result do
+ local line = result[i]
+ if vim.startswith(line, '@@') then
+ hunks[#hunks + 1] = Hunk:new(line)
+ else
+ if #hunks > 0 then
+ local hunk = hunks[#hunks]
+ hunk:append_diff_line(line)
+ end
+ end
+ end
+ return callback(nil, hunks)
+ end,
+ })
+ job:start()
+end, 4)
+
+Git.index_hunks = loop.promisify(function(self, filename, callback)
+ local result = {}
+ local err = {}
+ local args = {
+ '-C',
+ self.cwd,
+ '--no-pager',
+ '-c',
+ 'core.safecrlf=false',
+ 'diff',
+ '--color=never',
+ string.format('--diff-algorithm=%s', self.diff_algorithm),
+ '--patch-with-raw',
+ '--unified=0',
+ '--',
+ filename,
+ }
+ local job = Job:new({
+ command = 'git',
+ args = args,
+ on_stdout = function(line)
+ result[#result + 1] = line
+ end,
+ on_stderr = function(line)
+ err[#err + 1] = line
+ end,
+ on_exit = function()
+ if #err ~= 0 then
+ return callback(err, nil)
+ end
+ local hunks = {}
+ for i = 1, #result do
+ local line = result[i]
+ if vim.startswith(line, '@@') then
+ hunks[#hunks + 1] = Hunk:new(line)
+ else
+ if #hunks > 0 then
+ local hunk = hunks[#hunks]
+ hunk:append_diff_line(line)
+ end
+ end
+ end
+ return callback(nil, hunks)
+ end,
+ })
+ job:start()
+end, 3)
+
+Git.remote_hunks = loop.promisify(
+ function(self, filename, parent_hash, commit_hash, callback)
+ local result = {}
+ local err = {}
+ local args = {
+ '-C',
+ self.cwd,
+ '--no-pager',
+ '-c',
+ 'core.safecrlf=false',
+ 'diff',
+ '--color=never',
+ string.format('--diff-algorithm=%s', self.diff_algorithm),
+ '--patch-with-raw',
+ '--unified=0',
+ }
+ if parent_hash and commit_hash then
+ utils.list_concat(args, {
+ #parent_hash > 0 and parent_hash or self.empty_tree_hash,
+ commit_hash,
+ '--',
+ filename,
+ })
+ elseif parent_hash and not commit_hash then
+ utils.list_concat(args, {
+ parent_hash,
+ '--',
+ filename,
+ })
+ else
+ utils.list_concat(args, {
+ '--',
+ filename,
+ })
+ end
+ local job = Job:new({
+ command = 'git',
+ args = args,
+ on_stdout = function(line)
+ result[#result + 1] = line
+ end,
+ on_stderr = function(line)
+ err[#err + 1] = line
+ end,
+ on_exit = function()
+ if #err ~= 0 then
+ return callback(err, nil)
+ end
+ local hunks = {}
+ for i = 1, #result do
+ local line = result[i]
+ if vim.startswith(line, '@@') then
+ hunks[#hunks + 1] = Hunk:new(line)
+ else
+ if #hunks > 0 then
+ local hunk = hunks[#hunks]
+ hunk:append_diff_line(line)
+ end
+ end
+ end
+ return callback(nil, hunks)
+ end,
+ })
+ job:start()
+ end,
+ 5
+)
+
+Git.staged_hunks = loop.promisify(function(self, filename, callback)
+ local result = {}
+ local err = {}
+ local args = {
+ '-C',
+ self.cwd,
+ '--no-pager',
+ '-c',
+ 'core.safecrlf=false',
+ 'diff',
+ '--color=never',
+ string.format('--diff-algorithm=%s', self.diff_algorithm),
+ '--patch-with-raw',
+ '--unified=0',
+ '--cached',
+ '--',
+ filename,
+ }
+ local job = Job:new({
+ command = 'git',
+ args = args,
+ on_stdout = function(line)
+ result[#result + 1] = line
+ end,
+ on_stderr = function(line)
+ err[#err + 1] = line
+ end,
+ on_exit = function()
+ if #err ~= 0 then
+ return callback(err, nil)
+ end
+ local hunks = {}
+ for i = 1, #result do
+ local line = result[i]
+ if vim.startswith(line, '@@') then
+ hunks[#hunks + 1] = Hunk:new(line)
+ else
+ if #hunks > 0 then
+ local hunk = hunks[#hunks]
+ hunk:append_diff_line(line)
+ end
+ end
+ end
+ return callback(nil, hunks)
+ end,
+ })
+ job:start()
+end, 3)
+
+function Git:untracked_hunks(lines)
+ local diff = {}
+ for i = 1, #lines do
+ diff[#diff + 1] = string.format('+%s', lines[i])
+ end
+ local hunk = Hunk:new()
+ hunk.start = 1
+ hunk.finish = #lines
+ hunk.type = 'add'
+ hunk.diff = diff
+ hunk.stat = {
+ added = #lines,
+ removed = 0,
+ }
+ return { hunk }
+end
+
+function Git:deleted_hunks(lines)
+ local diff = {}
+ for i = 1, #lines do
+ diff[#diff + 1] = string.format('+%s', lines[i])
+ end
+ local hunk = Hunk:new()
+ hunk.start = 1
+ hunk.finish = #lines
+ hunk.type = 'remove'
+ hunk.diff = diff
+ hunk.stat = {
+ added = 0,
+ removed = #lines,
+ }
+ return { hunk }
+end
+
+Git.show = loop.promisify(
+ function(self, tracked_filename, commit_hash, callback)
+ local err = {}
+ local result = {}
+ commit_hash = commit_hash or ''
+ local job = Job:new({
+ command = 'git',
+ args = {
+ '-C',
+ self.cwd,
+ 'show',
+ -- git will attach self.cwd to the command which means we are going to search
+ -- from the current relative path "./" basically just means "${self.cwd}/".
+ string.format('%s:./%s', commit_hash, tracked_filename),
+ },
+ on_stdout = function(line)
+ result[#result + 1] = line
+ end,
+ on_stderr = function(line)
+ err[#err + 1] = line
+ end,
+ on_exit = function()
+ if #err ~= 0 then
+ return callback(err, nil)
+ end
+ callback(nil, result)
+ end,
+ })
+ job:start()
+ end,
+ 4
+)
+
+Git.is_in_remote = loop.promisify(function(self, tracked_filename, callback)
+ local err = false
+ local job = Job:new({
+ command = 'git',
+ args = {
+ '-C',
+ self.cwd,
+ 'show',
+ -- git will attach self.cwd to the command which means we are going to search
+ -- from the current relative path "./" basically just means "${self.cwd}/".
+ string.format('HEAD:./%s', tracked_filename),
+ },
+ on_stderr = function(line)
+ if line then
+ err = true
+ end
+ end,
+ on_exit = function()
+ callback(not err)
+ end,
+ })
+ job:start()
+end, 3)
+
+Git.stage = loop.promisify(function(self, callback)
+ local err = {}
+ local job = Job:new({
+ command = 'git',
+ args = {
+ '-C',
+ self.cwd,
+ 'add',
+ '.',
+ },
+ on_stderr = function(line)
+ err[#err + 1] = line
+ end,
+ on_exit = function()
+ if #err ~= 0 then
+ return callback(err)
+ end
+ callback(nil)
+ end,
+ })
+ job:start()
+end, 2)
+
+Git.unstage = loop.promisify(function(self, callback)
+ local err = {}
+ local job = Job:new({
+ command = 'git',
+ args = {
+ '-C',
+ self.cwd,
+ 'reset',
+ '-q',
+ 'HEAD',
+ '.',
+ },
+ on_stderr = function(line)
+ err[#err + 1] = line
+ end,
+ on_exit = function()
+ if #err ~= 0 then
+ return callback(err)
+ end
+ callback(nil)
+ end,
+ })
+ job:start()
+end, 2)
+
+Git.stage_file = loop.promisify(function(self, filename, callback)
+ local err = {}
+ local job = Job:new({
+ command = 'git',
+ args = {
+ '-C',
+ self.cwd,
+ 'add',
+ filename,
+ },
+ on_stderr = function(line)
+ err[#err + 1] = line
+ end,
+ on_exit = function()
+ if #err ~= 0 then
+ return callback(err)
+ end
+ callback(nil)
+ end,
+ })
+ job:start()
+end, 3)
+
+Git.unstage_file = loop.promisify(function(self, filename, callback)
+ local err = {}
+ local job = Job:new({
+ command = 'git',
+ args = {
+ '-C',
+ self.cwd,
+ 'reset',
+ '-q',
+ 'HEAD',
+ '--',
+ filename,
+ },
+ on_stderr = function(line)
+ err[#err + 1] = line
+ end,
+ on_exit = function()
+ if #err ~= 0 then
+ return callback(err)
+ end
+ callback(nil)
+ end,
+ })
+ job:start()
+end, 3)
+
+Git.stage_hunk_from_patch = loop.promisify(
+ function(self, patch_filename, callback)
+ local err = {}
+ local job = Job:new({
+ command = 'git',
+ args = {
+ '-C',
+ self.cwd,
+ 'apply',
+ '--cached',
+ '--whitespace=nowarn',
+ '--unidiff-zero',
+ patch_filename,
+ },
+ on_stderr = function(line)
+ err[#err + 1] = line
+ end,
+ on_exit = function()
+ if #err ~= 0 then
+ return callback(err)
+ end
+ callback(nil)
+ end,
+ })
+ job:start()
+ end,
+ 3
+)
+
+Git.is_ignored = loop.promisify(function(self, filename, callback)
+ filename = utils.strip_substring(filename, self.cwd)
+ local err = {}
+ local job = Job:new({
+ command = 'git',
+ args = {
+ '-C',
+ self.cwd,
+ 'check-ignore',
+ filename,
+ },
+ on_stdout = function(line)
+ err[#err + 1] = line
+ end,
+ on_stderr = function(line)
+ err[#err + 1] = line
+ end,
+ on_exit = function()
+ if #err ~= 0 then
+ return callback(true)
+ end
+ callback(false)
+ end,
+ })
+ job:start()
+end, 3)
+
+Git.reset = loop.promisify(function(self, filename, callback)
+ local err = {}
+ local job = Job:new({
+ command = 'git',
+ args = {
+ '-C',
+ self.cwd,
+ 'checkout',
+ '-q',
+ '--',
+ filename,
+ },
+ on_stderr = function(line)
+ err[#err + 1] = line
+ end,
+ on_exit = function()
+ if #err ~= 0 then
+ return callback(err)
+ end
+ callback(nil)
+ end,
+ })
+ job:start()
+end, 3)
+
+Git.current_branch = loop.promisify(function(self, callback)
+ local err = {}
+ local result = {}
+ local job = Job:new({
+ command = 'git',
+ args = {
+ '-C',
+ self.cwd,
+ 'branch',
+ '--show-current',
+ },
+ on_stdout = function(line)
+ result[#result + 1] = line
+ end,
+ on_stderr = function(line)
+ err[#err + 1] = line
+ end,
+ on_exit = function()
+ if #err ~= 0 then
+ return callback(err, result)
+ end
+ callback(nil, result)
+ end,
+ })
+ job:start()
+end, 2)
+
+Git.tracked_filename = loop.promisify(function(self, filename, callback)
+ filename = utils.strip_substring(filename, self.cwd)
+ local result = {}
+ local job = Job:new({
+ command = 'git',
+ args = {
+ '-C',
+ self.cwd,
+ 'ls-files',
+ '--exclude-standard',
+ filename,
+ },
+ on_stdout = function(line)
+ result[#result + 1] = line
+ end,
+ on_exit = function()
+ callback(result[1])
+ end,
+ })
+ job:start()
+end, 3)
+
+Git.tracked_full_filename = loop.promisify(function(self, filename, callback)
+ filename = utils.strip_substring(filename, self.cwd)
+ local result = {}
+ local job = Job:new({
+ command = 'git',
+ args = {
+ '-C',
+ self.cwd,
+ 'ls-files',
+ '--exclude-standard',
+ '--full-name',
+ filename,
+ },
+ on_stdout = function(line)
+ result[#result + 1] = line
+ end,
+ on_exit = function()
+ callback(result[1])
+ end,
+ })
+ job:start()
+end, 3)
+
+Git.ls_changed = loop.promisify(function(self, callback)
+ local err = {}
+ local result = {}
+ local job = Job:new({
+ command = 'git',
+ args = {
+ '-C',
+ self.cwd,
+ 'status',
+ '-u',
+ '-s',
+ '--no-renames',
+ },
+ on_stdout = function(line)
+ result[#result + 1] = {
+ filename = line:sub(4, #line),
+ status = Status:new(line:sub(1, 2)),
+ }
+ end,
+ on_stderr = function(line)
+ err[#err + 1] = line
+ end,
+ on_exit = function()
+ if #err ~= 0 then
+ return callback(err, result)
+ end
+ callback(nil, result)
+ end,
+ })
+ job:start()
+end, 2)
+
+return Git
diff --git a/lua/vgit/cli/models/Blame.lua b/lua/vgit/cli/models/Blame.lua
new file mode 100644
index 00000000..0a9a91e2
--- /dev/null
+++ b/lua/vgit/cli/models/Blame.lua
@@ -0,0 +1,65 @@
+local Object = require('vgit.core.Object')
+
+local Blame = Object:extend()
+
+local function split_by_whitespace(str)
+ return vim.split(str, ' ')
+end
+
+function Blame:new(info)
+ if not info then
+ return setmetatable({}, Blame)
+ end
+ -- TODO this is badly done crashes randomly please fix this parsing.
+ local commit_hash_info = split_by_whitespace(info[1])
+ local author_mail_info = split_by_whitespace(info[3])
+ local author_time_info = split_by_whitespace(info[4])
+ local author_tz_info = split_by_whitespace(info[5])
+ local committer_mail_info = split_by_whitespace(info[7])
+ local committer_time_info = split_by_whitespace(info[8])
+ local committer_tz_info = split_by_whitespace(info[9])
+ local parent_hash_info = split_by_whitespace(info[11])
+ local author = info[2]:sub(8, #info[2])
+ local author_mail = author_mail_info[2]
+ local committer = info[6]:sub(11, #info[6])
+ local committer_mail = committer_mail_info[2]
+ local lnum = tonumber(commit_hash_info[3] or '1')
+ local committed = true
+ if
+ author == 'Not Committed Yet'
+ and committer == 'Not Committed Yet'
+ and author_mail == ''
+ and committer_mail == ''
+ then
+ committed = false
+ end
+ return setmetatable({
+ lnum = lnum,
+ commit_hash = commit_hash_info[1],
+ parent_hash = parent_hash_info[2],
+ author = author,
+ author_mail = (function()
+ local mail = author_mail
+ if mail:sub(1, 1) == '<' and mail:sub(#mail, #mail) then
+ mail = mail:sub(2, #mail - 1)
+ end
+ return mail
+ end)(),
+ author_time = tonumber(author_time_info[2]),
+ author_tz = author_tz_info[2],
+ committer = committer,
+ committer_mail = (function()
+ local mail = committer_mail
+ if mail:sub(1, 1) == '<' and mail:sub(#mail, #mail) then
+ mail = mail:sub(2, #mail - 1)
+ end
+ return mail
+ end)(),
+ committer_time = tonumber(committer_time_info[2]),
+ committer_tz = committer_tz_info[2],
+ commit_message = info[10]:sub(9, #info[10]),
+ committed = committed,
+ }, Blame)
+end
+
+return Blame
diff --git a/lua/vgit/Hunk.lua b/lua/vgit/cli/models/Hunk.lua
similarity index 80%
rename from lua/vgit/Hunk.lua
rename to lua/vgit/cli/models/Hunk.lua
index 220d9863..dc240e30 100644
--- a/lua/vgit/Hunk.lua
+++ b/lua/vgit/cli/models/Hunk.lua
@@ -1,4 +1,4 @@
-local Object = require('plenary.class')
+local Object = require('vgit.core.Object')
local Hunk = Object:extend()
@@ -38,6 +38,18 @@ function Hunk:parse_diff(diff)
return removed_lines, added_lines
end
+function Hunk:append_diff_line(line)
+ local stat = self.stat
+ local type = line:sub(1, 1)
+ if type == '+' then
+ stat.added = stat.added + 1
+ elseif type == '-' then
+ stat.removed = stat.removed + 1
+ end
+ local diff = self.diff
+ diff[#diff + 1] = line
+end
+
function Hunk:new(header)
if not header then
return setmetatable({
@@ -46,6 +58,10 @@ function Hunk:new(header)
finish = nil,
type = nil,
diff = {},
+ stat = {
+ added = 0,
+ removed = 0,
+ },
}, Hunk)
end
local previous, current
@@ -60,6 +76,10 @@ function Hunk:new(header)
finish = current[1] + current[2] - 1,
type = nil,
diff = {},
+ stat = {
+ added = 0,
+ removed = 0,
+ },
}
if current[2] == 0 then
hunk.finish = hunk.start
diff --git a/lua/vgit/cli/models/Log.lua b/lua/vgit/cli/models/Log.lua
new file mode 100644
index 00000000..f5b33a84
--- /dev/null
+++ b/lua/vgit/cli/models/Log.lua
@@ -0,0 +1,23 @@
+local Object = require('vgit.core.Object')
+
+local Log = Object:extend()
+
+function Log:new(line, revision_count)
+ local log = vim.split(line, '-')
+ -- Sometimes you can have multiple parents, in that instance we pick the first!
+ local parents = vim.split(log[2], ' ')
+ if #parents > 1 then
+ log[2] = parents[1]
+ end
+ return setmetatable({
+ revision = string.format('HEAD~%s', revision_count),
+ commit_hash = log[1]:sub(2, #log[1]),
+ parent_hash = log[2],
+ timestamp = log[3],
+ author_name = log[4],
+ author_email = log[5],
+ summary = log[6]:sub(1, #log[6] - 1),
+ }, Log)
+end
+
+return Log
diff --git a/lua/vgit/Patch.lua b/lua/vgit/cli/models/Patch.lua
similarity index 93%
rename from lua/vgit/Patch.lua
rename to lua/vgit/cli/models/Patch.lua
index e8f84417..6b51a3bf 100644
--- a/lua/vgit/Patch.lua
+++ b/lua/vgit/cli/models/Patch.lua
@@ -1,4 +1,4 @@
-local Object = require('plenary.class')
+local Object = require('vgit.core.Object')
local Patch = Object:extend()
diff --git a/lua/vgit/cli/models/Status.lua b/lua/vgit/cli/models/Status.lua
new file mode 100644
index 00000000..d389f47e
--- /dev/null
+++ b/lua/vgit/cli/models/Status.lua
@@ -0,0 +1,43 @@
+local Object = require('vgit.core.Object')
+
+local Status = Object:extend()
+
+function Status:new(value)
+ local first, second = Status:parse(value)
+ return setmetatable({
+ value = value,
+ first = first,
+ second = second,
+ }, Status)
+end
+
+function Status:parse(status)
+ return status:sub(1, 1), status:sub(2, 2)
+end
+
+function Status:has(status)
+ local first, second = self:parse(status)
+ if self.first ~= ' ' then
+ return self.first == first
+ end
+ if self.second ~= ' ' then
+ return self.second == second
+ end
+ return self.value == status
+end
+
+function Status:has_either(status)
+ local first, second = self:parse(status)
+ return first == self.first or second == self.second
+end
+
+function Status:has_both(status)
+ local first, second = self:parse(status)
+ return first == self.first and second == self.second
+end
+
+function Status:to_string()
+ return self.value
+end
+
+return Status
diff --git a/lua/vgit/components/CodeComponent.lua b/lua/vgit/components/CodeComponent.lua
deleted file mode 100644
index d56e3918..00000000
--- a/lua/vgit/components/CodeComponent.lua
+++ /dev/null
@@ -1,242 +0,0 @@
-local Component = require('vgit.Component')
-local Interface = require('vgit.Interface')
-local icons = require('vgit.icons')
-local buffer = require('vgit.buffer')
-local VirtualLineNrDecorator = require('vgit.decorators.VirtualLineNrDecorator')
-local AppBarDecorator = require('vgit.decorators.AppBarDecorator')
-local render_store = require('vgit.stores.render_store')
-
-local CodeComponent = Component:extend()
-
-function CodeComponent:new(options)
- assert(
- options == nil or type(options) == 'table',
- 'type error :: expected table or nil'
- )
- options = options or {}
- local height = self:get_min_height()
- local width = self:get_min_width()
- return setmetatable({
- anim_id = nil,
- timer_id = nil,
- state = {
- buf = nil,
- win_id = nil,
- ns_id = nil,
- virtual_line_nr = nil,
- loading = false,
- error = false,
- mounted = false,
- cache = {
- lines = {},
- cursor = nil,
- },
- paint_count = 0,
- },
- config = Interface
- :new({
- filetype = '',
- header = {
- enabled = true,
- },
- border = {
- enabled = false,
- hl = 'FloatBorder',
- chars = { '', '', '', '', '', '', '', '' },
- },
- buf_options = {
- ['modifiable'] = false,
- ['buflisted'] = false,
- ['bufhidden'] = 'wipe',
- },
- win_options = {
- ['wrap'] = false,
- ['number'] = false,
- ['winhl'] = 'Normal:',
- ['cursorline'] = false,
- ['cursorbind'] = false,
- ['scrollbind'] = false,
- ['signcolumn'] = 'auto',
- },
- window_props = {
- style = 'minimal',
- relative = 'editor',
- height = height,
- width = width,
- row = 1,
- col = 0,
- focusable = true,
- zindex = 60,
- },
- virtual_line_nr = {
- enabled = false,
- width = render_store.get('preview').virtual_line_nr_width,
- },
- static = false,
- })
- :assign(options),
- }, CodeComponent)
-end
-
-function CodeComponent:get_header_buf()
- return self:get_header() and self:get_header():get_buf() or nil
-end
-
-function CodeComponent:get_header_win_id()
- return self:get_header() and self:get_header():get_win_id() or nil
-end
-
-function CodeComponent:get_header()
- return self.state.header
-end
-
-function CodeComponent:is_header_enabled()
- return self.config:get('header').enabled
-end
-
-function CodeComponent:set_header(header)
- assert(type(header) == 'table', 'type error :: expected table')
- self.state.header = header
- return self
-end
-
-function CodeComponent:set_title(title, filename, filetype)
- if not self:is_header_enabled() then
- return self
- end
- local icon, icon_hl = icons.file_icon(filename, filetype)
- local header = self:get_header()
- if title == '' then
- header:set_lines({ string.format('%s %s', icon, filename) })
- else
- header:set_lines({ string.format('%s %s %s', title, icon, filename) })
- end
- if icon_hl then
- if title == '' then
- vim.api.nvim_buf_add_highlight(header:get_buf(), -1, icon_hl, 0, 0, #icon)
- else
- vim.api.nvim_buf_add_highlight(
- header:get_buf(),
- -1,
- icon_hl,
- 0,
- #title + 1,
- #title + 1 + #icon
- )
- end
- end
- return self
-end
-
-function CodeComponent:notify(text)
- if not self:is_header_enabled() then
- return self
- end
- local epoch = 2000
- local header = self:get_header()
- if self.timer_id then
- vim.fn.timer_stop(self.timer_id)
- self.timer_id = nil
- end
- header:transpose_text({ text, 'Comment' }, 0, 0, 'eol')
- self.timer_id = vim.fn.timer_start(epoch, function()
- if buffer.is_valid(header:get_buf()) then
- header:clear_ns_id()
- end
- vim.fn.timer_stop(self.timer_id)
- self.timer_id = nil
- end)
- return self
-end
-
-function CodeComponent:mount()
- if self:is_mounted() then
- return self
- end
- local buf_options = self.config:get('buf_options')
- local window_props = self.config:get('window_props')
- local win_options = self.config:get('win_options')
- self:set_buf(vim.api.nvim_create_buf(false, true))
- local buf = self:get_buf()
- buffer.assign_options(buf, buf_options)
- local win_ids = {}
- if self:is_virtual_line_nr_enabled() then
- local virtual_line_nr_config = self.config:get('virtual_line_nr')
- if self:is_header_enabled() then
- self:set_header(AppBarDecorator:new(window_props, buf):mount())
- end
- self:set_virtual_line_nr(
- VirtualLineNrDecorator:new(virtual_line_nr_config, window_props, buf)
- )
- local virtual_line_nr = self:get_virtual_line_nr()
- virtual_line_nr:mount()
- window_props.width = window_props.width - virtual_line_nr_config.width
- window_props.col = window_props.col + virtual_line_nr_config.width
- win_ids[#win_ids + 1] = virtual_line_nr:get_win_id()
- else
- if self:is_header_enabled() then
- self:set_header(AppBarDecorator:new(window_props, buf):mount())
- end
- end
- if self:is_border_enabled() then
- local border_config = self.config:get('border')
- window_props.border = self:make_border(border_config)
- end
- if self:is_header_enabled() then
- -- Correct addition of header decorator parameters.
- window_props.row = window_props.row + 3
- if window_props.height - 3 > 1 then
- window_props.height = window_props.height - 3
- end
- end
- local win_id = vim.api.nvim_open_win(buf, true, window_props)
- for key, value in pairs(win_options) do
- vim.api.nvim_win_set_option(win_id, key, value)
- end
- self:set_win_id(win_id)
- self:set_ns_id(
- vim.api.nvim_create_namespace(
- string.format('tanvirtin/vgit.nvim/%s/%s', buf, win_id)
- )
- )
- if self:is_virtual_line_nr_enabled() then
- local virtual_line_nr_config = self.config:get('virtual_line_nr')
- window_props.width = window_props.width + virtual_line_nr_config.width
- window_props.col = window_props.col - virtual_line_nr_config.width
- end
- win_ids[#win_ids + 1] = win_id
- self:on(
- 'BufWinLeave',
- string.format(':lua require("vgit").renderer.hide_windows(%s)', win_ids)
- )
- self:add_syntax_highlights()
- self:set_mounted(true)
- return self
-end
-
-function CodeComponent:unmount()
- self:set_mounted(false)
- local win_id = self:get_win_id()
- if vim.api.nvim_win_is_valid(win_id) then
- self:clear()
- pcall(vim.api.nvim_win_close, win_id, true)
- end
- if self:has_virtual_line_nr() then
- local virtual_line_nr_win_id = self:get_virtual_line_nr_win_id()
- if
- virtual_line_nr_win_id
- and vim.api.nvim_win_is_valid(virtual_line_nr_win_id)
- then
- pcall(vim.api.nvim_win_close, virtual_line_nr_win_id, true)
- end
- end
- if self.config:get('header').enabled then
- local header_win_id = self:get_header_win_id()
- if vim.api.nvim_win_is_valid(header_win_id) then
- pcall(vim.api.nvim_win_close, header_win_id, true)
- end
- end
- return self
-end
-
-return CodeComponent
diff --git a/lua/vgit/components/TableComponent.lua b/lua/vgit/components/TableComponent.lua
deleted file mode 100644
index 6d5fabe2..00000000
--- a/lua/vgit/components/TableComponent.lua
+++ /dev/null
@@ -1,276 +0,0 @@
-local Component = require('vgit.Component')
-local Interface = require('vgit.Interface')
-local buffer = require('vgit.buffer')
-local AppBarDecorator = require('vgit.decorators.AppBarDecorator')
-
-local function shorten_str(str, limit)
- if #str > limit then
- str = str:sub(1, limit - 3)
- str = str .. '...'
- end
- return str
-end
-
-local function make_paddings(
- rows,
- column_labels,
- column_spacing,
- max_column_len
-)
- local padding = {}
- for i = 1, #rows do
- local items = rows[i]
- assert(
- #column_labels == #items,
- 'number of columns should be the same as number of column_labels'
- )
- for j = 1, #items do
- local value = shorten_str(items[j], max_column_len)
- if padding[j] then
- padding[j] = math.max(padding[j], #value + column_spacing)
- else
- padding[j] = column_spacing + #value + column_spacing
- end
- end
- end
- return padding
-end
-
-local function make_heading(
- paddings,
- column_labels,
- column_spacing,
- max_column_len
-)
- local row = string.format('%s', string.rep(' ', column_spacing))
- for i = 1, #column_labels do
- local label = shorten_str(column_labels[i], max_column_len)
- row = string.format(
- '%s%s%s',
- row,
- label,
- string.rep(' ', paddings[i] - #label)
- )
- end
- return { row }
-end
-
-local function make(rows, paddings, column_spacing, max_column_len)
- local lines = {}
- for i = 1, #rows do
- local row = string.format('%s', string.rep(' ', column_spacing))
- local items = rows[i]
- for j = 1, #items do
- local value = shorten_str(items[j], max_column_len)
- row = string.format(
- '%s%s%s',
- row,
- value,
- string.rep(' ', paddings[j] - #value)
- )
- end
- lines[#lines + 1] = row
- end
- return lines
-end
-
-local TableComponent = Component:extend()
-
-function TableComponent:new(options)
- assert(
- options == nil or type(options) == 'table',
- 'type error :: expected table or nil'
- )
- options = options or {}
- local height = self:get_min_height()
- local width = self:get_min_width()
- return setmetatable({
- anim_id = nil,
- state = {
- buf = nil,
- win_id = nil,
- ns_id = nil,
- border = nil,
- header = nil,
- loading = false,
- error = false,
- mounted = false,
- paddings = {},
- cache = {
- lines = {},
- cursor = nil,
- },
- paint_count = 0,
- },
- config = Interface
- :new({
- filetype = '',
- header = {},
- column_spacing = 10,
- max_column_len = 40,
- border = {
- enabled = false,
- title = '',
- footer = '',
- hl = 'FloatBorder',
- chars = { '', '', '', '', '', '', '', '' },
- },
- buf_options = {
- ['modifiable'] = false,
- ['buflisted'] = false,
- ['bufhidden'] = 'wipe',
- },
- win_options = {
- ['wrap'] = false,
- ['number'] = false,
- ['winhl'] = 'Normal:',
- ['cursorline'] = false,
- ['cursorbind'] = false,
- ['scrollbind'] = false,
- ['signcolumn'] = 'auto',
- },
- window_props = {
- style = 'minimal',
- relative = 'editor',
- height = height,
- width = width,
- row = 1,
- col = 0,
- focusable = true,
- },
- static = false,
- })
- :assign(options),
- }, TableComponent)
-end
-
-function TableComponent:get_header_buf()
- return self:get_header() and self:get_header():get_buf() or nil
-end
-
-function TableComponent:get_header_win_id()
- return self:get_header() and self:get_header():get_win_id() or nil
-end
-
-function TableComponent:get_header()
- return self.state.header
-end
-
-function TableComponent:get_paddings()
- return self.state.paddings
-end
-
-function TableComponent:get_column_spacing()
- return self.config:get('column_spacing')
-end
-
-function TableComponent:get_column_ranges()
- local column_ranges = {}
- local paddings = self:get_paddings()
- local last_range = nil
- for i = 1, #paddings do
- if i == 1 then
- column_ranges[#column_ranges + 1] = { 0, paddings[i] }
- else
- column_ranges[#column_ranges + 1] = {
- last_range[2],
- last_range[2] + paddings[i],
- }
- end
- last_range = column_ranges[#column_ranges]
- end
- return column_ranges
-end
-
-function TableComponent:set_paddings(paddings)
- assert(type(paddings) == 'table', 'type error :: expected table')
- self.state.paddings = paddings
- return self
-end
-
-function TableComponent:set_header(header)
- assert(type(header) == 'table', 'type error :: expected table')
- self.state.header = header
- return self
-end
-
-function TableComponent:set_lines(lines, force)
- if self:is_static() and self:has_lines() and not force then
- return self
- end
- assert(type(lines) == 'table', 'type error :: expected table')
- self:increment_paint_count()
- self:clear_timers()
- local header = self.config:get('header')
- local column_spacing = self.config:get('column_spacing')
- local max_column_len = self.config:get('max_column_len')
- local paddings = make_paddings(lines, header, column_spacing, max_column_len)
- local column_header = make_heading(
- paddings,
- header,
- column_spacing,
- max_column_len
- )
- local rows = make(lines, paddings, column_spacing, max_column_len)
- buffer.set_lines(self:get_buf(), rows)
- self:get_header():set_lines(column_header)
- self:set_paddings(paddings)
- return self
-end
-
-function TableComponent:mount()
- if self:is_mounted() then
- return self
- end
- local buf_options = self.config:get('buf_options')
- local border_config = self.config:get('border')
- local window_props = self.config:get('window_props')
- local win_options = self.config:get('win_options')
- self:set_buf(vim.api.nvim_create_buf(false, true))
- local buf = self:get_buf()
- buffer.assign_options(buf, buf_options)
- local win_ids = {}
- if self:is_border_enabled() then
- window_props.border = self:make_border(border_config)
- end
- self:set_header(AppBarDecorator:new(window_props, buf):mount())
- -- Correct addition of header decorator parameters.
- window_props.row = window_props.row + 3
- if window_props.height - 3 > 1 then
- window_props.height = window_props.height - 3
- end
- local win_id = vim.api.nvim_open_win(buf, true, window_props)
- for key, value in pairs(win_options) do
- vim.api.nvim_win_set_option(win_id, key, value)
- end
- self:set_win_id(win_id)
- self:set_ns_id(
- vim.api.nvim_create_namespace(
- string.format('tanvirtin/vgit.nvim/%s/%s', buf, win_id)
- )
- )
- win_ids[#win_ids + 1] = win_id
- self:on(
- 'BufWinLeave',
- string.format(':lua require("vgit").renderer.hide_windows(%s)', win_ids)
- )
- self:add_syntax_highlights()
- self:set_mounted(true)
- return self
-end
-
-function TableComponent:unmount()
- self:set_mounted(false)
- local win_id = self:get_win_id()
- if vim.api.nvim_win_is_valid(win_id) then
- self:clear()
- pcall(vim.api.nvim_win_close, win_id, true)
- end
- local header_win_id = self:get_header_win_id()
- if vim.api.nvim_win_is_valid(header_win_id) then
- pcall(vim.api.nvim_win_close, header_win_id, true)
- end
- return self
-end
-
-return TableComponent
diff --git a/lua/vgit/core/Buffer.lua b/lua/vgit/core/Buffer.lua
new file mode 100644
index 00000000..c274521c
--- /dev/null
+++ b/lua/vgit/core/Buffer.lua
@@ -0,0 +1,121 @@
+local keymap = require('vgit.core.keymap')
+local GitObject = require('vgit.core.GitObject')
+local fs = require('vgit.core.fs')
+local Object = require('vgit.core.Object')
+
+local Buffer = Object:extend()
+
+function Buffer:new(bufnr)
+ local filename = nil
+ if bufnr == 0 then
+ bufnr = vim.api.nvim_get_current_buf()
+ filename = fs.filename(bufnr)
+ end
+ return setmetatable({
+ bufnr = bufnr,
+ filename = filename,
+ watcher = nil,
+ git_object = nil,
+ }, Buffer)
+end
+
+function Buffer:sync()
+ local filename = fs.filename(self.bufnr)
+ self.filename = filename
+ self.git_object = GitObject:new(filename)
+ return self
+end
+
+function Buffer:sync_git()
+ self.git_object = GitObject:new(self.filename)
+ return self
+end
+
+function Buffer:create(listed, scratch)
+ listed = listed == nil and false or listed
+ scratch = scratch == nil and true or scratch
+ self.bufnr = vim.api.nvim_create_buf(listed, scratch)
+ return self
+end
+
+function Buffer:is_current()
+ return self.bufnr == vim.api.nvim_get_current_buf()
+end
+
+function Buffer:is_valid()
+ local bufnr = self.bufnr
+ return vim.api.nvim_buf_is_valid(bufnr) and vim.api.nvim_buf_is_loaded(bufnr)
+end
+
+function Buffer:delete(opts)
+ opts = opts or {}
+ vim.tbl_extend('keep', opts, { force = true })
+ vim.api.nvim_buf_delete(self.bufnr, opts)
+ return self
+end
+
+function Buffer:get_lines(start, finish)
+ start = start or 0
+ finish = finish or -1
+ return vim.api.nvim_buf_get_lines(self.bufnr, start, finish, false)
+end
+
+function Buffer:get_option(key)
+ return vim.api.nvim_buf_get_option(self.bufnr, key)
+end
+
+function Buffer:set_lines(lines, start, finish)
+ start = start or 0
+ finish = finish or -1
+ local bufnr = self.bufnr
+ local modifiable = vim.api.nvim_buf_get_option(bufnr, 'modifiable')
+ if modifiable then
+ vim.api.nvim_buf_set_lines(bufnr, start, finish, false, lines)
+ return
+ end
+ vim.api.nvim_buf_set_option(bufnr, 'modifiable', true)
+ vim.api.nvim_buf_set_lines(bufnr, start, finish, false, lines)
+ vim.api.nvim_buf_set_option(bufnr, 'modifiable', false)
+ return self
+end
+
+function Buffer:set_option(key, value)
+ vim.api.nvim_buf_set_option(self.bufnr, key, value)
+ return self
+end
+
+function Buffer:assign_options(options)
+ local bufnr = self.bufnr
+ for key, value in pairs(options) do
+ vim.api.nvim_buf_set_option(bufnr, key, value)
+ end
+ return self
+end
+
+function Buffer:get_line_count()
+ return vim.api.nvim_buf_line_count(self.bufnr)
+end
+
+function Buffer:editing()
+ return self:get_option('modified')
+end
+
+function Buffer:filetype()
+ return fs.detect_filetype(self.filename)
+end
+
+function Buffer:list()
+ local bufnrs = vim.api.nvim_list_bufs()
+ local buffers = {}
+ for i = 1, #bufnrs do
+ buffers[#buffers + 1] = Buffer:new(bufnrs[i])
+ end
+ return buffers
+end
+
+function Buffer:set_keymap(mode, key, action)
+ keymap.buffer_set(self, mode, key, action)
+ return self
+end
+
+return Buffer
diff --git a/lua/vgit/core/CodeDTO.lua b/lua/vgit/core/CodeDTO.lua
new file mode 100644
index 00000000..a7e48445
--- /dev/null
+++ b/lua/vgit/core/CodeDTO.lua
@@ -0,0 +1,21 @@
+local Object = require('vgit.core.Object')
+
+local CodeDTO = Object:extend()
+
+function CodeDTO:new(opts)
+ opts = opts or {}
+ return setmetatable({
+ lines = opts.lines or {},
+ current_lines = opts.current_lines or {},
+ previous_lines = opts.previous_lines or {},
+ lnum_changes = opts.lnum_changes or {},
+ hunks = opts.hunks or {},
+ marks = opts.marks or {},
+ stat = opts.stat or {
+ added = 0,
+ removed = 0,
+ },
+ }, CodeDTO)
+end
+
+return CodeDTO
diff --git a/lua/vgit/core/Config.lua b/lua/vgit/core/Config.lua
new file mode 100644
index 00000000..9106d469
--- /dev/null
+++ b/lua/vgit/core/Config.lua
@@ -0,0 +1,47 @@
+local utils = require('vgit.core.utils')
+local Object = require('vgit.core.Object')
+
+local Config = Object:extend()
+
+function Config:new(state)
+ assert(
+ type(state) == 'nil' or type(state) == 'table',
+ 'type error :: expected table or nil'
+ )
+ return setmetatable({ data = type(state) == 'table' and state or {} }, Config)
+end
+
+function Config:get(key)
+ assert(type(key) == 'string', 'type error :: expected string')
+ assert(self.data[key] ~= nil, string.format('key "%s" does not exist', key))
+ return self.data[key]
+end
+
+function Config:set(key, value)
+ assert(self.data[key] ~= nil, string.format('key "%s" does not exist', key))
+ assert(
+ type(self.data[key]) == type(value),
+ string.format('type error :: expected %s', key)
+ )
+ self.data[key] = value
+end
+
+function Config:assign(config)
+ return utils.object_assign(self.data, config)
+end
+
+function Config:for_each(callback)
+ for key, value in pairs(self.data) do
+ callback(key, value)
+ end
+end
+
+function Config:size()
+ local count = 0
+ for _, _ in pairs(self.data) do
+ count = count + 1
+ end
+ return count
+end
+
+return Config
diff --git a/lua/vgit/core/GitObject.lua b/lua/vgit/core/GitObject.lua
new file mode 100644
index 00000000..2dd4169c
--- /dev/null
+++ b/lua/vgit/core/GitObject.lua
@@ -0,0 +1,148 @@
+local utils = require('vgit.core.utils')
+local fs = require('vgit.core.fs')
+local loop = require('vgit.core.loop')
+local Patch = require('vgit.cli.models.Patch')
+local Git = require('vgit.cli.Git')
+local Object = require('vgit.core.Object')
+
+local GitObject = Object:extend()
+
+function GitObject:new(filename)
+ local dirname = fs.dirname(filename)
+ return setmetatable({
+ dirname = dirname,
+ filename = {
+ native = filename,
+ tracked = nil,
+ },
+ git = Git:new(dirname),
+ hunks = nil,
+ }, GitObject)
+end
+
+function GitObject:is_tracked()
+ return self:tracked_filename() ~= ''
+end
+
+function GitObject:is_in_remote()
+ return self.git:is_in_remote(self:tracked_filename())
+end
+
+function GitObject:tracked_filename()
+ if self.filename.tracked == nil then
+ -- NOTE: git.tracked_filename will return nil if the file does not exist in the git repo.
+ self.filename.tracked = self.git:tracked_filename(self.filename.native)
+ or ''
+ return self.filename.tracked
+ end
+ return self.filename.tracked
+end
+
+function GitObject:patch_hunk(hunk)
+ return Patch:new(self.git:tracked_full_filename(self.filename.native), hunk)
+end
+
+function GitObject:stage_hunk_from_patch(patch)
+ local patch_filename = fs.tmpname()
+ fs.write_file(patch_filename, patch)
+ loop.await_fast_event()
+ local err = self.git:stage_hunk_from_patch(patch_filename)
+ loop.await_fast_event()
+ fs.remove_file(patch_filename)
+ loop.await_fast_event()
+ return err
+end
+
+function GitObject:stage_hunk(hunk)
+ return self:stage_hunk_from_patch(self:patch_hunk(hunk))
+end
+
+function GitObject:stage()
+ local filename = self:tracked_filename()
+ if not self:is_tracked() then
+ filename = utils.strip_substring(self.filename.native, self.dirname)
+ end
+ return self.git:stage_file(filename)
+end
+
+function GitObject:unstage()
+ return self.git:unstage_file(self:tracked_filename())
+end
+
+function GitObject:is_inside_git_dir()
+ return self.git:is_inside_git_dir()
+end
+
+function GitObject:lines(commit_hash)
+ commit_hash = commit_hash or ''
+ return self.git:show(self:tracked_filename(), commit_hash)
+end
+
+function GitObject:revision_lines(commit_hash)
+ return self.git:show(self:tracked_filename(), commit_hash)
+end
+
+function GitObject:is_ignored()
+ return self.git:is_ignored(self.filename.native)
+end
+
+function GitObject:blame_line(lnum)
+ return self.git:blame_line(self:tracked_filename(), lnum)
+end
+
+function GitObject:blames()
+ return self.git:blames(self:tracked_filename())
+end
+
+function GitObject:config()
+ return self.git:config()
+end
+
+function GitObject:live_hunks(current_lines)
+ local filename = self:tracked_filename()
+ loop.await_fast_event()
+ if filename == '' then
+ local hunks = self.git:untracked_hunks(current_lines)
+ self.hunks = hunks
+ return nil, hunks
+ end
+ local temp_filename_b = fs.tmpname()
+ local temp_filename_a = fs.tmpname()
+ local original_lines_err, original_lines = self:lines()
+ loop.await_fast_event()
+ if original_lines_err then
+ return original_lines_err
+ end
+ fs.write_file(temp_filename_a, original_lines)
+ loop.await_fast_event()
+ fs.write_file(temp_filename_b, current_lines)
+ loop.await_fast_event()
+ local hunks_err, hunks = self.git:file_hunks(temp_filename_a, temp_filename_b)
+ loop.await_fast_event()
+ fs.remove_file(temp_filename_a)
+ loop.await_fast_event()
+ fs.remove_file(temp_filename_b)
+ loop.await_fast_event()
+ if not hunks_err then
+ self.hunks = hunks
+ end
+ return hunks_err, hunks
+end
+
+function GitObject:staged_hunks()
+ return self.git:staged_hunks(self:tracked_filename())
+end
+
+function GitObject:remote_hunks(parent_hash, commit_hash)
+ return self.git:remote_hunks(
+ self:tracked_filename(),
+ parent_hash,
+ commit_hash
+ )
+end
+
+function GitObject:logs()
+ return self.git:logs(self:tracked_filename())
+end
+
+return GitObject
diff --git a/lua/vgit/core/Job.lua b/lua/vgit/core/Job.lua
new file mode 100644
index 00000000..cd74b197
--- /dev/null
+++ b/lua/vgit/core/Job.lua
@@ -0,0 +1,74 @@
+local Object = require('vgit.core.Object')
+local Job = Object:extend()
+
+function Job:new(spec)
+ return setmetatable({
+ spec = spec,
+ }, Job)
+end
+
+function Job:parse_result(output, callback)
+ if not callback then
+ return
+ end
+ local line = ''
+ for i = 1, #output do
+ local char = output:sub(i, i)
+ if char == '\n' then
+ callback(line)
+ line = ''
+ else
+ line = line .. char
+ end
+ end
+ if #line ~= 0 then
+ callback(line)
+ end
+end
+
+function Job:start()
+ local stdout_result = ''
+ local stderr_result = ''
+
+ local stdout = vim.loop.new_pipe(false)
+ local stderr = vim.loop.new_pipe(false)
+
+ vim.loop.spawn(self.spec.command, {
+ args = self.spec.args,
+ stdio = { nil, stdout, stderr },
+ cwd = self.spec.cwd,
+ }, function(code, signal)
+ stdout:read_stop()
+ stderr:read_stop()
+
+ if not stdout:is_closing() then
+ stdout:close()
+ end
+ if not stderr:is_closing() then
+ stderr:close()
+ end
+
+ self:parse_result(stdout_result, self.spec.on_stdout)
+ self:parse_result(stderr_result, self.spec.on_stderr)
+
+ if self.spec.on_exit then
+ self.spec.on_exit(code, signal)
+ end
+ end)
+
+ stdout:read_start(function(_, chunk)
+ if chunk then
+ stdout_result = stdout_result .. chunk
+ end
+ end)
+
+ stderr:read_start(function(_, chunk)
+ if chunk then
+ stderr_result = stderr_result .. chunk
+ end
+ end)
+
+ return self
+end
+
+return Job
diff --git a/lua/vgit/core/Object.lua b/lua/vgit/core/Object.lua
new file mode 100644
index 00000000..019375ae
--- /dev/null
+++ b/lua/vgit/core/Object.lua
@@ -0,0 +1,50 @@
+local Object = {}
+Object.__index = Object
+
+function Object:new() end
+
+function Object:extend()
+ local cls = {}
+ for k, v in pairs(self) do
+ if k:find('__') == 1 then
+ cls[k] = v
+ end
+ end
+ cls.__index = cls
+ cls.super = self
+ setmetatable(cls, self)
+ return cls
+end
+
+function Object:implement(...)
+ for _, cls in pairs({ ... }) do
+ for k, v in pairs(cls) do
+ if self[k] == nil and type(v) == 'function' then
+ self[k] = v
+ end
+ end
+ end
+end
+
+function Object:is(T)
+ local mt = getmetatable(self)
+ while mt do
+ if mt == T then
+ return true
+ end
+ mt = getmetatable(mt)
+ end
+ return false
+end
+
+function Object:__tostring()
+ return 'Object'
+end
+
+function Object:__call(...)
+ local obj = setmetatable({}, self)
+ obj:new(...)
+ return obj
+end
+
+return Object
diff --git a/lua/vgit/core/Versioning.lua b/lua/vgit/core/Versioning.lua
new file mode 100644
index 00000000..caf71612
--- /dev/null
+++ b/lua/vgit/core/Versioning.lua
@@ -0,0 +1,58 @@
+local console = require('vgit.core.console')
+local Object = require('vgit.core.Object')
+
+local Versioning = Object:extend()
+
+function Versioning:new()
+ return setmetatable({
+ history = {
+ {
+ patch = 0,
+ minor = 0,
+ major = 0,
+ },
+ {
+ patch = 0,
+ minor = 1,
+ major = 0,
+ },
+ },
+ }, Versioning)
+end
+
+function Versioning:current()
+ return self.history[#self.history]
+end
+
+function Versioning:previous()
+ return self.history[#self.history - 1]
+end
+
+function Versioning:is_neovim_compatible()
+ local plugin_version = self:current()
+ local expected_neovim_version = {
+ patch = 0,
+ minor = 5,
+ major = 0,
+ }
+ local actual_neovim_version = vim.version()
+ if
+ actual_neovim_version.patch >= expected_neovim_version.patch
+ and actual_neovim_version.minor >= expected_neovim_version.minor
+ and actual_neovim_version.major >= expected_neovim_version.major
+ then
+ return true
+ end
+ console.info(
+ string.format(
+ 'Current Neovim version %s.%s is incompatible with VGit %s.%s.',
+ actual_neovim_version.major,
+ actual_neovim_version.minor,
+ plugin_version.major,
+ plugin_version.minor
+ )
+ )
+ return false
+end
+
+return Versioning
diff --git a/lua/vgit/core/Window.lua b/lua/vgit/core/Window.lua
new file mode 100644
index 00000000..46fe48c2
--- /dev/null
+++ b/lua/vgit/core/Window.lua
@@ -0,0 +1,128 @@
+local assertion = require('vgit.core.assertion')
+local Object = require('vgit.core.Object')
+
+local Window = Object:extend()
+
+function Window:new(win_id)
+ assertion.assert_number(win_id)
+ if win_id == 0 then
+ win_id = vim.api.nvim_get_current_win()
+ end
+ return setmetatable({
+ win_id = win_id,
+ }, Window)
+end
+
+function Window:open(buffer, opts)
+ opts = opts or {}
+ local focus = opts.focus
+ if opts.focus then
+ opts.focus = nil
+ end
+ local win_id = vim.api.nvim_open_win(
+ buffer.bufnr,
+ focus ~= nil and focus or false,
+ opts
+ )
+ return setmetatable({
+ win_id = win_id,
+ }, Window)
+end
+
+function Window:get_cursor()
+ local _, cursor = pcall(vim.api.nvim_win_get_cursor, self.win_id)
+ if not cursor then
+ return { 1, 1 }
+ end
+ return cursor
+end
+
+function Window:get_lnum()
+ return self:get_cursor()[1]
+end
+
+function Window:get_position()
+ return vim.api.nvim_win_get_position(self.win_id)
+end
+
+function Window:get_height()
+ return vim.api.nvim_win_get_height(self.win_id)
+end
+
+function Window:get_width()
+ return vim.api.nvim_win_get_width(self.win_id)
+end
+
+function Window:set_cursor(cursor)
+ return self:call(function()
+ pcall(vim.api.nvim_win_set_cursor, self.win_id, cursor)
+ end)
+end
+
+function Window:set_lnum(lnum)
+ local cursor = self:get_cursor()
+ return self:set_cursor({ lnum, cursor[2] })
+end
+
+function Window:set_option(key, value)
+ vim.api.nvim_win_set_option(self.win_id, key, value)
+ return self
+end
+
+function Window:set_height(height)
+ vim.api.nvim_win_set_height(self.win_id, height)
+ return self
+end
+
+function Window:set_width(width)
+ vim.api.nvim_win_set_width(self.win_id, width)
+ return self
+end
+
+function Window:set_window_props(window_props)
+ if window_props.focus then
+ window_props.focus = nil
+ end
+ vim.api.nvim_win_set_config(self.win_id, window_props)
+ return self
+end
+
+function Window:get_window_props()
+ return vim.api.nvim_win_get_config(self.win_id)
+end
+
+function Window:assign_options(options)
+ for key, value in pairs(options) do
+ vim.api.nvim_win_set_option(self.win_id, key, value)
+ end
+ return self
+end
+
+function Window:is_valid()
+ return vim.api.nvim_win_is_valid(self.win_id)
+end
+
+function Window:close()
+ pcall(vim.api.nvim_win_hide, self.win_id)
+ return self
+end
+
+function Window:is_focused()
+ return self.win_id == vim.api.nvim_get_current_win()
+end
+
+function Window:focus()
+ vim.api.nvim_set_current_win(self.win_id)
+ return self
+end
+
+function Window:is_same(window)
+ return self.win_id == window.win_id
+end
+
+function Window:call(callback)
+ vim.api.nvim_win_call(self.win_id, callback)
+ return self
+end
+
+return Window
diff --git a/lua/vgit/core/assertion.lua b/lua/vgit/core/assertion.lua
new file mode 100644
index 00000000..a6a4d4f9
--- /dev/null
+++ b/lua/vgit/core/assertion.lua
@@ -0,0 +1,69 @@
+local assertion = {}
+
+function assertion.assert(cond, msg)
+ if not cond then
+ error(debug.traceback(msg))
+ end
+ return assertion
+end
+
+function assertion.assert_type(value, t)
+ assertion.assert(
+ type(value) == t,
+ string.format('type error :: expected %s', t)
+ )
+ return assertion
+end
+
+function assertion.assert_types(value, types)
+ assertion.assert_list(types)
+ local passed = false
+ for i = 1, #types do
+ local t = types[i]
+ if type(value) == t then
+ passed = true
+ end
+ end
+ assertion.assert(
+ passed,
+ string.format('type error :: expected %s', vim.inspect(types))
+ )
+ return assertion
+end
+
+function assertion.assert_number(value)
+ assertion.assert_type(value, 'number')
+ return assertion
+end
+
+function assertion.assert_string(value)
+ assertion.assert_type(value, 'string')
+ return assertion
+end
+
+function assertion.assert_function(value)
+ assertion.assert_type(value, 'function')
+ return assertion
+end
+
+function assertion.assert_boolean(value)
+ assertion.assert_type(value, 'boolean')
+ return assertion
+end
+
+function assertion.assert_nil(value)
+ assertion.assert_type(value, 'nil')
+ return assertion
+end
+
+function assertion.assert_table(value)
+ assertion.assert_type(value, 'table')
+ return assertion
+end
+
+function assertion.assert_list(value)
+ assertion.assert(vim.tbl_islist(value), 'type error :: expected list')
+ return assertion
+end
+
+return assertion
diff --git a/lua/vgit/core/autocmd.lua b/lua/vgit/core/autocmd.lua
new file mode 100644
index 00000000..92cede99
--- /dev/null
+++ b/lua/vgit/core/autocmd.lua
@@ -0,0 +1,49 @@
+local state = {
+ namespace = 'VGit',
+}
+
+local autocmd = {}
+
+autocmd.register_module = function(dependency)
+ vim.api.nvim_exec(
+ string.format('aug %s | autocmd! | aug END', state.namespace),
+ false
+ )
+ if dependency then
+ dependency()
+ end
+end
+
+autocmd.off = function()
+ vim.api.nvim_exec(
+ string.format('aug %s | autocmd! | aug END', state.namespace),
+ false
+ )
+end
+
+autocmd.on = function(cmd, handler, options)
+ options = options or {}
+ if options.once == nil then
+ options.once = false
+ end
+ if options.override == nil then
+ options.override = true
+ end
+ if options.nested == nil then
+ options.nested = false
+ end
+ vim.api.nvim_exec(
+ string.format(
+ 'au%s %s %s * %s %s :lua _G.package.loaded.vgit.%s',
+ options.override and '!' or '',
+ state.namespace,
+ cmd,
+ options.nested and '++nested' or '',
+ options.once and '++once' or '',
+ handler
+ ),
+ false
+ )
+end
+
+return autocmd
diff --git a/lua/vgit/core/console.lua b/lua/vgit/core/console.lua
new file mode 100644
index 00000000..87f8159f
--- /dev/null
+++ b/lua/vgit/core/console.lua
@@ -0,0 +1,81 @@
+local loop = require('vgit.core.loop')
+local env = require('vgit.core.env')
+
+local console = {}
+
+local function add_vgit_prefix(msg)
+ return string.format('[VGit] %s', msg)
+end
+
+local function vgit_stringify(msg)
+ if type(msg) ~= 'table' then
+ return add_vgit_prefix(msg)
+ end
+ local acc = ''
+ for i = 1, #msg do
+ if i == #msg then
+ acc = string.format('%s%s', acc, msg[i])
+ else
+ acc = string.format('%s%s\n', acc, msg[i])
+ end
+ end
+ return acc
+end
+
+local function log_msg(msg, hi)
+ if hi then
+ vim.cmd(string.format('echohl %s', hi))
+ end
+ vim.cmd(string.format('echo "%s"', vgit_stringify(msg)))
+ if hi then
+ vim.cmd('echohl NONE')
+ end
+end
+
+console.error = loop.async(function(msg)
+ loop.await_fast_event()
+ log_msg(msg, 'ErrorMsg')
+end)
+
+console.warn = loop.async(function(msg)
+ loop.await_fast_event()
+ log_msg(msg, 'WarningMsg')
+end)
+
+console.log = loop.async(function(msg)
+ loop.await_fast_event()
+ log_msg(msg, 'TODO')
+end)
+
+console.info = loop.async(function(msg)
+ loop.await_fast_event()
+ vim.notify(msg, 'info')
+end)
+
+console.debug = loop.async(function(msg, trace)
+ if not env.get('DEBUG') then
+ return
+ end
+ local new_msg = ''
+ if vim.tbl_islist(msg) then
+ for i = 1, #msg do
+ local m = msg[i]
+ if i == 1 then
+ new_msg = new_msg .. m
+ else
+ new_msg = new_msg .. ', ' .. m
+ end
+ end
+ else
+ new_msg = msg
+ end
+ local log = ''
+ if trace then
+ log = string.format('[%s] %s\n%s', os.date('%H:%M:%S'), new_msg, trace)
+ else
+ log = string.format('[%s] %s', os.date('%H:%M:%S'), new_msg)
+ end
+ print(vgit_stringify(log))
+end)
+
+return console
diff --git a/lua/vgit/core/env.lua b/lua/vgit/core/env.lua
new file mode 100644
index 00000000..69e27dc0
--- /dev/null
+++ b/lua/vgit/core/env.lua
@@ -0,0 +1,28 @@
+local assertion = require('vgit.core.assertion')
+
+local state = {}
+
+local env = {}
+
+function env.set(key, value)
+ assertion.assert_string(key).assert_types(
+ value,
+ { 'string', 'number', 'boolean' }
+ )
+ state[key] = value
+ return env
+end
+
+function env.unset(key)
+ assertion.assert_string(key)
+ assertion.assert(state[key], 'error :: no value set for given key')
+ state[key] = nil
+ return env
+end
+
+function env.get(key)
+ assertion.assert_string(key)
+ return state[key]
+end
+
+return env
diff --git a/lua/vgit/core/fs.lua b/lua/vgit/core/fs.lua
new file mode 100644
index 00000000..4cab2da8
--- /dev/null
+++ b/lua/vgit/core/fs.lua
@@ -0,0 +1,108 @@
+local Path = require('plenary.path')
+local pfiletype = require('plenary.filetype')
+
+local fs = {}
+
+fs.cwd_filename = function(filepath)
+ local end_index = nil
+ for i = #filepath, 1, -1 do
+ local letter = filepath:sub(i, i)
+ if letter == '/' then
+ end_index = i
+ end
+ end
+ if not end_index then
+ return ''
+ end
+ return filepath:sub(1, end_index)
+end
+
+fs.relative_filename = function(filepath)
+ return Path:new(filepath):make_relative(vim.loop.cwd())
+end
+
+fs.short_filename = function(filepath)
+ local filename = ''
+ for i = #filepath, 1, -1 do
+ local letter = filepath:sub(i, i)
+ if letter == '/' then
+ break
+ end
+ filename = letter .. filename
+ end
+ return filename
+end
+
+fs.filename = function(buf)
+ local filepath = vim.api.nvim_buf_get_name(buf)
+ return fs.relative_filename(filepath)
+end
+
+fs.filetype = function(buffer)
+ return buffer:get_option('filetype')
+end
+
+fs.detect_filetype = pfiletype.detect
+
+fs.tmpname = function()
+ local length = 6
+ local res = ''
+ for _ = 1, length do
+ res = res .. string.char(math.random(97, 122))
+ end
+ return string.format('/tmp/%s_vgit', res)
+end
+
+fs.read_file = function(filepath)
+ local fd = vim.loop.fs_open(filepath, 'r', 438)
+ if fd == nil then
+ return { 'File not found' }, nil
+ end
+ local stat = vim.loop.fs_fstat(fd)
+ if stat.type ~= 'file' then
+ return { 'File not found' }, nil
+ end
+ local data = vim.loop.fs_read(fd, stat.size, 0)
+ if not vim.loop.fs_close(fd) then
+ return { 'Failed to close file' }, nil
+ end
+ local split_data = {}
+ local line = ''
+ for i = 1, #data do
+ local word = data:sub(i, i)
+ if word == '\n' or word == '\r' then
+ split_data[#split_data + 1] = line
+ line = ''
+ else
+ line = line .. word
+ end
+ end
+ if not line == '' then
+ split_data[#split_data + 1] = line
+ end
+ return nil, split_data
+end
+
+fs.write_file = function(filepath, lines)
+ local f = io.open(filepath, 'wb')
+ for i = 1, #lines do
+ local l = lines[i]
+ f:write(l)
+ f:write('\n')
+ end
+ f:close()
+end
+
+fs.remove_file = function(filepath)
+ return os.remove(filepath)
+end
+
+fs.exists = function(filepath)
+ return (vim.loop.fs_stat(filepath) and true) or false
+end
+
+fs.dirname = function(filepath)
+ return filepath:match('(.*[/\\])') or ''
+end
+
+return fs
diff --git a/lua/vgit/core/highlighter.lua b/lua/vgit/core/highlighter.lua
new file mode 100644
index 00000000..867f5bc7
--- /dev/null
+++ b/lua/vgit/core/highlighter.lua
@@ -0,0 +1,58 @@
+local hls_setting = require('vgit.settings.hls')
+
+local highlighter = {}
+
+highlighter.create = function(group, color)
+ local link = color
+ if type(link) == 'string' then
+ vim.api.nvim_exec(string.format('highlight link %s %s', group, link), false)
+ return
+ end
+ if color.override == false then
+ local ok, hl = pcall(vim.api.nvim_get_hl_by_name, group, true)
+ -- TODO: If a highlight gets cleared neovim returns { [true] = 'some id' }.
+ -- this might be changed in the future by neovim, revisit this.
+ if ok and (type(hl) == 'table' and not hl[true] and hl ~= nil) then
+ return
+ end
+ end
+ local gui = color.gui and 'gui = ' .. color.gui or 'gui = NONE'
+ local fg = color.fg and 'guifg = ' .. color.fg or 'guifg = NONE'
+ local bg = color.bg and 'guibg = ' .. color.bg or 'guibg = NONE'
+ local sp = color.sp and 'guisp = ' .. color.sp or ''
+ vim.api.nvim_exec(
+ 'highlight ' .. group .. ' ' .. gui .. ' ' .. fg .. ' ' .. bg .. ' ' .. sp,
+ false
+ )
+end
+
+highlighter.create_theme = function(hls)
+ for hl, color in pairs(hls) do
+ highlighter.create(hl, color)
+ end
+end
+
+highlighter.create_default_theme = function()
+ hls_setting:for_each(function(hl, color)
+ highlighter.create(hl, color)
+ end)
+end
+
+highlighter.register_module = function(dependency)
+ hls_setting:for_each(function(hl, color)
+ highlighter.create(hl, color)
+ end)
+ if dependency then
+ dependency()
+ end
+end
+
+highlighter.add = function(buffer, ...)
+ vim.api.nvim_buf_add_highlight(buffer.bufnr, ...)
+end
+
+highlighter.clear = function(buffer, ns_id)
+ vim.api.nvim_buf_clear_namespace(buffer.bufnr, ns_id, 0, -1)
+end
+
+return highlighter
diff --git a/lua/vgit/icons.lua b/lua/vgit/core/icons.lua
similarity index 68%
rename from lua/vgit/icons.lua
rename to lua/vgit/core/icons.lua
index eab451dc..cb3ad3de 100644
--- a/lua/vgit/icons.lua
+++ b/lua/vgit/core/icons.lua
@@ -1,6 +1,6 @@
-local M = {}
+local icons = {}
-M.file_icon = function(fname, extension)
+icons.file_icon = function(fname, extension)
local ok, web_devicons = pcall(require, 'nvim-web-devicons')
if not ok then
return ' ', nil
@@ -8,4 +8,4 @@ M.file_icon = function(fname, extension)
return web_devicons.get_icon(fname, extension)
end
-return M
+return icons
diff --git a/lua/vgit/core/keymap.lua b/lua/vgit/core/keymap.lua
new file mode 100644
index 00000000..c45e8379
--- /dev/null
+++ b/lua/vgit/core/keymap.lua
@@ -0,0 +1,39 @@
+local keymap = {}
+
+local function parse_commands(commands)
+ local parsed_commands = vim.split(commands, ' ')
+ for i = 1, #parsed_commands do
+ local c = parsed_commands[i]
+ parsed_commands[i] = vim.trim(c)
+ end
+ return parsed_commands
+end
+
+keymap.set = function(mode, key, action)
+ vim.api.nvim_set_keymap(mode, key, string.format(':VGit %s', action), {
+ noremap = true,
+ silent = true,
+ })
+end
+
+keymap.buffer_set = function(buffer, mode, key, action)
+ vim.api.nvim_buf_set_keymap(
+ buffer.bufnr,
+ mode,
+ key,
+ string.format(':VGit %s', action),
+ {
+ silent = true,
+ noremap = true,
+ }
+ )
+end
+
+keymap.define = function(keymaps)
+ for commands, action in pairs(keymaps) do
+ commands = parse_commands(commands)
+ keymap.set(commands[1], commands[2], action)
+ end
+end
+
+return keymap
diff --git a/lua/vgit/core/loop.lua b/lua/vgit/core/loop.lua
new file mode 100644
index 00000000..989546af
--- /dev/null
+++ b/lua/vgit/core/loop.lua
@@ -0,0 +1,52 @@
+local scheduler = require('plenary.async.util').scheduler
+local async = require('plenary.async.async')
+-- Facade module for libuv lua bindings.
+local loop = {}
+
+loop.throttle = function(fn, ms)
+ local timer = vim.loop.new_timer()
+ local running = false
+ return function(...)
+ if running then
+ return
+ end
+ timer:start(ms, 0, function()
+ running = false
+ end)
+ running = true
+ fn(...)
+ end
+end
+
+loop.debounce = function(fn, ms)
+ local timer = vim.loop.new_timer()
+ return function(...)
+ local argv = { ... }
+ local argc = select('#', ...)
+ timer:start(ms, 0, function()
+ fn(unpack(argv, 1, argc))
+ end)
+ end
+end
+
+loop.watch = function(filepath, callback)
+ local watcher = vim.loop.new_fs_event()
+ vim.loop.fs_event_start(watcher, filepath, {
+ watch_entry = false,
+ stat = false,
+ recursive = false,
+ }, callback)
+ return watcher
+end
+
+loop.unwatch = function(watcher)
+ vim.loop.fs_event_stop(watcher)
+end
+
+loop.async = async.void
+
+loop.promisify = async.wrap
+
+loop.await_fast_event = scheduler
+
+return loop
diff --git a/lua/vgit/core/sign.lua b/lua/vgit/core/sign.lua
new file mode 100644
index 00000000..c508f9c7
--- /dev/null
+++ b/lua/vgit/core/sign.lua
@@ -0,0 +1,54 @@
+local signs_setting = require('vgit.settings.signs')
+
+local sign = {}
+
+local ns_id = 'tanvirtin/vgit.nvim/hunk/signs'
+
+sign.define = function(name, config)
+ vim.fn.sign_define(name, {
+ text = config.text,
+ texthl = config.texthl,
+ numhl = config.numhl,
+ icon = config.icon,
+ linehl = config.linehl,
+ })
+end
+
+sign.place = function(buffer, lnum, type, priority)
+ local bufnr = buffer.bufnr
+ vim.fn.sign_place(lnum, string.format('%s/%s', ns_id, bufnr), type, bufnr, {
+ id = lnum,
+ lnum = lnum,
+ priority = priority,
+ })
+end
+
+sign.unplace = function(buffer)
+ local bufnr = buffer.bufnr
+ vim.fn.sign_unplace(string.format('%s/%s', ns_id, bufnr))
+end
+
+sign.get = function(buffer, lnum)
+ local bufnr = buffer.bufnr
+ local signs_to_define = vim.fn.sign_getplaced(bufnr, {
+ group = string.format('%s/%s', ns_id, bufnr),
+ id = lnum,
+ })[1].signs
+ local result = {}
+ for i = 1, #signs_to_define do
+ local sign_to_define = signs_to_define[i]
+ result[i] = sign_to_define.name
+ end
+ return result
+end
+
+sign.register_module = function(dependency)
+ for name, config in pairs(signs_setting:get('definitions')) do
+ sign.define(name, config)
+ end
+ if dependency then
+ dependency()
+ end
+end
+
+return sign
diff --git a/lua/vgit/core/utils.lua b/lua/vgit/core/utils.lua
new file mode 100644
index 00000000..9677eed5
--- /dev/null
+++ b/lua/vgit/core/utils.lua
@@ -0,0 +1,86 @@
+local utils = {}
+
+utils.retrieve = function(cmd, ...)
+ if type(cmd) == 'function' then
+ return cmd(...)
+ end
+ return cmd
+end
+
+utils.round = function(x)
+ return x >= 0 and math.floor(x + 0.5) or math.ceil(x - 0.5)
+end
+
+utils.shorten_string = function(str, limit)
+ if #str > limit then
+ str = str:sub(1, limit - 3)
+ str = str .. '...'
+ end
+ return str
+end
+
+utils.accumulate_string = function(existing_text, new_text)
+ local start_range = #existing_text
+ local end_range = start_range + #new_text
+ local text = existing_text .. new_text
+ return text, {
+ start = start_range,
+ finish = end_range,
+ }
+end
+
+utils.strip_substring = function(given_string, substring)
+ if substring == '' then
+ return given_string
+ end
+ local rc_s = ''
+ local i = 1
+ local found = false
+ while i <= #given_string do
+ local temp_i = 0
+ if not found then
+ for j = 1, #substring do
+ local s_j = substring:sub(j, j)
+ local s_i = given_string:sub(i + temp_i, i + temp_i)
+ if s_j == s_i then
+ temp_i = temp_i + 1
+ end
+ end
+ end
+ if temp_i == #substring then
+ found = true
+ i = i + temp_i
+ else
+ rc_s = rc_s .. given_string:sub(i, i)
+ i = i + 1
+ end
+ end
+ return rc_s
+end
+
+-- Does deep object assign
+utils.object_assign = function(state_segment, config_segment)
+ if type(config_segment) == 'table' and not vim.tbl_islist(config_segment) then
+ for key, value in pairs(config_segment) do
+ if not state_segment[key] then
+ state_segment[key] = value
+ else
+ if type(value) == 'table' and not vim.tbl_islist(value) then
+ utils.object_assign(state_segment[key], value)
+ else
+ state_segment[key] = value
+ end
+ end
+ end
+ end
+ return state_segment
+end
+
+utils.list_concat = function(a, b)
+ for i = 1, #b do
+ a[#a + 1] = b[i]
+ end
+ return a
+end
+
+return utils
diff --git a/lua/vgit/core/virtual_text.lua b/lua/vgit/core/virtual_text.lua
new file mode 100644
index 00000000..61519319
--- /dev/null
+++ b/lua/vgit/core/virtual_text.lua
@@ -0,0 +1,34 @@
+local virtual_text = {}
+
+virtual_text.add = function(buffer, ...)
+ vim.api.nvim_buf_set_extmark(buffer.bufnr, ...)
+end
+
+virtual_text.delete = function(buffer, ...)
+ vim.api.nvim_buf_del_extmark(buffer.bufnr, ...)
+end
+
+virtual_text.transpose_text =
+ function(buffer, text, ns_id, hl_group, row, col_start, pos)
+ vim.api.nvim_buf_set_extmark(buffer.bufnr, ns_id, row, col_start, {
+ id = row + 1 + col_start,
+ virt_text = { { text, hl_group } },
+ virt_text_pos = pos or 'overlay',
+ hl_mode = 'combine',
+ })
+ end
+
+virtual_text.transpose_line = function(buffer, texts, ns_id, lnum, pos)
+ vim.api.nvim_buf_set_extmark(buffer.bufnr, ns_id, lnum, 0, {
+ id = lnum + 1,
+ virt_text = texts,
+ virt_text_pos = pos or 'overlay',
+ hl_mode = 'combine',
+ })
+end
+
+virtual_text.clear = function(buffer, ns_id)
+ vim.api.nvim_buf_clear_namespace(buffer.bufnr, ns_id, 0, -1)
+end
+
+return virtual_text
diff --git a/lua/vgit/decorators/AppBarDecorator.lua b/lua/vgit/decorators/AppBarDecorator.lua
deleted file mode 100644
index 2a0a8cac..00000000
--- a/lua/vgit/decorators/AppBarDecorator.lua
+++ /dev/null
@@ -1,113 +0,0 @@
-local Object = require('plenary.class')
-local autocmd = require('vgit.autocmd')
-local buffer = require('vgit.buffer')
-local virtual_text = require('vgit.virtual_text')
-local render_store = require('vgit.stores.render_store')
-local AppBarDecorator = Object:extend()
-
-local config = render_store.get('layout').decorator
-
-function AppBarDecorator:new(window_props, content_buf)
- return setmetatable({
- buf = nil,
- win_id = nil,
- content_buf = content_buf,
- window_props = window_props,
- ns_id = vim.api.nvim_create_namespace('tanvirtin/vgit.nvim/AppBarDecorator'),
- }, AppBarDecorator)
-end
-
-function AppBarDecorator:make_border(c)
- if c.hl then
- local new_border = {}
- for _, char in pairs(c.chars) do
- if type(char) == 'table' then
- char[2] = c.hl
- new_border[#new_border + 1] = char
- else
- new_border[#new_border + 1] = { char, c.hl }
- end
- end
- return new_border
- end
- return c.chars
-end
-
-function AppBarDecorator:mount()
- self.buf = vim.api.nvim_create_buf(true, true)
- buffer.assign_options(self.buf, {
- ['modifiable'] = false,
- ['bufhidden'] = 'wipe',
- ['buflisted'] = false,
- })
- self.win_id = vim.api.nvim_open_win(self.buf, false, {
- border = self:make_border(config.app_bar.border),
- style = 'minimal',
- focusable = false,
- relative = self.window_props.relative,
- row = self.window_props.row,
- col = self.window_props.col,
- width = self.window_props.width - 2,
- height = 1,
- zindex = 100,
- })
- vim.api.nvim_win_set_option(self.win_id, 'cursorbind', false)
- vim.api.nvim_win_set_option(self.win_id, 'scrollbind', false)
- vim.api.nvim_win_set_option(self.win_id, 'winhl', 'Normal:')
- autocmd.buf.on(
- self.content_buf,
- 'WinClosed',
- string.format(
- ':lua _G.package.loaded.vgit.renderer.hide_windows({ %s })',
- self.win_id
- ),
- { once = true }
- )
- return self
-end
-
-function AppBarDecorator:get_win_id()
- return self.win_id
-end
-
-function AppBarDecorator:get_buf()
- return self.buf
-end
-
-function AppBarDecorator:get_ns_id()
- return self.ns_id
-end
-
-function AppBarDecorator:get_lines()
- return buffer.get_lines(self:get_buf())
-end
-
-function AppBarDecorator:set_lines(lines)
- assert(vim.tbl_islist(lines), 'type error :: expected list table')
- buffer.set_lines(self:get_buf(), lines)
- return self
-end
-
-function AppBarDecorator:transpose_text(text, row, col, pos)
- assert(vim.tbl_islist(text), 'type error :: expected list table')
- assert(#text == 2, 'invalid number of text entries')
- assert(type(row) == 'number', 'type error :: expected number')
- assert(type(col) == 'number', 'type error :: expected number')
- virtual_text.transpose_text(
- self:get_buf(),
- text[1],
- self:get_ns_id(),
- text[2],
- row,
- col,
- pos
- )
- return self
-end
-
-function AppBarDecorator:clear_ns_id()
- virtual_text.clear(self:get_buf(), self:get_ns_id())
- return self
-end
-
-return AppBarDecorator
diff --git a/lua/vgit/decorators/VirtualLineNrDecorator.lua b/lua/vgit/decorators/VirtualLineNrDecorator.lua
deleted file mode 100644
index a608d443..00000000
--- a/lua/vgit/decorators/VirtualLineNrDecorator.lua
+++ /dev/null
@@ -1,81 +0,0 @@
-local Object = require('plenary.class')
-local autocmd = require('vgit.autocmd')
-local buffer = require('vgit.buffer')
-
-local VirtualLineNrDecorator = Object:extend()
-
-function VirtualLineNrDecorator:new(config, window_props, content_buf)
- return setmetatable({
- buf = nil,
- win_id = nil,
- ns_id = nil,
- config = config,
- content_buf = content_buf,
- window_props = window_props,
- }, VirtualLineNrDecorator)
-end
-
-function VirtualLineNrDecorator:mount()
- self.buf = vim.api.nvim_create_buf(false, true)
- buffer.assign_options(self.buf, {
- ['modifiable'] = false,
- ['bufhidden'] = 'wipe',
- ['buflisted'] = false,
- })
- self.win_id = vim.api.nvim_open_win(self.buf, false, {
- relative = 'editor',
- style = 'minimal',
- focusable = false,
- row = self.window_props.row,
- col = self.window_props.col,
- height = self.window_props.height,
- width = self.config.width,
- })
- vim.api.nvim_win_set_option(self.win_id, 'cursorbind', true)
- vim.api.nvim_win_set_option(self.win_id, 'scrollbind', true)
- vim.api.nvim_win_set_option(self.win_id, 'winhl', 'Normal:')
- self.ns_id = vim.api.nvim_create_namespace(
- string.format(
- 'tanvirtin/vgit.nvim/virtual_line_nr/%s/%s',
- self.buf,
- self.win_id
- )
- )
- autocmd.buf.on(
- self.content_buf,
- 'WinClosed',
- string.format(
- ':lua _G.package.loaded.vgit.renderer.hide_windows({ %s })',
- self.win_id
- ),
- { once = true }
- )
- return self
-end
-
-function VirtualLineNrDecorator:get_win_id()
- return self.win_id
-end
-
-function VirtualLineNrDecorator:get_buf()
- return self.buf
-end
-
-function VirtualLineNrDecorator:set_lines(lines)
- buffer.set_lines(self.buf, lines)
- return self
-end
-
-function VirtualLineNrDecorator:set_hls(hls)
- for i = 1, #hls do
- vim.api.nvim_buf_add_highlight(self.buf, -1, hls[i], i - 1, 0, -1)
- end
- return self
-end
-
-function VirtualLineNrDecorator:unmount()
- vim.api.nvim_win_close(self:get_win_id(), true)
- return self
-end
-
-return VirtualLineNrDecorator
diff --git a/lua/vgit/defer.lua b/lua/vgit/defer.lua
deleted file mode 100644
index 251c6b36..00000000
--- a/lua/vgit/defer.lua
+++ /dev/null
@@ -1,29 +0,0 @@
-local M = {}
-
-M.throttle_leading = function(fn, ms)
- local timer = vim.loop.new_timer()
- local running = false
- return function(...)
- if running then
- return
- end
- timer:start(ms, 0, function()
- running = false
- end)
- running = true
- fn(...)
- end
-end
-
-M.debounce_trailing = function(fn, ms)
- local timer = vim.loop.new_timer()
- return function(...)
- local argv = { ... }
- local argc = select('#', ...)
- timer:start(ms, 0, function()
- fn(unpack(argv, 1, argc))
- end)
- end
-end
-
-return M
diff --git a/lua/vgit/features/BufferHunks.lua b/lua/vgit/features/BufferHunks.lua
new file mode 100644
index 00000000..bbfd9ce9
--- /dev/null
+++ b/lua/vgit/features/BufferHunks.lua
@@ -0,0 +1,230 @@
+local console = require('vgit.core.console')
+local Window = require('vgit.core.Window')
+local loop = require('vgit.core.loop')
+local Feature = require('vgit.Feature')
+
+local BufferHunks = Feature:extend()
+
+function BufferHunks:new(git_store, navigation, marker)
+ return setmetatable({
+ git_store = git_store,
+ navigation = navigation,
+ marker = marker,
+ }, BufferHunks)
+end
+
+function BufferHunks:move_up()
+ loop.await_fast_event()
+ local buffer = self.git_store:current()
+ if not buffer then
+ return
+ end
+ local hunks = buffer.git_object.hunks
+ if hunks and #hunks ~= 0 then
+ local window = Window:new(0)
+ local selected = self.navigation:hunk_up(window, hunks)
+ self.marker:mark_current_hunk(
+ buffer,
+ window,
+ string.format('%s/%s Changes', selected, #hunks)
+ )
+ end
+end
+
+function BufferHunks:move_down()
+ loop.await_fast_event()
+ local buffer = self.git_store:current()
+ if not buffer then
+ return
+ end
+ local hunks = buffer.git_object.hunks
+ if hunks and #hunks ~= 0 then
+ local window = Window:new(0)
+ local selected = self.navigation:hunk_down(window, hunks)
+ self.marker:mark_current_hunk(
+ buffer,
+ window,
+ string.format('%s/%s Changes', selected, #hunks)
+ )
+ end
+end
+
+function BufferHunks:cursor_hunk()
+ loop.await_fast_event()
+ local buffer = self.git_store:current()
+ if not buffer then
+ return
+ end
+ local window = Window:new(0)
+ local lnum = window:get_lnum()
+ local hunks = buffer.git_object.hunks
+ if not hunks then
+ return
+ end
+ for i = 1, #hunks do
+ local hunk = hunks[i]
+ if lnum == 1 and hunk.start == 0 and hunk.finish == 0 then
+ return hunk, i
+ end
+ if lnum >= hunk.start and lnum <= hunk.finish then
+ return hunk, i
+ end
+ end
+end
+
+function BufferHunks:stage_all()
+ loop.await_fast_event()
+ local buffer = self.git_store:current()
+ if not buffer then
+ return
+ end
+ local err = buffer.git_object:stage()
+ loop.await_fast_event()
+ if err then
+ console.debug(err, debug.traceback())
+ return
+ end
+ vim.cmd('edit')
+end
+
+function BufferHunks:cursor_stage()
+ loop.await_fast_event()
+ local buffer = self.git_store:current()
+ if not buffer then
+ return
+ end
+ if buffer:editing() then
+ return
+ end
+ local git_object = buffer.git_object
+ if not git_object:is_tracked() then
+ local err = git_object:stage()
+ loop.await_fast_event()
+ if err then
+ console.debug(err, debug.traceback())
+ return
+ end
+ vim.cmd('edit')
+ return
+ end
+ local hunk = self:cursor_hunk()
+ if not hunk then
+ return
+ end
+ local err = git_object:stage_hunk(hunk)
+ if err then
+ console.debug(err, debug.traceback())
+ return
+ end
+ vim.cmd('edit')
+end
+
+function BufferHunks:unstage_all()
+ loop.await_fast_event()
+ local buffer = self.git_store:current()
+ if not buffer then
+ return
+ end
+ local err = buffer.git_object:unstage()
+ loop.await_fast_event()
+ if err then
+ console.debug(err, debug.traceback())
+ return
+ end
+ vim.cmd('edit')
+end
+
+function BufferHunks:reset_all()
+ loop.await_fast_event()
+ local buffer = self.git_store:current()
+ if not buffer then
+ return
+ end
+ local hunks = buffer.git_object.hunks
+ if not hunks and #hunks == 0 then
+ return
+ end
+ local err, lines = buffer.git_object:lines()
+ loop.await_fast_event()
+ if err then
+ return console.debug(err, debug.traceback())
+ end
+ buffer:set_lines(lines)
+ vim.cmd('update')
+end
+
+function BufferHunks:cursor_reset()
+ loop.await_fast_event()
+ local buffer = self.git_store:current()
+ if not buffer then
+ return
+ end
+ local window = Window:new(0)
+ local lnum = window:get_lnum()
+ local hunks = buffer.git_object.hunks
+ if not hunks then
+ return
+ end
+ if lnum == 1 then
+ local current_lines = buffer:get_lines()
+ if #hunks > 0 and #current_lines == 1 and current_lines[1] == '' then
+ local all_removes = true
+ for i = 1, #hunks do
+ local hunk = hunks[i]
+ if hunk.type ~= 'remove' then
+ all_removes = false
+ break
+ end
+ end
+ if all_removes then
+ self:reset_all()
+ end
+ end
+ end
+ local selected_hunk = nil
+ local selected_hunk_index = nil
+ for i = 1, #hunks do
+ local hunk = hunks[i]
+ if
+ (lnum >= hunk.start and lnum <= hunk.finish)
+ or (
+ hunk.start == 0
+ and hunk.finish == 0
+ and lnum - 1 == hunk.start
+ and lnum - 1 == hunk.finish
+ )
+ then
+ selected_hunk = hunk
+ selected_hunk_index = i
+ break
+ end
+ end
+ if selected_hunk then
+ local replaced_lines = {}
+ for i = 1, #selected_hunk.diff do
+ local line = selected_hunk.diff[i]
+ local is_line_removed = vim.startswith(line, '-')
+ if is_line_removed then
+ replaced_lines[#replaced_lines + 1] = string.sub(line, 2, -1)
+ end
+ end
+ local start = selected_hunk.start
+ local finish = selected_hunk.finish
+ if start and finish then
+ if selected_hunk.type == 'remove' then
+ buffer:set_lines(replaced_lines, start, finish)
+ else
+ buffer:set_lines(replaced_lines, start - 1, finish)
+ end
+ local new_lnum = start
+ if new_lnum < 1 then
+ new_lnum = 1
+ end
+ window:set_lnum(new_lnum)
+ table.remove(hunks, selected_hunk_index)
+ vim.cmd('update')
+ end
+ end
+end
+
+return BufferHunks
diff --git a/lua/vgit/features/LiveBlame.lua b/lua/vgit/features/LiveBlame.lua
new file mode 100644
index 00000000..73eee648
--- /dev/null
+++ b/lua/vgit/features/LiveBlame.lua
@@ -0,0 +1,134 @@
+local Buffer = require('vgit.core.Buffer')
+local Window = require('vgit.core.Window')
+local console = require('vgit.core.console')
+local live_blame_setting = require('vgit.settings.live_blame')
+local loop = require('vgit.core.loop')
+local virtual_text = require('vgit.core.virtual_text')
+
+local Feature = require('vgit.Feature')
+local LiveBlame = Feature:extend()
+
+function LiveBlame:new(git_store)
+ return setmetatable({
+ id = 1,
+ ns_id = vim.api.nvim_create_namespace(''),
+ git_store = git_store,
+ last_lnum = nil,
+ }, LiveBlame)
+end
+
+function LiveBlame:display(lnum, buffer, config, blame)
+ loop.await_fast_event()
+ if buffer:is_valid() then
+ local virt_text = live_blame_setting:get('format')(blame, config)
+ if type(virt_text) == 'string' then
+ pcall(virtual_text.add, buffer, self.ns_id, lnum - 1, 0, {
+ id = self.id,
+ virt_text = { { virt_text, 'GitComment' } },
+ virt_text_pos = 'eol',
+ hl_mode = 'combine',
+ })
+ end
+ end
+end
+
+function LiveBlame:hide(buffer)
+ loop.await_fast_event()
+ if buffer:is_valid() then
+ pcall(virtual_text.delete, buffer, self.ns_id, self.id)
+ end
+end
+
+LiveBlame.sync = loop.debounce(
+ loop.async(function(self)
+ loop.await_fast_event()
+ local window = Window:new(0)
+ local buffer = self.git_store:current()
+ if not buffer then
+ return
+ end
+ if buffer:editing() then
+ console.debug(
+ string.format('Buffer %s is being edited right now', buffer.bufnr)
+ )
+ return
+ end
+ if not self:is_buffer_valid(buffer) then
+ return
+ end
+ if not self:is_buffer_tracked(buffer) then
+ return
+ end
+ local lnum = window:get_lnum()
+ if self.last_lnum and self.last_lnum == lnum then
+ return
+ end
+ loop.await_fast_event()
+ local blame_err, blame = buffer.git_object:blame_line(lnum)
+ loop.await_fast_event()
+ if not buffer:is_valid() then
+ console.debug(
+ 'Buffer on which blame was live blame was being synced is no longer valid'
+ )
+ end
+ if not window:is_valid() then
+ console.debug(
+ 'Window on which blame was live blame was being synced is no longer valid'
+ )
+ return
+ end
+ local new_lnum = window:get_lnum()
+ if lnum ~= new_lnum then
+ console.debug(
+ string.format(
+ 'Suspending blame computation for %s user is currently on %s and not in %s',
+ buffer.bufnr,
+ new_lnum,
+ lnum
+ )
+ )
+ return
+ end
+ if blame_err then
+ console.debug(blame_err, debug.traceback())
+ return
+ end
+ loop.await_fast_event()
+ local config_err, config = buffer.git_object:config()
+ loop.await_fast_event()
+ if config_err then
+ console.debug(config_err, debug.traceback())
+ return
+ end
+ self:hide(buffer)
+ self:display(lnum, buffer, config, blame)
+ self.last_lnum = lnum
+ end),
+ live_blame_setting:get('debounce_ms')
+)
+
+function LiveBlame:desync(force)
+ loop.await_fast_event()
+ local window = Window:new(0)
+ local buffer = self.git_store:current()
+ if not buffer then
+ return
+ end
+ buffer = self.git_store:get(buffer)
+ local lnum = window:get_lnum()
+ if not force and self.last_lnum and self.last_lnum == lnum then
+ return
+ end
+ self:hide(buffer)
+end
+
+function LiveBlame:desync_all()
+ loop.await_fast_event()
+ local buffers = Buffer:list()
+ for i = 1, #buffers do
+ loop.await_fast_event()
+ self:hide(buffers[i])
+ end
+end
+
+return LiveBlame
diff --git a/lua/vgit/features/LiveGutter.lua b/lua/vgit/features/LiveGutter.lua
new file mode 100644
index 00000000..8704f02f
--- /dev/null
+++ b/lua/vgit/features/LiveGutter.lua
@@ -0,0 +1,134 @@
+local signs_setting = require('vgit.settings.signs')
+local live_gutter_setting = require('vgit.settings.live_gutter')
+local sign = require('vgit.core.sign')
+local loop = require('vgit.core.loop')
+local Buffer = require('vgit.core.Buffer')
+local console = require('vgit.core.console')
+local Feature = require('vgit.Feature')
+
+local LiveGutter = Feature:extend()
+
+function LiveGutter:new(git_store)
+ return setmetatable({ git_store = git_store }, LiveGutter)
+end
+
+LiveGutter.sync = loop.debounce(
+ loop.async(function(self, buffer)
+ loop.await_fast_event()
+ local err = buffer.git_object:live_hunks(buffer:get_lines())
+ loop.await_fast_event()
+ if err then
+ console.debug(err, debug.traceback())
+ return
+ end
+ self:hide(buffer)
+ self:display(buffer)
+ end),
+ live_gutter_setting:get('debounce_ms')
+)
+
+LiveGutter.resync = loop.debounce(
+ loop.async(function(self)
+ loop.await_fast_event()
+ local buffer = self.git_store:current()
+ if buffer then
+ self:sync(buffer)
+ end
+ end),
+ live_gutter_setting:get('debounce_ms')
+)
+
+function LiveGutter:watch(buffer)
+ buffer.watcher = loop.watch(
+ buffer.filename,
+ loop.async(function(err, _, events)
+ loop.await_fast_event()
+ if err then
+ console.debug(
+ string.format('Error encountered while watching %s', buffer.filename)
+ )
+ return
+ end
+ if events.rename then
+ -- Deleting a buffer also triggers this event, so we need to check if the buffer is still valid
+ if buffer:is_valid() then
+ buffer:sync()
+ self:sync(buffer)
+ end
+ end
+ end)
+ )
+end
+
+function LiveGutter:display(buffer)
+ if not live_gutter_setting:get('enabled') then
+ return
+ end
+ loop.await_fast_event()
+ local hunks = buffer.git_object.hunks
+ if not hunks then
+ return
+ end
+ for i = 1, #hunks do
+ loop.await_fast_event()
+ local hunk = hunks[i]
+ for j = hunk.start, hunk.finish do
+ sign.place(
+ buffer,
+ (hunk.type == 'remove' and j == 0) and 1 or j,
+ signs_setting:get('usage').main[hunk.type],
+ signs_setting:get('priority')
+ )
+ loop.await_fast_event()
+ end
+ loop.await_fast_event()
+ end
+end
+
+function LiveGutter:hide(buffer)
+ loop.await_fast_event()
+ sign.unplace(buffer)
+end
+
+function LiveGutter:attach()
+ loop.await_fast_event()
+ local buffer = Buffer:new(0)
+ buffer:sync_git()
+ if not self:is_inside_git_dir(buffer) then
+ return
+ end
+ if not self:is_buffer_valid(buffer) then
+ return
+ end
+ if not self:is_buffer_in_disk(buffer) then
+ return
+ end
+ if self:is_buffer_ignored(buffer) then
+ return
+ end
+ self.git_store:add(buffer)
+ vim.api.nvim_buf_attach(buffer.bufnr, false, {
+ on_lines = loop.async(function(_, _, _, _, p_lnum, n_lnum, byte_count)
+ if p_lnum == n_lnum and byte_count == 0 then
+ return
+ end
+ loop.await_fast_event()
+ self:sync(buffer)
+ end),
+ on_reload = loop.async(function()
+ loop.await_fast_event()
+ self:sync(buffer)
+ end),
+ })
+ self:sync(buffer)
+ self:watch(buffer)
+end
+
+function LiveGutter:detach()
+ loop.await_fast_event()
+ self.git_store:clean(function(buffer)
+ loop.unwatch(buffer.watcher)
+ end)
+end
+
+return LiveGutter
diff --git a/lua/vgit/features/ProjectHunksList.lua b/lua/vgit/features/ProjectHunksList.lua
new file mode 100644
index 00000000..8d88b4b8
--- /dev/null
+++ b/lua/vgit/features/ProjectHunksList.lua
@@ -0,0 +1,75 @@
+local fs = require('vgit.core.fs')
+local console = require('vgit.core.console')
+local loop = require('vgit.core.loop')
+local Git = require('vgit.cli.Git')
+local Object = require('vgit.core.Object')
+
+local ProjectHunksList = Object:extend()
+
+function ProjectHunksList:new()
+ return setmetatable({
+ git = Git:new(),
+ }, ProjectHunksList)
+end
+
+function ProjectHunksList:fetch()
+ local git = self.git
+ local entries = {}
+ local changed_files_err, changed_files = git:ls_changed()
+ loop.await_fast_event()
+ if changed_files_err then
+ return console.debug(changed_files_err, debug.traceback())
+ end
+ if #changed_files == 0 then
+ return entries
+ end
+ for i = 1, #changed_files do
+ local file = changed_files[i]
+ local filename = file.filename
+ local status = file.status
+ local hunks_err, hunks
+ if status:has_both('??') then
+ local show_err, lines = fs.read_file(filename)
+ if not show_err then
+ hunks = git:untracked_hunks(lines)
+ else
+ console.debug(show_err, debug.traceback())
+ end
+ else
+ hunks_err, hunks = git:index_hunks(filename)
+ end
+ loop.await_fast_event()
+ if not hunks_err then
+ for j = 1, #hunks do
+ local hunk = hunks[j]
+ entries[#entries + 1] = {
+ text = string.format(
+ 'lines: [%s..%s] +%s -%s',
+ hunk.start,
+ hunk.finish,
+ hunk.stat.added,
+ hunk.stat.removed,
+ hunk.stat.modified
+ ),
+ filename = filename,
+ lnum = hunk.start,
+ col = 0,
+ }
+ end
+ else
+ console.debug(hunks_err, debug.traceback())
+ end
+ end
+ return entries
+end
+
+function ProjectHunksList:show_as_quickfix(entries)
+ if #entries == 0 then
+ console.info('No changes found in working directory')
+ return entries
+ end
+ vim.fn.setqflist(entries, 'r')
+ vim.cmd('copen')
+end
+
+return ProjectHunksList
diff --git a/lua/vgit/features/scenes/DiffScene.lua b/lua/vgit/features/scenes/DiffScene.lua
new file mode 100644
index 00000000..8e167be2
--- /dev/null
+++ b/lua/vgit/features/scenes/DiffScene.lua
@@ -0,0 +1,146 @@
+local utils = require('vgit.core.utils')
+local Scene = require('vgit.ui.Scene')
+local loop = require('vgit.core.loop')
+local dimensions = require('vgit.ui.dimensions')
+local CodeComponent = require('vgit.ui.components.CodeComponent')
+local CodeScene = require('vgit.ui.abstract_scenes.CodeScene')
+local console = require('vgit.core.console')
+local fs = require('vgit.core.fs')
+local Hunk = require('vgit.cli.models.Hunk')
+
+local DiffScene = CodeScene:extend()
+
+function DiffScene:new(...)
+ local this = CodeScene:new(...)
+ this.cache = {
+ buffer = nil,
+ title = nil,
+ options = nil,
+ err = false,
+ data = nil,
+ }
+ return setmetatable(this, DiffScene)
+end
+
+function DiffScene:fetch()
+ local cache = self.cache
+ local buffer = cache.buffer
+ local read_file_err, lines = fs.read_file(buffer.filename)
+ if read_file_err then
+ console.debug(read_file_err, debug.traceback())
+ cache.err = read_file_err
+ return self
+ end
+ local hunks = buffer.git_object.hunks
+ if not hunks then
+ -- This scenario will occur if current buffer has not computer it's live hunk yet.
+ local hunks_err, calculated_hunks = buffer.git_object:live_hunks(lines)
+ if hunks_err then
+ console.debug(hunks_err, debug.traceback())
+ cache.err = hunks_err
+ return self
+ end
+ hunks = calculated_hunks
+ end
+ cache.data = {
+ filename = buffer.filename,
+ filetype = buffer:filetype(),
+ dto = self:generate_diff(hunks, lines),
+ selected_hunk = self.buffer_hunks:cursor_hunk() or Hunk:new(),
+ }
+ return self
+end
+
+function DiffScene:get_unified_scene_options(options)
+ return {
+ current = CodeComponent:new(utils.object_assign({
+ config = {
+ win_options = {
+ cursorbind = true,
+ scrollbind = true,
+ cursorline = true,
+ },
+ window_props = {
+ height = dimensions.global_height(),
+ width = dimensions.global_width(),
+ },
+ },
+ }, options)),
+ }
+end
+
+function DiffScene:get_split_scene_options(options)
+ return {
+ previous = CodeComponent:new(utils.object_assign({
+ config = {
+ win_options = {
+ cursorbind = true,
+ scrollbind = true,
+ cursorline = true,
+ },
+ window_props = {
+ height = dimensions.global_height(),
+ width = math.floor(dimensions.global_width() / 2),
+ },
+ },
+ }, options)),
+ current = CodeComponent:new(utils.object_assign({
+ config = {
+ win_options = {
+ cursorbind = true,
+ scrollbind = true,
+ cursorline = true,
+ },
+ window_props = {
+ height = dimensions.global_height(),
+ width = math.floor(dimensions.global_width() / 2),
+ col = math.floor(dimensions.global_width() / 2),
+ },
+ },
+ }, options)),
+ }
+end
+
+function DiffScene:show(title, options)
+ local buffer = self.git_store:current()
+ if not buffer then
+ console.info('Buffer has no hunks')
+ return false
+ end
+ if buffer:editing() then
+ console.debug(
+ string.format('Buffer %s is being edited right now', buffer.bufnr)
+ )
+ return
+ end
+ local cache = self.cache
+ cache.buffer = buffer
+ cache.title = title
+ cache.options = options
+ self:fetch()
+ loop.await_fast_event()
+ if cache.err then
+ console.error(cache.err)
+ return false
+ end
+ if #cache.data.dto.hunks == 0 then
+ console.info('No hunks found')
+ return false
+ end
+ -- selected_hunk must always be called before creating the scene.
+ local _, selected_hunk = self.buffer_hunks:cursor_hunk()
+ self.scene = Scene:new(self:get_scene_options(options)):mount()
+ local data = cache.data
+ self
+ :set_title(title, {
+ filename = data.filename,
+ filetype = data.filetype,
+ stat = data.dto.stat,
+ })
+ :make()
+ :set_cursor_on_mark(selected_hunk, 'center')
+ :paint()
+ return true
+end
+
+return DiffScene
diff --git a/lua/vgit/features/scenes/GutterBlameScene.lua b/lua/vgit/features/scenes/GutterBlameScene.lua
new file mode 100644
index 00000000..9986fec1
--- /dev/null
+++ b/lua/vgit/features/scenes/GutterBlameScene.lua
@@ -0,0 +1,154 @@
+local CodeDTO = require('vgit.core.CodeDTO')
+local loop = require('vgit.core.loop')
+local utils = require('vgit.core.utils')
+local Scene = require('vgit.ui.Scene')
+local dimensions = require('vgit.ui.dimensions')
+local PresentationalComponent = require(
+ 'vgit.ui.components.PresentationalComponent'
+)
+local CodeComponent = require('vgit.ui.components.CodeComponent')
+local CodeScene = require('vgit.ui.abstract_scenes.CodeScene')
+local console = require('vgit.core.console')
+
+local GutterBlameScene = CodeScene:extend()
+
+function GutterBlameScene:new(...)
+ return setmetatable(CodeScene:new(...), GutterBlameScene)
+end
+
+function GutterBlameScene:fetch()
+ local cache = self.cache
+ local buffer = cache.buffer
+ local blames_err, blames = buffer.git_object:blames()
+ if blames_err then
+ console.debug(blames_err, debug.traceback())
+ cache.err = blames_err
+ return self
+ end
+ loop.await_fast_event()
+ cache.data = {
+ filename = buffer.filename,
+ filetype = buffer:filetype(),
+ dto = CodeDTO:new({ lines = buffer:get_lines() }),
+ blames = blames,
+ }
+ return self
+end
+
+function GutterBlameScene:get_blame_line(blame)
+ local time = os.difftime(os.time(), blame.author_time) / (24 * 60 * 60)
+ local time_format = string.format('%s days ago', utils.round(time))
+ local time_divisions = {
+ { 24, 'hours' },
+ { 60, 'minutes' },
+ { 60, 'seconds' },
+ }
+ local division_counter = 1
+ while time < 1 and division_counter ~= #time_divisions do
+ local division = time_divisions[division_counter]
+ time = time * division[1]
+ time_format = string.format('%s %s ago', utils.round(time), division[2])
+ division_counter = division_counter + 1
+ end
+ if blame.committed then
+ return string.format(
+ '%s (%s) • %s',
+ blame.author,
+ time_format,
+ blame.committed and blame.commit_message or 'Uncommitted changes'
+ )
+ end
+ return 'Uncommitted changes'
+end
+
+function GutterBlameScene:get_scene_options(options)
+ return {
+ blames = PresentationalComponent:new(utils.object_assign({
+ config = {
+ win_options = {
+ cursorbind = true,
+ scrollbind = true,
+ cursorline = true,
+ },
+ window_props = {
+ height = dimensions.global_height(),
+ width = math.floor(dimensions.global_width() * 0.4),
+ },
+ },
+ }, options)),
+ current = CodeComponent:new(utils.object_assign({
+ config = {
+ win_options = {
+ cursorbind = true,
+ scrollbind = true,
+ cursorline = true,
+ },
+ window_props = {
+ height = dimensions.global_height(),
+ width = math.floor(dimensions.global_width() * 0.6),
+ col = math.floor(dimensions.global_width() * 0.4),
+ },
+ },
+ }, options)),
+ }
+end
+
+function GutterBlameScene:make_blames()
+ local lines = {}
+ local blames = self.cache.data.blames
+ for i = 1, #blames do
+ lines[#lines + 1] = self:get_blame_line(blames[i])
+ end
+ self.scene.components.blames:set_lines(lines)
+ return self
+end
+
+function GutterBlameScene:set_title(title, options)
+ self.scene.components.blames:set_title(title, options)
+ return self
+end
+
+function GutterBlameScene:notify(text)
+ self.scene.components.blames:notify(text)
+ return self
+end
+
+function GutterBlameScene:show(title, options)
+ local buffer = self.git_store:current()
+ if not buffer then
+ console.info('Buffer has no blames')
+ return false
+ end
+ local git_object = buffer.git_object
+ if git_object:tracked_filename() == '' then
+ console.info('Buffer has no blames')
+ return false
+ end
+ if not git_object:is_in_remote() then
+ console.info('Buffer has no blames')
+ return false
+ end
+ local cache = self.cache
+ cache.buffer = buffer
+ cache.title = title
+ cache.options = options
+ if cache.err then
+ console.error(cache.err)
+ return false
+ end
+ self:fetch()
+ loop.await_fast_event()
+ self.scene = Scene:new(self:get_scene_options(options)):mount()
+ local data = cache.data
+ self
+ :set_title(title, {
+ filename = data.filename,
+ filetype = data.filetype,
+ })
+ :make()
+ :make_blames()
+ :paint()
+ return true
+end
+
+return GutterBlameScene
diff --git a/lua/vgit/features/scenes/HistoryScene.lua b/lua/vgit/features/scenes/HistoryScene.lua
new file mode 100644
index 00000000..3dc6b4e3
--- /dev/null
+++ b/lua/vgit/features/scenes/HistoryScene.lua
@@ -0,0 +1,205 @@
+local utils = require('vgit.core.utils')
+local loop = require('vgit.core.loop')
+local CodeComponent = require('vgit.ui.components.CodeComponent')
+local TableComponent = require('vgit.ui.components.TableComponent')
+local CodeDataScene = require('vgit.ui.abstract_scenes.CodeDataScene')
+local Scene = require('vgit.ui.Scene')
+local dimensions = require('vgit.ui.dimensions')
+local console = require('vgit.core.console')
+
+local HistoryScene = CodeDataScene:extend()
+
+function HistoryScene:new(...)
+ return setmetatable(CodeDataScene:new(...), HistoryScene)
+end
+
+function HistoryScene:fetch(selected)
+ selected = selected or 1
+ local cache = self.cache
+ local data = cache.data
+ local buffer = cache.buffer
+ local git_object = buffer.git_object
+ local lines, hunks
+ local err, logs
+ if data then
+ logs = data.logs
+ else
+ err, logs = git_object:logs()
+ end
+ if err then
+ console.debug(err, debug.traceback())
+ cache.err = err
+ return self
+ end
+ local log = logs[selected]
+ if not log then
+ err = { 'Failed to access logs' }
+ console.debug(err, debug.traceback())
+ cache.err = err
+ return self
+ end
+ local parent_hash = log.parent_hash
+ local commit_hash = log.commit_hash
+ err, hunks = git_object:remote_hunks(parent_hash, commit_hash)
+ if err then
+ console.debug(err, debug.traceback())
+ cache.err = err
+ return self
+ end
+ err, lines = git_object:lines(commit_hash)
+ if err then
+ console.debug(err, debug.traceback())
+ cache.err = err
+ return self
+ end
+ loop.await_fast_event()
+ cache.data = {
+ filename = buffer.filename,
+ filetype = buffer:filetype(),
+ logs = logs,
+ dto = self:generate_diff(hunks, lines),
+ }
+ return self
+end
+
+function HistoryScene:get_unified_scene_options(options)
+ local table_height = math.floor(dimensions.global_height() * 0.15)
+ return {
+ current = CodeComponent:new(utils.object_assign({
+ config = {
+ win_options = {
+ cursorbind = true,
+ scrollbind = true,
+ cursorline = true,
+ },
+ window_props = {
+ height = dimensions.global_height() - table_height,
+ row = table_height,
+ },
+ },
+ }, options)),
+ table = TableComponent:new(utils.object_assign({
+ header = { 'Revision', 'Author Name', 'Commit Hash', 'Time', 'Summary' },
+ config = {
+ window_props = {
+ height = table_height,
+ row = 0,
+ },
+ },
+ }, options)),
+ }
+end
+
+function HistoryScene:get_split_scene_options(options)
+ local table_height = math.floor(dimensions.global_height() * 0.15)
+ return {
+ previous = CodeComponent:new(utils.object_assign({
+ config = {
+ win_options = {
+ cursorbind = true,
+ scrollbind = true,
+ cursorline = true,
+ },
+ window_props = {
+ height = dimensions.global_height() - table_height,
+ width = math.floor(dimensions.global_width() / 2),
+ row = table_height,
+ },
+ },
+ }, options)),
+ current = CodeComponent:new(utils.object_assign({
+ config = {
+ win_options = {
+ cursorbind = true,
+ scrollbind = true,
+ cursorline = true,
+ },
+ window_props = {
+ height = dimensions.global_height() - table_height,
+ width = math.floor(dimensions.global_width() / 2),
+ col = math.floor(dimensions.global_width() / 2),
+ row = table_height,
+ },
+ },
+ }, options)),
+ table = TableComponent:new(utils.object_assign({
+ header = { 'Revision', 'Author Name', 'Commit Hash', 'Time', 'Summary' },
+ config = {
+ window_props = {
+ height = table_height,
+ row = 0,
+ },
+ },
+ }, options)),
+ }
+end
+
+function HistoryScene:table_change()
+ self:update()
+end
+
+function HistoryScene:make_table()
+ self.scene.components.table
+ :unlock()
+ :make_rows(self.cache.data.logs, function(log)
+ return {
+ log.revision,
+ log.author_name or '',
+ log.commit_hash or '',
+ (log.timestamp and os.date('%Y-%m-%d', tonumber(log.timestamp))) or '',
+ log.summary or '',
+ }
+ end)
+ :set_keymap('n', 'j', 'on_j')
+ :set_keymap('n', 'J', 'on_j')
+ :set_keymap('n', 'k', 'on_k')
+ :set_keymap('n', 'K', 'on_k')
+ :set_keymap('n', '', 'on_enter')
+ :focus()
+ :lock()
+ return self
+end
+
+function HistoryScene:show(title, options)
+ local buffer = self.git_store:current()
+ if not buffer then
+ console.info('Buffer has no history')
+ return false
+ end
+ local git_object = buffer.git_object
+ if git_object:tracked_filename() == '' then
+ loop.await_fast_event()
+ console.info('Buffer has no history')
+ return false
+ end
+ if not git_object:is_in_remote() then
+ loop.await_fast_event()
+ console.info('Buffer has no history')
+ return false
+ end
+ local cache = self.cache
+ cache.title = title
+ cache.options = options
+ cache.buffer = buffer
+ self:fetch().scene = Scene:new(self:get_scene_options(options)):mount()
+ if cache.err then
+ console.error(cache.err)
+ return false
+ end
+ local data = cache.data
+ self
+ :set_title(title, {
+ filename = data.filename,
+ filetype = data.filetype,
+ stat = data.dto.stat,
+ })
+ :make()
+ :make_table()
+ :set_cursor_on_mark(1)
+ :paint()
+ -- Must be after initial fetch
+ cache.last_selected = 1
+ return true
+end
+
+return HistoryScene
diff --git a/lua/vgit/features/scenes/LineBlameScene.lua b/lua/vgit/features/scenes/LineBlameScene.lua
new file mode 100644
index 00000000..6bf07797
--- /dev/null
+++ b/lua/vgit/features/scenes/LineBlameScene.lua
@@ -0,0 +1,135 @@
+local Window = require('vgit.core.Window')
+local loop = require('vgit.core.loop')
+local utils = require('vgit.core.utils')
+local Scene = require('vgit.ui.Scene')
+local PopupComponent = require('vgit.ui.components.PopupComponent')
+local CodeScene = require('vgit.ui.abstract_scenes.CodeScene')
+local console = require('vgit.core.console')
+
+local LineBlameScene = CodeScene:extend()
+
+function LineBlameScene:new(...)
+ return setmetatable(CodeScene:new(...), LineBlameScene)
+end
+
+function LineBlameScene:fetch()
+ local cache = self.cache
+ local buffer = cache.buffer
+ loop.await_fast_event()
+ local blame_err, blame = buffer.git_object:blame_line(
+ Window:new(0):get_lnum()
+ )
+ if blame_err then
+ console.debug(blame_err, debug.traceback())
+ cache.err = blame_err
+ return self
+ end
+ cache.data = blame
+ return self
+end
+
+function LineBlameScene:create_uncommitted_lines(blame)
+ return {
+ string.format('%sLine #%s', ' ', blame.lnum),
+ string.format('%s%s', ' ', 'Uncommitted changes'),
+ string.format('%s%s -> %s', ' ', blame.parent_hash, blame.commit_hash),
+ }
+end
+
+function LineBlameScene:create_committed_lines(blame)
+ local max_line_length = 88
+ local time = os.difftime(os.time(), blame.author_time) / (24 * 60 * 60)
+ local time_format = string.format('%s days ago', utils.round(time))
+ local time_divisions = {
+ { 24, 'hours' },
+ { 60, 'minutes' },
+ { 60, 'seconds' },
+ }
+ local division_counter = 1
+ while time < 1 and division_counter ~= #time_divisions do
+ local division = time_divisions[division_counter]
+ time = time * division[1]
+ time_format = string.format('%s %s ago', utils.round(time), division[2])
+ division_counter = division_counter + 1
+ end
+ local commit_message = blame.commit_message
+ if #commit_message > max_line_length then
+ commit_message = commit_message:sub(1, max_line_length) .. '...'
+ end
+ return {
+ string.format('%sLine #%s', ' ', blame.lnum),
+ string.format(' %s (%s)', blame.author, blame.author_mail),
+ string.format(' %s (%s)', time_format, os.date('%c', blame.author_time)),
+ string.format('%s%s', ' ', commit_message),
+ string.format('%s%s -> %s', ' ', blame.parent_hash, blame.commit_hash),
+ }
+end
+
+function LineBlameScene:get_scene_options(options)
+ return {
+ current = PopupComponent:new(utils.object_assign({
+ config = {
+ window_props = {
+ height = 10,
+ width = 50,
+ },
+ },
+ }, options)),
+ }
+end
+
+function LineBlameScene:make_lines()
+ local get_width = function(lines)
+ local max_line_width = 50
+ for i = 1, #lines do
+ local line = lines[i]
+ if #line > max_line_width then
+ max_line_width = #line + 1
+ end
+ end
+ return max_line_width
+ end
+ local component = self.scene.components.current
+ local blame = self.cache.data
+ if not blame.committed then
+ local uncommitted_lines = self:create_uncommitted_lines(blame)
+ component
+ :set_lines(uncommitted_lines)
+ :set_height(#uncommitted_lines)
+ :set_width(get_width(uncommitted_lines))
+ return self
+ end
+ local committed_lines = self:create_committed_lines(blame)
+ component
+ :set_lines(committed_lines)
+ :set_height(#committed_lines)
+ :set_width(get_width(committed_lines))
+end
+
+function LineBlameScene:show(options)
+ local buffer = self.git_store:current()
+ if not buffer then
+ return false
+ end
+ local git_object = buffer.git_object
+ if git_object:tracked_filename() == '' then
+ return false
+ end
+ if not git_object:is_in_remote() then
+ return false
+ end
+ local cache = self.cache
+ cache.buffer = buffer
+ cache.options = options
+ self:fetch()
+ loop.await_fast_event()
+ if cache.err then
+ console.error(cache.err)
+ return false
+ end
+ self.scene = Scene:new(self:get_scene_options(options)):mount()
+ self:make_lines()
+ return true
+end
+
+return LineBlameScene
diff --git a/lua/vgit/features/scenes/ProjectDiffScene.lua b/lua/vgit/features/scenes/ProjectDiffScene.lua
new file mode 100644
index 00000000..c749699e
--- /dev/null
+++ b/lua/vgit/features/scenes/ProjectDiffScene.lua
@@ -0,0 +1,326 @@
+local Window = require('vgit.core.Window')
+local icons = require('vgit.core.icons')
+local utils = require('vgit.core.utils')
+local loop = require('vgit.core.loop')
+local CodeComponent = require('vgit.ui.components.CodeComponent')
+local TableComponent = require('vgit.ui.components.TableComponent')
+local CodeDataScene = require('vgit.ui.abstract_scenes.CodeDataScene')
+local Scene = require('vgit.ui.Scene')
+local dimensions = require('vgit.ui.dimensions')
+local console = require('vgit.core.console')
+local fs = require('vgit.core.fs')
+local Diff = require('vgit.Diff')
+
+local ProjectDiffScene = CodeDataScene:extend()
+
+function ProjectDiffScene:new(...)
+ return setmetatable(CodeDataScene:new(...), ProjectDiffScene)
+end
+
+function ProjectDiffScene:fetch(selected)
+ selected = selected or 1
+ local cache = self.cache
+ local git = self.git
+ local changed_files_err, changed_files = git:ls_changed()
+ if changed_files_err then
+ console.debug(changed_files_err, debug.traceback())
+ cache.err = changed_files_err
+ return self
+ end
+ if #changed_files == 0 then
+ cache.data = {
+ changed_files = changed_files,
+ selected = selected,
+ }
+ return self
+ end
+ local file = changed_files[selected]
+ if not file then
+ selected = #changed_files
+ file = changed_files[selected]
+ end
+ local filename = file.filename
+ local status = file.status
+ local lines_err, lines
+ if status:has('D ') then
+ lines_err, lines = git:show(filename, 'HEAD')
+ elseif status:has(' D') then
+ lines_err, lines = git:show(git:tracked_filename(filename))
+ else
+ lines_err, lines = fs.read_file(filename)
+ end
+ if lines_err then
+ console.debug(lines_err, debug.traceback())
+ cache.err = lines_err
+ return self
+ end
+ local hunks_err, hunks
+ if status:has_both('??') then
+ hunks = git:untracked_hunks(lines)
+ elseif status:has_either('DD') then
+ hunks = git:deleted_hunks(lines)
+ else
+ hunks_err, hunks = git:index_hunks(filename)
+ end
+ if hunks_err then
+ console.debug(hunks_err, debug.traceback())
+ cache.err = hunks_err
+ return self
+ end
+ local dto
+ if self.layout_type == 'unified' then
+ if status:has_either('DD') then
+ dto = Diff:new(hunks):deleted_unified(lines)
+ else
+ dto = Diff:new(hunks):unified(lines)
+ end
+ else
+ if status:has_either('DD') then
+ dto = Diff:new(hunks):deleted_split(lines)
+ else
+ dto = Diff:new(hunks):split(lines)
+ end
+ end
+ cache.data = {
+ filename = filename,
+ filetype = fs.detect_filetype(filename),
+ changed_files = changed_files,
+ dto = dto,
+ selected = selected,
+ }
+ return self
+end
+
+function ProjectDiffScene:get_unified_scene_options(options)
+ local table_height = math.floor(dimensions.global_height() * 0.15)
+ return {
+ current = CodeComponent:new(utils.object_assign({
+ config = {
+ win_options = {
+ cursorbind = true,
+ scrollbind = true,
+ cursorline = true,
+ },
+ window_props = {
+ height = dimensions.global_height() - table_height,
+ row = table_height,
+ },
+ },
+ }, options)),
+ table = TableComponent:new(utils.object_assign({
+ header = { 'Filename', 'Status' },
+ config = {
+ window_props = {
+ height = table_height,
+ row = 0,
+ },
+ },
+ }, options)),
+ }
+end
+
+function ProjectDiffScene:get_split_scene_options(options)
+ local table_height = math.floor(dimensions.global_height() * 0.15)
+ return {
+ previous = CodeComponent:new(utils.object_assign({
+ config = {
+ win_options = {
+ cursorbind = true,
+ scrollbind = true,
+ cursorline = true,
+ },
+ window_props = {
+ height = dimensions.global_height() - table_height,
+ width = math.floor(dimensions.global_width() / 2),
+ row = table_height,
+ },
+ },
+ }, options)),
+ current = CodeComponent:new(utils.object_assign({
+ config = {
+ win_options = {
+ cursorbind = true,
+ scrollbind = true,
+ cursorline = true,
+ },
+ window_props = {
+ height = dimensions.global_height() - table_height,
+ width = math.floor(dimensions.global_width() / 2),
+ col = math.floor(dimensions.global_width() / 2),
+ row = table_height,
+ },
+ },
+ }, options)),
+ table = TableComponent:new(utils.object_assign({
+ header = { 'Filename', 'Status' },
+ config = {
+ window_props = {
+ height = table_height,
+ row = 0,
+ },
+ },
+ }, options)),
+ }
+end
+
+function ProjectDiffScene:run_command(command)
+ loop.await_fast_event()
+ self:reset()
+ local cache = self.cache
+ local components = self.scene.components
+ local table = components.table
+ loop.await_fast_event()
+ local selected = table:get_lnum()
+ local filename = cache.data.changed_files[selected].filename
+ if type(command) == 'function' then
+ command(filename)
+ end
+ if cache.err then
+ console.error(cache.err)
+ return self
+ end
+ self:fetch(selected)
+ loop.await_fast_event()
+ if cache.err then
+ console.error(cache.err)
+ return self
+ end
+ if #cache.data.changed_files == 0 then
+ return self:hide()
+ end
+ self
+ :set_title(cache.title, {
+ filename = cache.data.filename,
+ filetype = cache.data.filetype,
+ stat = cache.data.dto.stat,
+ })
+ :make()
+ :make_table()
+ :set_cursor_on_mark(1)
+ :paint()
+end
+
+ProjectDiffScene.git_reset = loop.debounce(
+ loop.async(function(self)
+ self:run_command(function(filename)
+ return self.git:reset(filename)
+ end)
+ end),
+ 50
+)
+
+ProjectDiffScene.git_stage = loop.debounce(
+ loop.async(function(self)
+ self:run_command(function(filename)
+ return self.git:stage_file(filename)
+ end)
+ end),
+ 50
+)
+
+ProjectDiffScene.git_unstage = loop.debounce(
+ loop.async(function(self)
+ self:run_command(function(filename)
+ return self.git:unstage_file(filename)
+ end)
+ end),
+ 50
+)
+
+function ProjectDiffScene:open_file()
+ local table = self.scene.components.table
+ loop.await_fast_event()
+ local selected = table:get_lnum()
+ if self.cache.last_selected == selected then
+ local data = self.cache.data
+ local filename = data.changed_files[selected].filename
+ self:hide()
+ vim.cmd(string.format('e %s', filename))
+ local mark = data.dto.marks[1]
+ local lnum = mark and mark.start
+ if lnum then
+ Window:new(0):set_lnum(lnum):call(function()
+ vim.cmd('norm! zz')
+ end)
+ end
+ return self
+ end
+ self:update(selected)
+end
+
+ProjectDiffScene.refresh = loop.debounce(
+ loop.async(function(self)
+ self:run_command()
+ end),
+ 50
+)
+
+function ProjectDiffScene:make_table()
+ self.scene.components.table
+ :unlock()
+ :make_rows(self.cache.data.changed_files, function(file)
+ local icon, icon_hl = icons.file_icon(file.filename, file.filetype)
+ return {
+ {
+ icon_before = {
+ icon = icon,
+ hl = icon_hl,
+ },
+ text = file.filename,
+ },
+ file.status:to_string(),
+ }
+ end)
+ :set_keymap('n', 'j', 'on_j')
+ :set_keymap('n', 'J', 'on_j')
+ :set_keymap('n', 'k', 'on_k')
+ :set_keymap('n', 'K', 'on_k')
+ :set_keymap('n', '', 'on_enter')
+ :focus()
+ :lock()
+ return self
+end
+
+function ProjectDiffScene:show(title, options)
+ local is_inside_git_dir = self.git:is_inside_git_dir()
+ if not is_inside_git_dir then
+ console.info('Project has no git folder')
+ console.debug(
+ 'project_diff_preview is disabled, we are not in git store anymore'
+ )
+ return false
+ end
+ self:hide()
+ local cache = self.cache
+ cache.title = title
+ cache.options = options
+ self:fetch()
+ loop.await_fast_event()
+ if not cache.err and cache.data and #cache.data.changed_files == 0 then
+ console.info('No changes found')
+ return false
+ end
+ if cache.err then
+ console.error(cache.err)
+ return false
+ end
+ self.scene = Scene:new(self:get_scene_options(options)):mount()
+ local data = cache.data
+ local filename = data.filename
+ local filetype = data.filetype
+ self
+ :set_title(title, {
+ filename = filename,
+ filetype = filetype,
+ stat = data.dto.stat,
+ })
+ :make()
+ :make_table()
+ :set_cursor_on_mark(1)
+ :paint()
+ -- Must be after initial fetch
+ cache.last_selected = 1
+ return true
+end
+
+return ProjectDiffScene
diff --git a/lua/vgit/features/scenes/ProjectHunksScene.lua b/lua/vgit/features/scenes/ProjectHunksScene.lua
new file mode 100644
index 00000000..77c685a9
--- /dev/null
+++ b/lua/vgit/features/scenes/ProjectHunksScene.lua
@@ -0,0 +1,284 @@
+local icons = require('vgit.core.icons')
+local Window = require('vgit.core.Window')
+local loop = require('vgit.core.loop')
+local utils = require('vgit.core.utils')
+local CodeComponent = require('vgit.ui.components.CodeComponent')
+local TableComponent = require('vgit.ui.components.TableComponent')
+local CodeDataScene = require('vgit.ui.abstract_scenes.CodeDataScene')
+local Scene = require('vgit.ui.Scene')
+local dimensions = require('vgit.ui.dimensions')
+local console = require('vgit.core.console')
+local fs = require('vgit.core.fs')
+local Diff = require('vgit.Diff')
+
+local ProjectHunksScene = CodeDataScene:extend()
+
+function ProjectHunksScene:new(...)
+ return setmetatable(CodeDataScene:new(...), ProjectHunksScene)
+end
+
+function ProjectHunksScene:fetch()
+ local git = self.git
+ local cache = self.cache
+ cache.entries = {}
+ local entries = cache.entries
+ local changed_files_err, changed_files = git:ls_changed()
+ if changed_files_err then
+ console.debug(changed_files_err, debug.traceback())
+ cache.err = changed_files_err
+ return self
+ end
+ if #changed_files == 0 then
+ console.debug({ 'No changes found' }, debug.traceback())
+ return self
+ end
+ for i = 1, #changed_files do
+ local file = changed_files[i]
+ local filename = file.filename
+ local status = file.status
+ local lines_err, lines
+ if status:has('D ') then
+ lines_err, lines = git:show(filename, 'HEAD')
+ elseif status:has(' D') then
+ lines_err, lines = git:show(git:tracked_filename(filename))
+ else
+ lines_err, lines = fs.read_file(filename)
+ end
+ if lines_err then
+ console.debug(lines_err, debug.traceback())
+ cache.err = lines_err
+ return self
+ end
+ local hunks_err, hunks
+ if status:has_both('??') then
+ hunks = git:untracked_hunks(lines)
+ elseif status:has_either('DD') then
+ hunks = git:deleted_hunks(lines)
+ else
+ hunks_err, hunks = git:index_hunks(filename)
+ end
+ if hunks_err then
+ console.debug(hunks_err, debug.traceback())
+ cache.err = hunks_err
+ return self
+ end
+ local dto
+ if self.layout_type == 'unified' then
+ if status:has_either('DD') then
+ dto = Diff:new(hunks):deleted_unified(lines)
+ else
+ dto = Diff:new(hunks):unified(lines)
+ end
+ else
+ if status:has_either('DD') then
+ dto = Diff:new(hunks):deleted_split(lines)
+ else
+ dto = Diff:new(hunks):split(lines)
+ end
+ end
+ if not hunks_err then
+ for j = 1, #hunks do
+ local hunk = hunks[j]
+ entries[#entries + 1] = {
+ hunk = hunk,
+ hunks = hunks,
+ filename = filename,
+ filetype = fs.detect_filetype(filename),
+ dto = dto,
+ index = j,
+ }
+ end
+ else
+ console.debug(hunks_err, debug.traceback())
+ end
+ end
+ cache.entries = entries
+ return self
+end
+
+function ProjectHunksScene:get_unified_scene_options(options)
+ local table_height = math.floor(dimensions.global_height() * 0.15)
+ return {
+ current = CodeComponent:new(utils.object_assign({
+ config = {
+ win_options = {
+ cursorbind = true,
+ scrollbind = true,
+ cursorline = true,
+ },
+ window_props = {
+ height = dimensions.global_height() - table_height,
+ row = table_height,
+ },
+ },
+ }, options)),
+ table = TableComponent:new(utils.object_assign({
+ header = { 'Filename', 'Hunk' },
+ config = {
+ window_props = {
+ height = table_height,
+ row = 0,
+ },
+ },
+ }, options)),
+ }
+end
+
+function ProjectHunksScene:get_split_scene_options(options)
+ local table_height = math.floor(dimensions.global_height() * 0.15)
+ return {
+ previous = CodeComponent:new(utils.object_assign({
+ config = {
+ win_options = {
+ cursorbind = true,
+ scrollbind = true,
+ cursorline = true,
+ },
+ window_props = {
+ height = dimensions.global_height() - table_height,
+ width = math.floor(dimensions.global_width() / 2),
+ row = table_height,
+ },
+ },
+ }, options)),
+ current = CodeComponent:new(utils.object_assign({
+ config = {
+ win_options = {
+ cursorbind = true,
+ scrollbind = true,
+ cursorline = true,
+ },
+ window_props = {
+ height = dimensions.global_height() - table_height,
+ width = math.floor(dimensions.global_width() / 2),
+ col = math.floor(dimensions.global_width() / 2),
+ row = table_height,
+ },
+ },
+ }, options)),
+ table = TableComponent:new(utils.object_assign({
+ header = { 'Filename', 'Hunk' },
+ config = {
+ window_props = {
+ height = table_height,
+ row = 0,
+ },
+ },
+ }, options)),
+ }
+end
+
+ProjectHunksScene.update = loop.debounce(
+ loop.async(function(self, selected)
+ loop.await_fast_event()
+ local cache = self.cache
+ self.cache.last_selected = selected
+ self.cache.data = cache.entries[selected]
+ local data = cache.data
+ self
+ :reset()
+ :set_title(cache.title, {
+ filename = data.filename,
+ filetype = data.filetype,
+ stat = data.dto.stat,
+ })
+ :make()
+ :paint()
+ :set_cursor_on_mark(data.index, 'top')
+ :notify(
+ string.format(
+ '%s%s/%s Changes',
+ string.rep(' ', 1),
+ data.index,
+ #data.dto.marks
+ )
+ )
+ end),
+ 50
+)
+
+function ProjectHunksScene:open_file()
+ local table = self.scene.components.table
+ loop.await_fast_event()
+ local selected = table:get_lnum()
+ if self.cache.last_selected == selected then
+ local data = self.cache.data
+ self:hide()
+ vim.cmd(string.format('e %s', data.filename))
+ Window:new(0):set_lnum(data.hunks[data.index].start):call(function()
+ vim.cmd('norm! zz')
+ end)
+ return self
+ end
+ self:update(selected)
+end
+
+function ProjectHunksScene:make_table()
+ self.scene.components.table
+ :unlock()
+ :make_rows(self.cache.entries, function(entry)
+ local filename = entry.filename
+ local filetype = entry.filetype
+ local icon, icon_hl = icons.file_icon(filename, filetype)
+ return {
+ {
+ icon_before = {
+ icon = icon,
+ hl = icon_hl,
+ },
+ text = filename,
+ },
+ string.format('%s/%s', entry.index, #entry.dto.marks),
+ }
+ end)
+ :set_keymap('n', 'j', 'on_j')
+ :set_keymap('n', 'J', 'on_j')
+ :set_keymap('n', 'k', 'on_k')
+ :set_keymap('n', 'K', 'on_k')
+ :set_keymap('n', '', 'on_enter')
+ :focus()
+ :lock()
+ return self
+end
+
+function ProjectHunksScene:show(title, options)
+ local is_inside_git_dir = self.git:is_inside_git_dir()
+ if not is_inside_git_dir then
+ console.info('Project has no git folder')
+ console.debug(
+ 'project_hunks_preview is disabled, we are not in git store anymore'
+ )
+ return false
+ end
+ self:hide()
+ local cache = self.cache
+ cache.title = title
+ cache.options = options
+ self:fetch()
+ loop.await_fast_event()
+ if not cache.err and cache.entries and #cache.entries == 0 then
+ console.info('No hunks found')
+ return false
+ end
+ if cache.err then
+ console.error(cache.err)
+ return false
+ end
+ self.scene = Scene:new(self:get_scene_options(options)):mount()
+ cache.data = cache.entries[1]
+ self
+ :set_title(title, {
+ filename = cache.data.filename,
+ filetype = cache.data.filetype,
+ stat = cache.data.dto.stat,
+ })
+ :make()
+ :make_table()
+ :set_cursor_on_mark(1, 'top')
+ :paint()
+ -- Must be after initial fetch
+ cache.last_selected = 1
+ return true
+end
+
+return ProjectHunksScene
diff --git a/lua/vgit/features/scenes/StagedDiffScene.lua b/lua/vgit/features/scenes/StagedDiffScene.lua
new file mode 100644
index 00000000..42b4579e
--- /dev/null
+++ b/lua/vgit/features/scenes/StagedDiffScene.lua
@@ -0,0 +1,42 @@
+local console = require('vgit.core.console')
+local Diff = require('vgit.Diff')
+local Hunk = require('vgit.cli.models.Hunk')
+local DiffScene = require('vgit.features.scenes.DiffScene')
+
+local StagedDiffScene = DiffScene:extend()
+
+function StagedDiffScene:new(...)
+ return setmetatable(DiffScene:new(...), StagedDiffScene)
+end
+
+function StagedDiffScene:fetch()
+ local cache = self.cache
+ local buffer = cache.buffer
+ local show_err, lines = buffer.git_object:lines()
+ if show_err then
+ console.debug(show_err, debug.traceback())
+ cache.err = show_err
+ return self
+ end
+ local dto
+ local hunks_err, hunks = buffer.git_object:staged_hunks()
+ if hunks_err then
+ console.debug(hunks_err, debug.traceback())
+ cache.err = hunks_err
+ return self
+ end
+ if self.layout_type == 'unified' then
+ dto = Diff:new(hunks):unified(lines)
+ else
+ dto = Diff:new(hunks):split(lines)
+ end
+ cache.data = {
+ filename = buffer.filename,
+ filetype = buffer:filetype(),
+ dto = dto,
+ selected_hunk = self.buffer_hunks:cursor_hunk() or Hunk:new(),
+ }
+ return self
+end
+
+return StagedDiffScene
diff --git a/lua/vgit/fs.lua b/lua/vgit/fs.lua
deleted file mode 100644
index b97e3046..00000000
--- a/lua/vgit/fs.lua
+++ /dev/null
@@ -1,112 +0,0 @@
-local buffer = require('vgit.buffer')
-local assert = require('vgit.assertion').assert
-local pfiletype = require('plenary.filetype')
-
-local M = {}
-
-M.cwd_filename = function(filepath)
- assert(type(filepath) == 'string', 'type error :: expected string')
- local end_index = nil
- for i = #filepath, 1, -1 do
- local letter = filepath:sub(i, i)
- if letter == '/' then
- end_index = i
- end
- end
- if not end_index then
- return ''
- end
- return filepath:sub(1, end_index)
-end
-
-M.relative_filename = function(filepath)
- assert(type(filepath) == 'string', 'type error :: expected string')
- local cwd = vim.loop.cwd()
- if not cwd or not filepath then
- return filepath
- end
- if filepath:sub(1, #cwd) == cwd then
- local offset = 0
- if cwd:sub(#cwd, #cwd) ~= '/' then
- offset = 1
- end
- filepath = filepath:sub(#cwd + 1 + offset, #filepath)
- end
- return filepath
-end
-
-M.short_filename = function(filepath)
- assert(type(filepath) == 'string', 'type error :: expected string')
- local filename = ''
- for i = #filepath, 1, -1 do
- local letter = filepath:sub(i, i)
- if letter == '/' then
- break
- end
- filename = letter .. filename
- end
- return filename
-end
-
-M.filename = function(buf)
- assert(type(buf) == 'number', 'type error :: expected number')
- local filepath = vim.api.nvim_buf_get_name(buf)
- return M.relative_filename(filepath)
-end
-
-M.filetype = function(buf)
- assert(type(buf) == 'number', 'type error :: expected number')
- return buffer.get_option(buf, 'filetype')
-end
-
-M.detect_filetype = pfiletype.detect
-
-M.tmpname = function()
- local length = 6
- local res = ''
- for _ = 1, length do
- res = res .. string.char(math.random(97, 122))
- end
- return string.format('/tmp/%s_vgit', res)
-end
-
-M.read_file = function(filepath)
- assert(type(filepath) == 'string', 'type error :: expected string')
- local fd = vim.loop.fs_open(filepath, 'r', 438)
- if fd == nil then
- return { 'File not found' }, nil
- end
- local stat = vim.loop.fs_fstat(fd)
- if stat.type ~= 'file' then
- return { 'File not found' }, nil
- end
- local data = vim.loop.fs_read(fd, stat.size, 0)
- if not vim.loop.fs_close(fd) then
- return { 'Failed to close file' }, nil
- end
- return nil, vim.split(data, '[\r]?\n')
-end
-
-M.write_file = function(filepath, lines)
- assert(type(filepath) == 'string', 'type error :: expected string')
- assert(vim.tbl_islist(lines), 'type error :: expected list table')
- local f = io.open(filepath, 'wb')
- for i = 1, #lines do
- local l = lines[i]
- f:write(l)
- f:write('\n')
- end
- f:close()
-end
-
-M.remove_file = function(filepath)
- assert(type(filepath) == 'string', 'type error :: expected string')
- return os.remove(filepath)
-end
-
-M.exists = function(filepath)
- assert(type(filepath) == 'string', 'type error :: expected string')
- return (vim.loop.fs_stat(filepath) and true) or false
-end
-
-return M
diff --git a/lua/vgit/git.lua b/lua/vgit/git.lua
deleted file mode 100644
index 95b8dbac..00000000
--- a/lua/vgit/git.lua
+++ /dev/null
@@ -1,776 +0,0 @@
-local utils = require('vgit.utils')
-local Job = require('vgit.Job')
-local Hunk = require('vgit.Hunk')
-local Interface = require('vgit.Interface')
-local wrap = require('plenary.async.async').wrap
-local void = require('plenary.async.async').void
-
-local M = {}
-
-M.constants = utils.readonly({
- diff_algorithm = 'myers',
- empty_tree_hash = '4b825dc642cb6eb9a060e54bf8d69288fbee4904',
-})
-
-M.state = Interface:new({
- diff_base = 'HEAD',
- config = {},
-})
-
-M.get_diff_base = function()
- return M.state:get('diff_base')
-end
-
-M.set_diff_base = function(diff_base)
- M.state:set('diff_base', diff_base)
-end
-
-M.is_commit_valid = wrap(function(commit, callback)
- local result = {}
- local err = {}
- local job = Job:new({
- command = 'git',
- args = {
- 'show',
- '--abbrev-commit',
- '--oneline',
- '--no-notes',
- '--no-patch',
- '--no-color',
- commit,
- },
- on_stdout = function(_, data, _)
- result[#result + 1] = data
- end,
- on_stderr = function(_, data, _)
- err[#err + 1] = data
- end,
- on_exit = function()
- if #err ~= 0 then
- return callback(false)
- end
- if #result == 0 then
- return callback(false)
- end
- callback(true)
- end,
- })
- job:start()
-end, 2)
-
-M.create_log = function(line, revision_count)
- local log = vim.split(line, '-')
- -- Sometimes you can have multiple parents, in that instance we pick the first!
- local parents = vim.split(log[2], ' ')
- if #parents > 1 then
- log[2] = parents[1]
- end
- return {
- revision = string.format('HEAD~%s', revision_count),
- commit_hash = log[1]:sub(2, #log[1]),
- parent_hash = log[2],
- timestamp = log[3],
- author_name = log[4],
- author_email = log[5],
- summary = log[6]:sub(1, #log[6] - 1),
- }
-end
-
-M.create_blame = function(info)
- local function split_by_whitespace(str)
- return vim.split(str, ' ')
- end
- local commit_hash_info = split_by_whitespace(info[1])
- local author_mail_info = split_by_whitespace(info[3])
- local author_time_info = split_by_whitespace(info[4])
- local author_tz_info = split_by_whitespace(info[5])
- local committer_mail_info = split_by_whitespace(info[7])
- local committer_time_info = split_by_whitespace(info[8])
- local committer_tz_info = split_by_whitespace(info[9])
- local parent_hash_info = split_by_whitespace(info[11])
- local author = info[2]:sub(8, #info[2])
- local author_mail = author_mail_info[2]
- local committer = info[6]:sub(11, #info[6])
- local committer_mail = committer_mail_info[2]
- local lnum = tonumber(commit_hash_info[3])
- local committed = true
- if
- author == 'Not Committed Yet'
- and committer == 'Not Committed Yet'
- and author_mail == ''
- and committer_mail == ''
- then
- committed = false
- end
- return {
- lnum = lnum,
- commit_hash = commit_hash_info[1],
- parent_hash = parent_hash_info[2],
- author = author,
- author_mail = (function()
- local mail = author_mail
- if mail:sub(1, 1) == '<' and mail:sub(#mail, #mail) then
- mail = mail:sub(2, #mail - 1)
- end
- return mail
- end)(),
- author_time = tonumber(author_time_info[2]),
- author_tz = author_tz_info[2],
- committer = committer,
- committer_mail = (function()
- local mail = committer_mail
- if mail:sub(1, 1) == '<' and mail:sub(#mail, #mail) then
- mail = mail:sub(2, #mail - 1)
- end
- return mail
- end)(),
- committer_time = tonumber(committer_time_info[2]),
- committer_tz = committer_tz_info[2],
- commit_message = info[10]:sub(9, #info[10]),
- committed = committed,
- }
-end
-
--- TODO: This needs to be removed.
-M.setup = void(function(config)
- M.state:assign(config)
- local err, git_config = M.config()
- if not err then
- M.state:set('config', git_config)
- end
-end)
-
-M.config = wrap(function(callback)
- local err = {}
- local result = {}
- local job = Job:new({
- command = 'git',
- args = {
- 'config',
- '--list',
- },
- on_stdout = function(_, line)
- local line_chunks = vim.split(line, '=')
- result[line_chunks[1]] = line_chunks[2]
- end,
- on_stderr = function(_, data, _)
- err[#err + 1] = data
- end,
- on_exit = function()
- if #err ~= 0 then
- return callback(err, nil)
- end
- callback(nil, result)
- end,
- })
- job:start()
-end, 1)
-
-M.has_commits = wrap(function(callback)
- local result = true
- local job = Job:new({
- command = 'git',
- args = { 'status' },
- on_stdout = function(_, line)
- if line == 'No commits yet' then
- result = false
- end
- end,
- on_exit = function()
- callback(result)
- end,
- })
- job:start()
-end, 1)
-
-M.is_inside_work_tree = wrap(function(callback)
- local err = {}
- local job = Job:new({
- command = 'git',
- args = {
- 'rev-parse',
- '--is-inside-work-tree',
- },
- on_stderr = function(_, data, _)
- err[#err + 1] = data
- end,
- on_exit = function()
- if #err ~= 0 then
- return callback(false)
- end
- callback(true)
- end,
- })
- job:start()
-end, 1)
-
-M.blames = wrap(function(filename, callback)
- local err = {}
- local result = {}
- local blame_info = {}
- local job = Job:new({
- command = 'git',
- args = {
- 'blame',
- '--line-porcelain',
- '--',
- filename,
- },
- on_stdout = function(_, data, _)
- if string.byte(data:sub(1, 3)) ~= 9 then
- table.insert(blame_info, data)
- else
- local blame = M.create_blame(blame_info)
- if blame then
- result[#result + 1] = blame
- end
- blame_info = {}
- end
- end,
- on_stderr = function(_, data, _)
- err[#err + 1] = data
- end,
- on_exit = function()
- if #err ~= 0 then
- return callback(err, nil)
- end
- callback(nil, result)
- end,
- })
- job:start()
-end, 2)
-
-M.blame_line = wrap(function(filename, lnum, callback)
- local err = {}
- local result = {}
- local job = Job:new({
- command = 'git',
- args = {
- 'blame',
- '-L',
- string.format('%s,+1', lnum),
- '--line-porcelain',
- '--',
- filename,
- },
- on_stdout = function(_, data, _)
- result[#result + 1] = data
- end,
- on_stderr = function(_, data, _)
- err[#err + 1] = data
- end,
- on_exit = function()
- if #err ~= 0 then
- return callback(err, nil)
- end
- callback(nil, M.create_blame(result))
- end,
- })
- job:start()
-end, 3)
-
-M.logs = wrap(function(filename, callback)
- local err = {}
- local logs = {}
- local revision_count = 0
- local job = Job:new({
- command = 'git',
- args = {
- 'log',
- '--color=never',
- '--pretty=format:"%H-%P-%at-%an-%ae-%s"',
- '--',
- filename,
- },
- on_stdout = function(_, data, _)
- revision_count = revision_count + 1
- local log = M.create_log(data, revision_count)
- if log then
- logs[#logs + 1] = log
- end
- end,
- on_stderr = function(_, data, _)
- err[#err + 1] = data
- end,
- on_exit = function()
- if #err ~= 0 then
- return callback(err, nil)
- end
- return callback(nil, logs)
- end,
- })
- job:start()
-end, 2)
-
-M.file_hunks = wrap(function(filename_a, filename_b, callback)
- local result = {}
- local err = {}
- local args = {
- '--no-pager',
- '-c',
- 'core.safecrlf=false',
- 'diff',
- '--color=never',
- string.format('--diff-algorithm=%s', M.constants.diff_algorithm),
- '--patch-with-raw',
- '--unified=0',
- '--no-index',
- filename_a,
- filename_b,
- }
- local job = Job:new({
- command = 'git',
- args = args,
- on_stdout = function(_, data, _)
- result[#result + 1] = data
- end,
- on_stderr = function(_, data, _)
- err[#err + 1] = data
- end,
- on_exit = function()
- if #err ~= 0 then
- return callback(err, nil)
- end
- local hunks = {}
- for i = 1, #result do
- local line = result[i]
- if vim.startswith(line, '@@') then
- hunks[#hunks + 1] = Hunk:new(line)
- else
- if #hunks > 0 then
- local hunk = hunks[#hunks]
- hunk.diff[#hunk.diff + 1] = line
- end
- end
- end
- return callback(nil, hunks)
- end,
- })
- job:start()
-end, 3)
-
-M.index_hunks = wrap(function(filename, callback)
- local result = {}
- local err = {}
- local args = {
- '--no-pager',
- '-c',
- 'core.safecrlf=false',
- 'diff',
- '--color=never',
- string.format('--diff-algorithm=%s', M.constants.diff_algorithm),
- '--patch-with-raw',
- '--unified=0',
- '--',
- filename,
- }
- local job = Job:new({
- command = 'git',
- args = args,
- on_stdout = function(_, data, _)
- result[#result + 1] = data
- end,
- on_stderr = function(_, data, _)
- err[#err + 1] = data
- end,
- on_exit = function()
- if #err ~= 0 then
- return callback(err, nil)
- end
- local hunks = {}
- for i = 1, #result do
- local line = result[i]
- if vim.startswith(line, '@@') then
- hunks[#hunks + 1] = Hunk:new(line)
- else
- if #hunks > 0 then
- local hunk = hunks[#hunks]
- hunk.diff[#hunk.diff + 1] = line
- end
- end
- end
- return callback(nil, hunks)
- end,
- })
- job:start()
-end, 2)
-
-M.remote_hunks = wrap(function(filename, parent_hash, commit_hash, callback)
- local result = {}
- local err = {}
- local args = {
- '--no-pager',
- '-c',
- 'core.safecrlf=false',
- 'diff',
- '--color=never',
- string.format('--diff-algorithm=%s', M.constants.diff_algorithm),
- '--patch-with-raw',
- '--unified=0',
- M.state:get('diff_base'),
- '--',
- filename,
- }
- if parent_hash and not commit_hash then
- args = {
- '--no-pager',
- '-c',
- 'core.safecrlf=false',
- 'diff',
- '--color=never',
- string.format('--diff-algorithm=%s', M.constants.diff_algorithm),
- '--patch-with-raw',
- '--unified=0',
- parent_hash,
- '--',
- filename,
- }
- end
- if parent_hash and commit_hash then
- args = {
- '--no-pager',
- '-c',
- 'core.safecrlf=false',
- 'diff',
- '--color=never',
- string.format('--diff-algorithm=%s', M.constants.diff_algorithm),
- '--patch-with-raw',
- '--unified=0',
- #parent_hash > 0 and parent_hash or M.constants.empty_tree_hash,
- commit_hash,
- '--',
- filename,
- }
- end
- local job = Job:new({
- command = 'git',
- args = args,
- on_stdout = function(_, data, _)
- result[#result + 1] = data
- end,
- on_stderr = function(_, data, _)
- err[#err + 1] = data
- end,
- on_exit = function()
- if #err ~= 0 then
- return callback(err, nil)
- end
- local hunks = {}
- for i = 1, #result do
- local line = result[i]
- if vim.startswith(line, '@@') then
- hunks[#hunks + 1] = Hunk:new(line)
- else
- if #hunks > 0 then
- local hunk = hunks[#hunks]
- hunk.diff[#hunk.diff + 1] = line
- end
- end
- end
- return callback(nil, hunks)
- end,
- })
- job:start()
-end, 4)
-
-M.staged_hunks = wrap(function(filename, callback)
- local result = {}
- local err = {}
- local args = {
- '--no-pager',
- '-c',
- 'core.safecrlf=false',
- 'diff',
- '--color=never',
- string.format('--diff-algorithm=%s', M.constants.diff_algorithm),
- '--patch-with-raw',
- '--unified=0',
- '--cached',
- '--',
- filename,
- }
- local job = Job:new({
- command = 'git',
- args = args,
- on_stdout = function(_, data, _)
- result[#result + 1] = data
- end,
- on_stderr = function(_, data, _)
- err[#err + 1] = data
- end,
- on_exit = function()
- if #err ~= 0 then
- return callback(err, nil)
- end
- local hunks = {}
- for i = 1, #result do
- local line = result[i]
- if vim.startswith(line, '@@') then
- hunks[#hunks + 1] = Hunk:new(line)
- else
- if #hunks > 0 then
- local hunk = hunks[#hunks]
- hunk.diff[#hunk.diff + 1] = line
- end
- end
- end
- return callback(nil, hunks)
- end,
- })
- job:start()
-end, 2)
-
-M.untracked_hunks = function(lines)
- local diff = {}
- for i = 1, #lines do
- diff[#diff + 1] = string.format('+%s', lines[i])
- end
- return {
- {
- header = nil,
- start = 1,
- finish = #lines,
- type = 'add',
- diff = diff,
- },
- }
-end
-
-M.show = wrap(function(filename, commit_hash, callback)
- local err = {}
- local result = {}
- commit_hash = commit_hash or ''
- local job = Job:new({
- command = 'git',
- args = {
- 'show',
- string.format('%s:%s', commit_hash, filename),
- },
- on_stdout = function(_, data, _)
- result[#result + 1] = data
- end,
- on_stderr = function(_, data, _)
- err[#err + 1] = data
- end,
- on_exit = function()
- if #err ~= 0 then
- return callback(err, nil)
- end
- callback(nil, result)
- end,
- })
- job:start()
-end, 3)
-
-M.stage_file = wrap(function(filename, callback)
- local err = {}
- local job = Job:new({
- command = 'git',
- args = {
- 'add',
- filename,
- },
- on_stderr = function(_, data, _)
- err[#err + 1] = data
- end,
- on_exit = function()
- if #err ~= 0 then
- return callback(err)
- end
- callback(nil)
- end,
- })
- job:start()
-end, 2)
-
-M.unstage_file = wrap(function(filename, callback)
- local err = {}
- local job = Job:new({
- command = 'git',
- args = {
- 'reset',
- '-q',
- 'HEAD',
- '--',
- filename,
- },
- on_stderr = function(_, data, _)
- err[#err + 1] = data
- end,
- on_exit = function()
- if #err ~= 0 then
- return callback(err)
- end
- callback(nil)
- end,
- })
- job:start()
-end, 2)
-
-M.stage_hunk_from_patch = wrap(function(patch_filename, callback)
- local err = {}
- local job = Job:new({
- command = 'git',
- args = {
- 'apply',
- '--cached',
- '--whitespace=nowarn',
- '--unidiff-zero',
- patch_filename,
- },
- on_stderr = function(_, data, _)
- err[#err + 1] = data
- end,
- on_exit = function()
- if #err ~= 0 then
- return callback(err)
- end
- callback(nil)
- end,
- })
- job:start()
-end, 2)
-
-M.check_ignored = wrap(function(filename, callback)
- local err = {}
- local job = Job:new({
- command = 'git',
- args = {
- 'check-ignore',
- filename,
- },
- on_stdout = function(_, data, _)
- err[#err + 1] = data
- end,
- on_stderr = function(_, data, _)
- err[#err + 1] = data
- end,
- on_exit = function()
- if #err ~= 0 then
- return callback(true)
- end
- callback(false)
- end,
- })
- job:start()
-end, 2)
-
-M.reset = wrap(function(filename, callback)
- local err = {}
- local job = Job:new({
- command = 'git',
- args = {
- 'checkout',
- '-q',
- '--',
- filename,
- },
- on_stderr = function(_, data, _)
- err[#err + 1] = data
- end,
- on_exit = function()
- if #err ~= 0 then
- return callback(err)
- end
- callback(nil)
- end,
- })
- job:start()
-end, 2)
-
-M.current_branch = wrap(function(callback)
- local err = {}
- local result = {}
- local job = Job:new({
- command = 'git',
- args = {
- 'branch',
- '--show-current',
- },
- on_stdout = function(_, data, _)
- result[#result + 1] = data
- end,
- on_stderr = function(_, data, _)
- err[#err + 1] = data
- end,
- on_exit = function()
- if #err ~= 0 then
- return callback(err, result)
- end
- callback(nil, result)
- end,
- })
- job:start()
-end, 1)
-
-M.tracked_filename = wrap(function(filename, callback)
- local result = {}
- local job = Job:new({
- command = 'git',
- args = {
- 'ls-files',
- '--exclude-standard',
- filename,
- },
- on_stdout = function(_, data, _)
- result[#result + 1] = data
- end,
- on_exit = function()
- callback(result[1])
- end,
- })
- job:start()
-end, 2)
-
-M.tracked_remote_filename = wrap(function(filename, callback)
- local result = {}
- local job = Job:new({
- command = 'git',
- args = {
- 'ls-files',
- '--exclude-standard',
- '--full-name',
- filename,
- },
- on_stdout = function(_, data, _)
- result[#result + 1] = data
- end,
- on_exit = function()
- callback(result[1])
- end,
- })
- job:start()
-end, 2)
-
-M.ls_changed = wrap(function(callback)
- local err = {}
- local result = {}
- local job = Job:new({
- command = 'git',
- args = {
- 'status',
- '-u',
- '-s',
- '--',
- '.',
- },
- on_stdout = function(_, data, _)
- result[#result + 1] = {
- filename = data:sub(4, #data),
- status = data:sub(1, 2),
- }
- end,
- on_stderr = function(_, data, _)
- err[#err + 1] = data
- end,
- on_exit = function()
- if #err ~= 0 then
- return callback(err, result)
- end
- callback(nil, result)
- end,
- })
- job:start()
-end, 1)
-
-return M
diff --git a/lua/vgit/highlight.lua b/lua/vgit/highlight.lua
deleted file mode 100644
index 8b73c06b..00000000
--- a/lua/vgit/highlight.lua
+++ /dev/null
@@ -1,36 +0,0 @@
-local Interface = require('vgit.Interface')
-
-local M = {}
-
-M.state = Interface:new(require('vgit.themes.monokai'))
-
-M.exists = function(name)
- return pcall(vim.api.nvim_get_hl_by_name, name, true)
-end
-
-M.create = function(group, color)
- local gui = color.gui and 'gui = ' .. color.gui or 'gui = NONE'
- local fg = color.fg and 'guifg = ' .. color.fg or 'guifg = NONE'
- local bg = color.bg and 'guibg = ' .. color.bg or 'guibg = NONE'
- local sp = color.sp and 'guisp = ' .. color.sp or ''
- vim.cmd(
- 'highlight ' .. group .. ' ' .. gui .. ' ' .. fg .. ' ' .. bg .. ' ' .. sp
- )
-end
-
-M.create_theme = function(hls)
- for hl, color in pairs(hls) do
- M.create(hl, color)
- end
-end
-
-M.setup = function(config, force)
- M.state:assign((config and config.hls) or config)
- for hl, color in pairs(M.state.data) do
- if force or not M.exists(hl) then
- M.create(hl, color)
- end
- end
-end
-
-return M
diff --git a/lua/vgit/key_mapper.lua b/lua/vgit/key_mapper.lua
deleted file mode 100644
index 203eb8c2..00000000
--- a/lua/vgit/key_mapper.lua
+++ /dev/null
@@ -1,28 +0,0 @@
-local M = {}
-
-local function set_keymap(mode, key, action)
- vim.api.nvim_set_keymap(mode, key, action, {
- noremap = true,
- silent = true,
- })
-end
-
-local function parse_commands(commands)
- local parsed_commands = vim.split(commands, ' ')
- for i = 1, #parsed_commands do
- local c = parsed_commands[i]
- parsed_commands[i] = vim.trim(c)
- end
- return parsed_commands
-end
-
-M.setup = function(config)
- config = config or {}
- local keymaps = config.keymaps or {}
- for commands, action in pairs(keymaps) do
- commands = parse_commands(commands)
- set_keymap(commands[1], commands[2], string.format(':VGit %s', action))
- end
-end
-
-return M
diff --git a/lua/vgit/layouts/default.lua b/lua/vgit/layouts/default.lua
deleted file mode 100644
index a98fa478..00000000
--- a/lua/vgit/layouts/default.lua
+++ /dev/null
@@ -1,205 +0,0 @@
-local dimensions = require('vgit.dimensions')
-
-return {
- decorator = {
- app_bar = {
- border = {
- chars = { '─', '─', '─', ' ', '─', '─', '─', ' ' },
- hl = 'VGitBorder',
- },
- },
- },
- blame_preview = {
- border = {
- enabled = true,
- chars = { '╭', '─', '╮', '│', '╯', '─', '╰', '│' },
- hl = 'FloatBorder',
- },
- },
- hunk_preview = {
- height = function()
- return 20
- end,
- border = {
- enabled = true,
- chars = { '─', '─', '─', ' ', '─', '─', '─', ' ' },
- hl = 'FloatBorder',
- },
- },
- gutter_blame_preview = {
- blame = {
- height = function()
- return dimensions.global_height()
- end,
- width = function()
- return math.ceil(dimensions.global_width() * 0.35)
- end,
- row = 0,
- col = 0,
- },
- preview = {
- height = function()
- return dimensions.global_height()
- end,
- width = function()
- return math.ceil(dimensions.global_width() * 0.65)
- end,
- row = 0,
- col = function()
- return math.ceil(dimensions.global_width() * 0.35)
- end,
- },
- },
- diff_preview = {
- horizontal = {
- height = function()
- return dimensions.global_height() - 1
- end,
- width = function()
- return dimensions.global_width()
- end,
- row = 0,
- col = 0,
- },
- vertical = {
- previous = {
- height = function()
- return dimensions.global_height() - 1
- end,
- width = function()
- return math.ceil(dimensions.global_width() / 2)
- end,
- row = 0,
- col = 0,
- },
- current = {
- height = function()
- return dimensions.global_height() - 1
- end,
- width = function()
- return math.ceil(dimensions.global_width() / 2)
- end,
- row = 0,
- col = function()
- return math.ceil(dimensions.global_width() / 2)
- end,
- },
- },
- },
- history_preview = {
- horizontal = {
- preview = {
- height = function()
- return math.ceil(dimensions.global_height() - 10)
- end,
- width = function()
- return dimensions.global_width()
- end,
- row = 0,
- col = 0,
- },
- table = {
- height = 9,
- width = function()
- return dimensions.global_width()
- end,
- row = function()
- return math.ceil(dimensions.global_height() - 10)
- end,
- col = 0,
- },
- },
- vertical = {
- previous = {
- height = function()
- return math.ceil(dimensions.global_height() - 10)
- end,
- width = function()
- return math.ceil(dimensions.global_width() / 2)
- end,
- row = 0,
- col = 0,
- },
- current = {
- height = function()
- return math.ceil(dimensions.global_height() - 10)
- end,
- width = function()
- return math.ceil(dimensions.global_width() / 2)
- end,
- row = 0,
- col = function()
- return math.ceil(dimensions.global_width() / 2)
- end,
- },
- table = {
- height = 9,
- width = function()
- return dimensions.global_width()
- end,
- row = function()
- return math.ceil(dimensions.global_height() - 10)
- end,
- col = 0,
- },
- },
- },
- project_diff_preview = {
- horizontal = {
- preview = {
- height = function()
- return math.ceil(dimensions.global_height() - 10)
- end,
- width = function()
- return dimensions.global_width()
- end,
- row = 0,
- col = 0,
- },
- table = {
- height = 9,
- width = function()
- return dimensions.global_width()
- end,
- row = function()
- return math.ceil(dimensions.global_height() - 10)
- end,
- col = 0,
- },
- },
- vertical = {
- previous = {
- height = function()
- return math.ceil(dimensions.global_height() - 10)
- end,
- width = function()
- return math.ceil(dimensions.global_width() / 2)
- end,
- row = 0,
- col = 0,
- },
- current = {
- height = function()
- return math.ceil(dimensions.global_height() - 10)
- end,
- width = function()
- return math.ceil(dimensions.global_width() / 2)
- end,
- row = 0,
- col = function()
- return math.ceil(dimensions.global_width() / 2)
- end,
- },
- table = {
- height = 9,
- width = function()
- return dimensions.global_width()
- end,
- row = function()
- return math.ceil(dimensions.global_height() - 10)
- end,
- col = 0,
- },
- },
- },
-}
diff --git a/lua/vgit/layouts/init.lua b/lua/vgit/layouts/init.lua
deleted file mode 100644
index eb944db3..00000000
--- a/lua/vgit/layouts/init.lua
+++ /dev/null
@@ -1,3 +0,0 @@
-return {
- default = require('vgit.layouts.default'),
-}
diff --git a/lua/vgit/logger.lua b/lua/vgit/logger.lua
deleted file mode 100644
index 2d748c9f..00000000
--- a/lua/vgit/logger.lua
+++ /dev/null
@@ -1,53 +0,0 @@
-local Interface = require('vgit.Interface')
-
-local M = {}
-
-M.state = Interface:new({
- debug = false,
- debug_logs = {},
-})
-
-M.setup = function(config)
- M.state:assign(config)
-end
-
-M.error = function(msg)
- vim.notify(msg, 'error')
-end
-
-M.info = function(msg)
- vim.notify(msg, 'info')
-end
-
-M.warn = function(msg)
- vim.notify(msg, 'warn')
-end
-
-M.debug = function(msg, trace)
- if not M.state:get('debug') then
- return
- end
- local new_msg = ''
- if vim.tbl_islist(msg) then
- for i = 1, #msg do
- local m = msg[i]
- if i == 1 then
- new_msg = new_msg .. m
- else
- new_msg = new_msg .. ', ' .. m
- end
- end
- else
- new_msg = msg
- end
- local debug_logs = M.state:get('debug_logs')
- local log = ''
- if trace then
- log = string.format('VGit[%s]: %s\n%s', os.date('%H:%M:%S'), new_msg, trace)
- else
- log = string.format('VGit[%s]: %s', os.date('%H:%M:%S'), new_msg)
- end
- debug_logs[#debug_logs + 1] = log
-end
-
-return M
diff --git a/lua/vgit/previews/BlamePreview.lua b/lua/vgit/previews/BlamePreview.lua
deleted file mode 100644
index 0145b040..00000000
--- a/lua/vgit/previews/BlamePreview.lua
+++ /dev/null
@@ -1,98 +0,0 @@
-local utils = require('vgit.utils')
-local render_store = require('vgit.stores.render_store')
-local CodeComponent = require('vgit.components.CodeComponent')
-local Preview = require('vgit.Preview')
-local config = render_store.get('layout').blame_preview
-
-local function create_uncommitted_lines(blame)
- return {
- string.format('%sLine #%s', ' ', blame.lnum),
- string.format('%s%s', ' ', 'Uncommitted changes'),
- string.format('%s%s -> %s', ' ', blame.parent_hash, blame.commit_hash),
- }
-end
-
-local function create_committed_lines(blame)
- local max_line_length = 88
- local time = os.difftime(os.time(), blame.author_time) / (24 * 60 * 60)
- local time_format = string.format('%s days ago', utils.round(time))
- local time_divisions = {
- { 24, 'hours' },
- { 60, 'minutes' },
- { 60, 'seconds' },
- }
- local division_counter = 1
- while time < 1 and division_counter ~= #time_divisions do
- local division = time_divisions[division_counter]
- time = time * division[1]
- time_format = string.format('%s %s ago', utils.round(time), division[2])
- division_counter = division_counter + 1
- end
- local commit_message = blame.commit_message
- if #commit_message > max_line_length then
- commit_message = commit_message:sub(1, max_line_length) .. '...'
- end
- return {
- string.format('%sLine #%s', ' ', blame.lnum),
- string.format(' %s (%s)', blame.author, blame.author_mail),
- string.format(' %s (%s)', time_format, os.date('%c', blame.author_time)),
- string.format('%s%s', ' ', commit_message),
- string.format('%s%s -> %s', ' ', blame.parent_hash, blame.commit_hash),
- }
-end
-
-local BlamePreview = Preview:extend()
-
-function BlamePreview:new()
- local this = Preview:new({
- CodeComponent:new({
- header = {
- enabled = false,
- },
- border = utils.retrieve(config.border),
- win_options = {
- ['winhl'] = string.format(
- 'Normal:%s',
- utils.retrieve(config.background_hl) or ''
- ),
- ['cursorline'] = true,
- },
- window_props = {
- style = 'minimal',
- relative = 'cursor',
- height = utils.retrieve(config.height),
- width = utils.retrieve(config.width),
- },
- }),
- }, {
- temporary = true,
- })
- return setmetatable(this, BlamePreview)
-end
-
-function BlamePreview:render()
- if not self:is_mounted() then
- return
- end
- local err, blame = self.err, self.data
- self:clear()
- if err then
- self:set_error(true)
- return self
- end
- if blame then
- local component = self:get_components()[1]
- if not blame.committed then
- local uncommitted_lines = create_uncommitted_lines(blame)
- component:set_lines(uncommitted_lines)
- component:set_height(#uncommitted_lines)
- return self
- end
- local committed_lines = create_committed_lines(blame)
- component:set_lines(committed_lines)
- component:set_height(#committed_lines)
- end
- return self
-end
-
-return BlamePreview
diff --git a/lua/vgit/previews/DiffPreview.lua b/lua/vgit/previews/DiffPreview.lua
deleted file mode 100644
index 2284dcc8..00000000
--- a/lua/vgit/previews/DiffPreview.lua
+++ /dev/null
@@ -1,192 +0,0 @@
-local CodeComponent = require('vgit.components.CodeComponent')
-local utils = require('vgit.utils')
-local fs = require('vgit.fs')
-local Preview = require('vgit.Preview')
-local render_store = require('vgit.stores.render_store')
-
-local config = render_store.get('layout').diff_preview
-
-local DiffPreview = Preview:extend()
-
-local function create_horizontal_widget(opts)
- return Preview:new({
- preview = CodeComponent:new({
- filetype = opts.filetype,
- border = utils.retrieve(config.horizontal.border),
- win_options = {
- ['winhl'] = string.format(
- 'Normal:%s',
- config.horizontal.background_hl or ''
- ),
- ['cursorline'] = true,
- ['cursorbind'] = true,
- ['scrollbind'] = true,
- },
- window_props = {
- style = 'minimal',
- relative = 'editor',
- width = utils.retrieve(config.horizontal.width),
- height = utils.retrieve(config.horizontal.height),
- row = utils.retrieve(config.horizontal.row),
- col = utils.retrieve(config.horizontal.col),
- },
- virtual_line_nr = {
- enabled = true,
- },
- }),
- }, opts)
-end
-
-local function create_vertical_widget(opts)
- return Preview:new({
- previous = CodeComponent:new({
- filetype = opts.filetype,
- border = utils.retrieve(config.vertical.previous.border),
- win_options = {
- ['winhl'] = string.format(
- 'Normal:%s',
- config.vertical.previous.background_hl or ''
- ),
- ['cursorbind'] = true,
- ['scrollbind'] = true,
- ['cursorline'] = true,
- },
- window_props = {
- style = 'minimal',
- relative = 'editor',
- width = utils.retrieve(config.vertical.previous.width),
- height = utils.retrieve(config.vertical.previous.height),
- row = utils.retrieve(config.vertical.previous.row),
- col = utils.retrieve(config.vertical.previous.col),
- },
- virtual_line_nr = {
- enabled = true,
- },
- }),
- current = CodeComponent:new({
- filetype = opts.filetype,
- border = utils.retrieve(config.vertical.current.border),
- win_options = {
- ['winhl'] = string.format(
- 'Normal:%s',
- config.vertical.previous.background_hl or ''
- ),
- ['cursorbind'] = true,
- ['scrollbind'] = true,
- ['cursorline'] = true,
- },
- window_props = {
- style = 'minimal',
- relative = 'editor',
- width = utils.retrieve(config.vertical.current.width),
- height = utils.retrieve(config.vertical.current.height),
- row = utils.retrieve(config.vertical.current.row),
- col = utils.retrieve(config.vertical.current.col),
- },
- virtual_line_nr = {
- enabled = true,
- },
- }),
- }, opts)
-end
-
-function DiffPreview:new(opts)
- local this = create_vertical_widget(opts)
- if opts.layout_type == 'horizontal' then
- this = create_horizontal_widget(opts)
- end
- return setmetatable(this, DiffPreview)
-end
-
-function DiffPreview:set_cursor(row, col)
- if self.layout_type == 'vertical' then
- self:get_components().previous:set_cursor(row, col)
- self:get_components().current:set_cursor(row, col)
- else
- self:get_components().preview:set_cursor(row, col)
- end
- return self
-end
-
-function DiffPreview:reposition_cursor(lnum)
- local new_lines_added = 0
- local diff_change = self.data.diff_change
- for i = 1, #diff_change.hunks do
- local hunk = diff_change.hunks[i]
- local type = hunk.type
- local diff = hunk.diff
- local current_new_lines_added = 0
- if type == 'remove' then
- for _ = 1, #diff do
- current_new_lines_added = current_new_lines_added + 1
- end
- elseif type == 'change' then
- local removed_lines, added_lines = hunk:parse_diff()
- if self.layout_type == 'vertical' then
- if #removed_lines ~= #added_lines and #removed_lines > #added_lines then
- current_new_lines_added = current_new_lines_added
- + (#removed_lines - #added_lines)
- end
- else
- current_new_lines_added = current_new_lines_added + #removed_lines
- end
- end
- new_lines_added = new_lines_added + current_new_lines_added
- local start = hunk.start + new_lines_added
- local finish = hunk.finish + new_lines_added
- local padded_lnum = lnum + new_lines_added
- if padded_lnum >= start and padded_lnum <= finish then
- if type == 'remove' then
- self:set_cursor(start - current_new_lines_added + 1, 0)
- else
- self:set_cursor(start - current_new_lines_added, 0)
- end
- vim.cmd('norm! zz')
- return
- end
- end
- local hunk = diff_change.hunks[1]
- if hunk then
- local start = hunk.start
- if hunk.type == 'remove' then
- start = start + 1
- end
- self:set_cursor(start, 0)
- vim.cmd('norm! zz')
- end
-end
-
-function DiffPreview:render()
- if not self:is_mounted() then
- return
- end
- local err, data = self.err, self.data
- self:clear()
- if err then
- self:set_error(true)
- return self
- end
- local diff_change = data.diff_change
- local filename = fs.short_filename(data.filename)
- local filetype = data.filetype
- if diff_change then
- if self.layout_type == 'horizontal' then
- local components = self:get_components()
- components.preview
- :set_lines(diff_change.lines)
- :set_title('Diff Preview:', filename, filetype)
- else
- local components = self:get_components()
- components.previous
- :set_lines(diff_change.previous_lines)
- :set_title('Diff Preview:', filename, filetype)
- components.current:set_lines(diff_change.current_lines)
- end
- self:make_virtual_line_nr(diff_change)
- self:highlight_diff_change(diff_change)
- self:reposition_cursor(self.selected)
- end
- return self
-end
-
-return DiffPreview
diff --git a/lua/vgit/previews/GutterBlamePreview.lua b/lua/vgit/previews/GutterBlamePreview.lua
deleted file mode 100644
index 60fd24c3..00000000
--- a/lua/vgit/previews/GutterBlamePreview.lua
+++ /dev/null
@@ -1,138 +0,0 @@
-local utils = require('vgit.utils')
-local CodeComponent = require('vgit.components.CodeComponent')
-local Preview = require('vgit.Preview')
-local render_store = require('vgit.stores.render_store')
-
-local config = render_store.get('layout').gutter_blame_preview
-
-local function get_blame_line(blame)
- local time = os.difftime(os.time(), blame.author_time) / (24 * 60 * 60)
- local time_format = string.format('%s days ago', utils.round(time))
- local time_divisions = {
- { 24, 'hours' },
- { 60, 'minutes' },
- { 60, 'seconds' },
- }
- local division_counter = 1
- while time < 1 and division_counter ~= #time_divisions do
- local division = time_divisions[division_counter]
- time = time * division[1]
- time_format = string.format('%s %s ago', utils.round(time), division[2])
- division_counter = division_counter + 1
- end
- if blame.committed then
- return string.format(
- '%s (%s) • %s',
- blame.author,
- time_format,
- blame.committed and blame.commit_message or 'Uncommitted changes'
- )
- end
- return 'Uncommitted changes'
-end
-
-local function get_blame_lines(blames)
- local blame_lines = {}
- local last_blame = nil
- for i = 1, #blames do
- local blame = blames[i]
- if last_blame then
- if blame.commit_hash == last_blame.commit_hash then
- blame_lines[#blame_lines + 1] = ''
- else
- blame_lines[#blame_lines + 1] = get_blame_line(blame)
- end
- else
- blame_lines[#blame_lines + 1] = get_blame_line(blame)
- end
- last_blame = blame
- end
- return blame_lines
-end
-
-local GutterBlamePreview = Preview:extend()
-
-function GutterBlamePreview:new(opts)
- local this = Preview:new({
- blame = CodeComponent:new({
- header = {
- enabled = false,
- },
- border = utils.retrieve(config.blame.border),
- win_options = {
- ['winhl'] = string.format(
- 'Normal:%s',
- utils.retrieve(config.blame.background_hl) or ''
- ),
- ['cursorbind'] = true,
- ['scrollbind'] = true,
- ['cursorline'] = true,
- },
- window_props = {
- focusable = false,
- style = 'minimal',
- height = utils.retrieve(config.blame.height),
- width = utils.retrieve(config.blame.width),
- row = utils.retrieve(config.blame.row),
- col = utils.retrieve(config.blame.col),
- },
- }),
- preview = CodeComponent:new({
- header = {
- enabled = false,
- },
- border = utils.retrieve(config.blame.preview),
- win_options = {
- ['winhl'] = string.format(
- 'Normal:%s',
- utils.retrieve(config.preview.background_hl) or ''
- ),
- ['cursorbind'] = true,
- ['scrollbind'] = true,
- ['cursorline'] = true,
- ['number'] = true,
- },
- window_props = {
- style = 'minimal',
- height = utils.retrieve(config.preview.height),
- width = utils.retrieve(config.preview.width),
- row = utils.retrieve(config.preview.row),
- col = utils.retrieve(config.preview.col),
- },
- filetype = opts.filetype,
- }),
- }, {
- temporary = true,
- })
- return setmetatable(this, GutterBlamePreview)
-end
-
-function GutterBlamePreview:get_preview_buf()
- return { self:get_components().preview:get_buf() }
-end
-
-function GutterBlamePreview:set_cursor(row, col)
- self:get_components().preview:set_cursor(row, col)
- return self
-end
-
-function GutterBlamePreview:render()
- if not self:is_mounted() then
- return
- end
- local err, data = self.err, self.data
- self:clear()
- local components = self:get_components()
- if err then
- self:set_error(true)
- return self
- end
- if data then
- components.preview:set_lines(data.lines)
- components.blame:set_lines(get_blame_lines(data.blames))
- end
- components.preview:focus()
- return self
-end
-
-return GutterBlamePreview
diff --git a/lua/vgit/previews/HistoryPreview.lua b/lua/vgit/previews/HistoryPreview.lua
deleted file mode 100644
index 9c9f89b4..00000000
--- a/lua/vgit/previews/HistoryPreview.lua
+++ /dev/null
@@ -1,280 +0,0 @@
-local render_store = require('vgit.stores.render_store')
-local utils = require('vgit.utils')
-local fs = require('vgit.fs')
-local TableComponent = require('vgit.components.TableComponent')
-local CodeComponent = require('vgit.components.CodeComponent')
-local Preview = require('vgit.Preview')
-
-local config = render_store.get('layout').history_preview
-
-local function create_horizontal_widget(opts)
- return Preview:new({
- preview = CodeComponent:new({
- filetype = opts.filetype,
- border = utils.retrieve(config.horizontal.preview.border),
- buf_options = {
- ['modifiable'] = false,
- ['buflisted'] = false,
- ['bufhidden'] = 'wipe',
- },
- win_options = {
- ['winhl'] = string.format(
- 'Normal:%s',
- utils.retrieve(config.horizontal.preview.background_hl) or ''
- ),
- ['cursorline'] = true,
- ['wrap'] = false,
- ['cursorbind'] = true,
- ['scrollbind'] = true,
- },
- window_props = {
- style = 'minimal',
- relative = 'editor',
- height = utils.retrieve(config.horizontal.preview.height),
- width = utils.retrieve(config.horizontal.preview.width),
- row = utils.retrieve(config.horizontal.preview.row),
- col = utils.retrieve(config.horizontal.preview.col),
- },
- virtual_line_nr = {
- enabled = true,
- },
- }),
- table = TableComponent:new({
- header = { 'Revision', 'Author Name', 'Commit Hash', 'Summary', 'Time' },
- static = true,
- title = 'History',
- border = utils.retrieve(config.horizontal.table.border),
- buf_options = {
- ['modifiable'] = false,
- ['buflisted'] = false,
- ['bufhidden'] = 'wipe',
- },
- win_options = {
- ['winhl'] = string.format(
- 'Normal:%s',
- utils.retrieve(config.horizontal.table.background_hl) or ''
- ),
- ['cursorline'] = true,
- ['cursorbind'] = false,
- ['scrollbind'] = false,
- ['wrap'] = false,
- },
- window_props = {
- style = 'minimal',
- relative = 'editor',
- height = utils.retrieve(config.horizontal.table.height),
- width = utils.retrieve(config.horizontal.table.width),
- row = utils.retrieve(config.horizontal.table.row),
- col = utils.retrieve(config.horizontal.table.col),
- },
- }),
- }, opts)
-end
-
-local function create_vertical_widget(opts)
- return Preview:new({
- previous = CodeComponent:new({
- filetype = opts.filetype,
- border = utils.retrieve(config.vertical.previous.border),
- buf_options = {
- ['modifiable'] = false,
- ['buflisted'] = false,
- ['bufhidden'] = 'wipe',
- },
- win_options = {
- ['winhl'] = string.format(
- 'Normal:%s',
- utils.retrieve(config.vertical.previous.background_hl) or ''
- ),
- ['cursorline'] = true,
- ['wrap'] = false,
- ['cursorbind'] = true,
- ['scrollbind'] = true,
- },
- window_props = {
- style = 'minimal',
- relative = 'editor',
- width = utils.retrieve(config.vertical.previous.width),
- height = utils.retrieve(config.vertical.previous.height),
- row = utils.retrieve(config.vertical.previous.row),
- col = utils.retrieve(config.vertical.previous.col),
- },
- virtual_line_nr = {
- enabled = true,
- },
- }),
- current = CodeComponent:new({
- filetype = opts.filetype,
- border = utils.retrieve(config.vertical.current.border),
- buf_options = {
- ['modifiable'] = false,
- ['buflisted'] = false,
- ['bufhidden'] = 'wipe',
- },
- win_options = {
- ['winhl'] = string.format(
- 'Normal:%s',
- utils.retrieve(config.vertical.current.background_hl) or ''
- ),
- ['cursorline'] = true,
- ['wrap'] = false,
- ['cursorbind'] = true,
- ['scrollbind'] = true,
- },
- window_props = {
- style = 'minimal',
- relative = 'editor',
- width = utils.retrieve(config.vertical.current.width),
- height = utils.retrieve(config.vertical.current.height),
- row = utils.retrieve(config.vertical.current.row),
- col = utils.retrieve(config.vertical.current.col),
- },
- virtual_line_nr = {
- enabled = true,
- },
- }),
- table = TableComponent:new({
- header = { 'Revision', 'Author Name', 'Commit Hash', 'Summary', 'Time' },
- static = true,
- border = utils.retrieve(config.vertical.table.border),
- buf_options = {
- ['modifiable'] = false,
- ['buflisted'] = false,
- ['bufhidden'] = 'wipe',
- },
- win_options = {
- ['winhl'] = string.format(
- 'Normal:%s',
- utils.retrieve(config.vertical.table.background_hl) or ''
- ),
- ['cursorline'] = true,
- ['cursorbind'] = false,
- ['scrollbind'] = false,
- ['wrap'] = false,
- },
- window_props = {
- style = 'minimal',
- relative = 'editor',
- width = utils.retrieve(config.vertical.table.width),
- height = utils.retrieve(config.vertical.table.height),
- row = utils.retrieve(config.vertical.table.row),
- col = utils.retrieve(config.vertical.table.col),
- },
- }),
- }, opts)
-end
-
-local HistoryPreview = Preview:extend()
-
-function HistoryPreview:new(opts)
- local this = create_vertical_widget(opts)
- if opts.layout_type == 'horizontal' then
- this = create_horizontal_widget(opts)
- end
- return setmetatable(this, HistoryPreview)
-end
-
-function HistoryPreview:mount()
- if self.state.mounted then
- return self
- end
- Preview.mount(self)
- local table = self:get_components().table
- table:add_keymap(
- '',
- string.format('_rerender_history(%s)', self:get_parent_buf())
- )
- table:add_keymap(
- '<2-LeftMouse>',
- string.format('_rerender_history(%s)', self:get_parent_buf())
- )
- table:focus()
- return self
-end
-
-function HistoryPreview:make_table()
- local logs = self.data.logs
- local components = self:get_components()
- local table = components.table
- local rows = {}
- for i = 1, #logs do
- local log = logs[i]
- rows[#rows + 1] = {
- log.revision,
- log.author_name or '',
- log.commit_hash or '',
- log.summary or '',
- (log.timestamp and os.date('%Y-%m-%d', tonumber(log.timestamp))) or '',
- }
- end
- table:set_lines(rows)
- local column_ranges = table:get_column_ranges()
- local column_hls = { '', '', 'Keyword', '', '' }
- for i = 1, #rows do
- for j = 1, #column_ranges do
- local r = column_ranges[j]
- vim.api.nvim_buf_add_highlight(
- table:get_buf(),
- -1,
- column_hls[j],
- i - 1,
- r[1],
- r[2]
- )
- end
- end
-end
-
-function HistoryPreview:show_indicator()
- local components = self:get_components()
- local table = components.table
- table:transpose_text({
- render_store.get('preview').symbols.indicator,
- render_store.get('preview').indicator_hl,
- }, self.selected, 0)
-end
-
-function HistoryPreview:render()
- if not self:is_mounted() then
- return
- end
- local err, data = self.err, self.data
- local components = self:get_components()
- local table = components.table
- self:clear()
- if err then
- self:set_error(true)
- self:show_indicator()
- return self
- elseif data then
- local diff_change = data.diff_change
- local filename = fs.short_filename(data.filename)
- local filetype = data.filetype
- if self.layout_type == 'horizontal' then
- components.preview
- :set_cursor(1, 0)
- :set_lines(diff_change.lines)
- :set_title('History:', filename, filetype)
- else
- components.previous
- :set_cursor(1, 0)
- :set_lines(diff_change.previous_lines)
- :set_title('History:', filename, filetype)
- components.current:set_cursor(1, 0):set_lines(diff_change.current_lines)
- end
- if not table:has_lines() then
- self:make_table()
- end
- self:show_indicator()
- self:make_virtual_line_nr(diff_change)
- self:highlight_diff_change(diff_change)
- else
- table:set_centered_text('There are no commits')
- table:remove_keymap('')
- table:remove_keymap('<2-LeftMouse>')
- end
- table:focus()
- return self
-end
-
-return HistoryPreview
diff --git a/lua/vgit/previews/HunkPreview.lua b/lua/vgit/previews/HunkPreview.lua
deleted file mode 100644
index 068ea08a..00000000
--- a/lua/vgit/previews/HunkPreview.lua
+++ /dev/null
@@ -1,110 +0,0 @@
-local dimensions = require('vgit.dimensions')
-local fs = require('vgit.fs')
-local utils = require('vgit.utils')
-local CodeComponent = require('vgit.components.CodeComponent')
-local Preview = require('vgit.Preview')
-local render_store = require('vgit.stores.render_store')
-
-local config = render_store.get('layout').hunk_preview
-
-local HunkPreview = Preview:extend()
-
-function HunkPreview:new(opts)
- local this = Preview:new({
- preview = CodeComponent:new({
- border = utils.retrieve(config.border),
- header = {
- enabled = false,
- },
- win_options = {
- ['winhl'] = string.format(
- 'Normal:%s',
- utils.retrieve(config.background_hl) or ''
- ),
- ['cursorbind'] = true,
- ['scrollbind'] = true,
- ['cursorline'] = true,
- },
- window_props = {
- style = 'minimal',
- relative = 'cursor',
- height = utils.retrieve(config.height),
- width = dimensions.global_width(),
- },
- filetype = opts.filetype,
- }),
- }, {
- temporary = true,
- layout_type = 'horizontal',
- selected = 1,
- })
- return setmetatable(this, HunkPreview)
-end
-
-function HunkPreview:set_cursor(row, col)
- self:get_components().preview:set_cursor(row, col)
- return self
-end
-
-function HunkPreview:reposition_cursor(lnum)
- local new_lines_added = 0
- local hunks = self.data.diff_change.hunks
- for i = 1, #hunks do
- local hunk = hunks[i]
- local type = hunk.type
- local diff = hunk.diff
- local current_new_lines_added = 0
- if type == 'remove' then
- for _ = 1, #diff do
- current_new_lines_added = current_new_lines_added + 1
- end
- elseif type == 'change' then
- for j = 1, #diff do
- local line = diff[j]
- local line_type = line:sub(1, 1)
- if line_type == '-' then
- current_new_lines_added = current_new_lines_added + 1
- end
- end
- end
- new_lines_added = new_lines_added + current_new_lines_added
- local start = hunk.start + new_lines_added
- local finish = hunk.finish + new_lines_added
- local padded_lnum = lnum + new_lines_added
- if padded_lnum >= start and padded_lnum <= finish then
- if type == 'remove' then
- self:set_cursor(start - current_new_lines_added + 1, 0)
- else
- self:set_cursor(start - current_new_lines_added, 0)
- end
- vim.cmd('norm! zt')
- break
- end
- end
-end
-
-function HunkPreview:render()
- if not self:is_mounted() then
- return
- end
- local err, data = self.err, self.data
- self:clear()
- if err then
- self:set_error(true)
- return self
- end
- if data then
- local diff_change = data.diff_change
- local filename = fs.short_filename(data.filename)
- local filetype = data.filetype
- local components = self:get_components()
- local component = components.preview
- component:set_lines(diff_change.lines)
- component:set_title('Hunk:', filename, filetype)
- self:highlight_diff_change(diff_change)
- self:reposition_cursor(self.selected)
- end
- return self
-end
-
-return HunkPreview
diff --git a/lua/vgit/previews/ProjectDiffPreview.lua b/lua/vgit/previews/ProjectDiffPreview.lua
deleted file mode 100644
index b5ee1fc2..00000000
--- a/lua/vgit/previews/ProjectDiffPreview.lua
+++ /dev/null
@@ -1,374 +0,0 @@
-local TableComponent = require('vgit.components.TableComponent')
-local fs = require('vgit.fs')
-local utils = require('vgit.utils')
-local render_store = require('vgit.stores.render_store')
-local CodeComponent = require('vgit.components.CodeComponent')
-local icons = require('vgit.icons')
-local Preview = require('vgit.Preview')
-
-local config = render_store.get('layout').project_diff_preview
-
-local function create_horizontal_widget(opts)
- return Preview:new({
- preview = CodeComponent:new({
- border = utils.retrieve(config.horizontal.preview.border),
- buf_options = {
- ['modifiable'] = false,
- ['buflisted'] = false,
- ['bufhidden'] = 'wipe',
- },
- win_options = {
- ['winhl'] = string.format(
- 'Normal:%s',
- utils.retrieve(config.horizontal.preview.background_hl) or ''
- ),
- ['cursorline'] = true,
- ['wrap'] = false,
- ['cursorbind'] = true,
- ['scrollbind'] = true,
- },
- window_props = {
- style = 'minimal',
- relative = 'editor',
- width = utils.retrieve(config.horizontal.preview.width),
- height = utils.retrieve(config.horizontal.preview.height),
- row = utils.retrieve(config.horizontal.preview.row),
- col = utils.retrieve(config.horizontal.preview.col),
- },
- virtual_line_nr = {
- enabled = true,
- },
- }),
- table = TableComponent:new({
- header = { 'Changes' },
- column_spacing = 3,
- max_column_len = 100,
- border = utils.retrieve(config.horizontal.table.border),
- buf_options = {
- ['modifiable'] = false,
- ['buflisted'] = false,
- ['bufhidden'] = 'wipe',
- },
- win_options = {
- ['winhl'] = string.format(
- 'Normal:%s',
- utils.retrieve(config.horizontal.table.background_hl) or ''
- ),
- ['cursorline'] = true,
- ['cursorbind'] = false,
- ['scrollbind'] = false,
- ['wrap'] = false,
- },
- window_props = {
- style = 'minimal',
- relative = 'editor',
- width = utils.retrieve(config.horizontal.table.width),
- height = utils.retrieve(config.horizontal.table.height),
- row = utils.retrieve(config.horizontal.table.row),
- col = utils.retrieve(config.horizontal.table.col),
- },
- static = true,
- }),
- }, opts)
-end
-
-local function create_vertical_widget(opts)
- return Preview:new({
- previous = CodeComponent:new({
- border = utils.retrieve(config.vertical.previous.border),
- buf_options = {
- ['modifiable'] = false,
- ['buflisted'] = false,
- ['bufhidden'] = 'wipe',
- },
- win_options = {
- ['winhl'] = string.format(
- 'Normal:%s',
- utils.retrieve(config.vertical.previous.background_hl) or ''
- ),
- ['cursorline'] = true,
- ['wrap'] = false,
- ['cursorbind'] = true,
- ['scrollbind'] = true,
- },
- window_props = {
- style = 'minimal',
- relative = 'editor',
- height = utils.retrieve(config.vertical.previous.height),
- width = utils.retrieve(config.vertical.previous.width),
- row = utils.retrieve(config.vertical.previous.row),
- col = utils.retrieve(config.vertical.previous.col),
- },
- virtual_line_nr = {
- enabled = true,
- },
- }),
- current = CodeComponent:new({
- border = utils.retrieve(config.vertical.current.border),
- buf_options = {
- ['modifiable'] = false,
- ['buflisted'] = false,
- ['bufhidden'] = 'wipe',
- },
- win_options = {
- ['winhl'] = string.format(
- 'Normal:%s',
- utils.retrieve(config.vertical.current.background_hl) or ''
- ),
- ['cursorline'] = true,
- ['wrap'] = false,
- ['cursorbind'] = true,
- ['scrollbind'] = true,
- },
- window_props = {
- style = 'minimal',
- relative = 'editor',
- height = utils.retrieve(config.vertical.current.height),
- width = utils.retrieve(config.vertical.current.width),
- row = utils.retrieve(config.vertical.current.row),
- col = utils.retrieve(config.vertical.current.col),
- },
- virtual_line_nr = {
- enabled = true,
- },
- }),
- table = TableComponent:new({
- header = { 'Changes' },
- column_spacing = 3,
- max_column_len = 100,
- border = utils.retrieve(config.vertical.table.border),
- buf_options = {
- ['modifiable'] = false,
- ['buflisted'] = false,
- ['bufhidden'] = 'wipe',
- },
- win_options = {
- ['winhl'] = string.format(
- 'Normal:%s',
- utils.retrieve(config.vertical.table.background_hl) or ''
- ),
- ['cursorline'] = true,
- ['cursorbind'] = false,
- ['scrollbind'] = false,
- ['wrap'] = false,
- },
- window_props = {
- style = 'minimal',
- relative = 'editor',
- height = utils.retrieve(config.vertical.table.height),
- width = utils.retrieve(config.vertical.table.width),
- row = utils.retrieve(config.vertical.table.row),
- col = utils.retrieve(config.vertical.table.col),
- },
- static = true,
- }),
- }, opts)
-end
-
-local ProjectDiffPreview = Preview:extend()
-
-function ProjectDiffPreview:new(opts)
- local this = create_vertical_widget(opts)
- if opts.layout_type == 'horizontal' then
- this = create_horizontal_widget(opts)
- end
- return setmetatable(this, ProjectDiffPreview)
-end
-
-function ProjectDiffPreview:mount()
- if self.state.mounted then
- return self
- end
- Preview.mount(self)
- local components = self:get_components()
- local table = components.table
- table:add_keymap('', '_rerender_project_diff()')
- table:add_keymap('<2-LeftMouse>', '_rerender_project_diff()')
- table:focus()
- if self.layout_type == 'vertical' then
- components.previous:add_keymap('', '_select_project_diff()')
- components.current:add_keymap('', '_select_project_diff()')
- else
- components.preview:add_keymap('', '_select_project_diff()')
- end
- return self
-end
-
-function ProjectDiffPreview:make_table()
- local changed_files = self.data.changed_files
- local components = self:get_components()
- local table = components.table
- local rows = {}
- local spacing = ' '
- local defered = {}
- for i = 1, #changed_files do
- local file = changed_files[i]
- local icon, icon_hl = icons.file_icon(
- file.filename,
- fs.detect_filetype(file.filename)
- )
- local filename = fs.short_filename(file.filename)
- local directory = fs.cwd_filename(file.filename)
- local segments = {
- string.format('%s', spacing),
- string.format(' %s', icon),
- string.format(' %s', filename),
- string.format(' %s', directory),
- string.format(' %s', file.status),
- }
- rows[#rows + 1] = { vim.fn.join(segments, '') }
- defered[#defered + 1] = function()
- if icon_hl then
- vim.api.nvim_buf_add_highlight(
- table:get_buf(),
- -1,
- icon_hl,
- i - 1,
- #segments[1],
- #segments[1] + #segments[2] + 3
- )
- end
- end
- defered[#defered + 1] = function()
- if icon_hl then
- vim.api.nvim_buf_add_highlight(
- table:get_buf(),
- -1,
- 'Comment',
- i - 1,
- #segments[1] + #segments[2] + 3 + #segments[3] + 1,
- #segments[1] + #segments[2] + 3 + #segments[3] + #segments[4]
- )
- end
- end
- defered[#defered + 1] = function()
- if icon_hl then
- vim.api.nvim_buf_add_highlight(
- table:get_buf(),
- -1,
- 'VGitStatus',
- i - 1,
- #segments[1] + #segments[2] + 3 + #segments[3] + #segments[4] + 1,
- #segments[1]
- + #segments[2]
- + 3
- + #segments[3]
- + #segments[4]
- + #segments[5]
- )
- end
- end
- end
- table:set_lines(rows)
- for i = 1, #defered do
- defered[i]()
- end
-end
-
-function ProjectDiffPreview:reposition_cursor()
- local diff_change = self.data.diff_change
- local hunk = diff_change.hunks[1]
- if hunk then
- local start = hunk.start
- if hunk.type == 'remove' then
- start = start + 1
- end
- local components = self:get_components()
- if self.layout_type == 'vertical' then
- components.previous:set_cursor(start, 0):call(function()
- vim.cmd('norm! zz')
- end)
- components.current:set_cursor(start, 0):call(function()
- vim.cmd('norm! zz')
- end)
- else
- components.preview:set_cursor(start, 0):call(function()
- vim.cmd('norm! zz')
- end)
- end
- end
-end
-
-function ProjectDiffPreview:show_indicator()
- local components = self:get_components()
- local table = components.table
- table:transpose_text({
- render_store.get('preview').symbols.indicator,
- render_store.get('preview').indicator_hl,
- }, self.selected, 0)
-end
-
-function ProjectDiffPreview:render()
- if not self:is_mounted() then
- return
- end
- local components = self:get_components()
- local table = components.table
- local err, data = self.err, self.data
- self:clear()
- if err then
- if err[1] == 'File not found' then
- local file_not_found_msg = 'File has been deleted'
- if self.layout_type == 'horizontal' then
- components.preview
- :set_cursor(1, 0)
- :set_centered_text(file_not_found_msg)
- else
- components.previous
- :set_cursor(1, 0)
- :set_centered_text(file_not_found_msg)
- components.current
- :set_cursor(1, 0)
- :set_centered_text(file_not_found_msg)
- end
- self:show_indicator()
- self:make_table()
- return
- end
- self:set_error(true)
- self:show_indicator()
- return self
- elseif data then
- local diff_change = data.diff_change
- local filetype = data.filetype
- local filename = fs.short_filename(data.filename)
- if self.layout_type == 'horizontal' then
- components.preview
- :set_cursor(1, 0)
- :set_lines(diff_change.lines)
- :set_filetype(filetype)
- :set_title('Project Diff:', filename, filetype)
- else
- components.previous
- :set_cursor(1, 0)
- :set_lines(diff_change.previous_lines)
- :set_filetype(filetype)
- :set_title('Project Diff:', filename, filetype)
- components.current
- :set_cursor(1, 0)
- :set_lines(diff_change.current_lines)
- :set_filetype(filetype)
- end
- if not table:has_lines() then
- self:make_table()
- end
- self:show_indicator()
- self:make_virtual_line_nr(diff_change)
- self:highlight_diff_change(diff_change)
- self:reposition_cursor()
- else
- table:set_centered_text('There are no changes')
- table:remove_keymap('')
- table:remove_keymap('<2-LeftMouse>')
- if self.layout_type == 'vertical' then
- components.previous:remove_keymap('')
- components.current:remove_keymap('')
- else
- components.preview:remove_keymap('')
- end
- end
- table:focus()
- return self
-end
-
-return ProjectDiffPreview
diff --git a/lua/vgit/renderer.lua b/lua/vgit/renderer.lua
deleted file mode 100644
index fba50f96..00000000
--- a/lua/vgit/renderer.lua
+++ /dev/null
@@ -1,326 +0,0 @@
-local utils = require('vgit.utils')
-local render_store = require('vgit.stores.render_store')
-local DiffPreview = require('vgit.previews.DiffPreview')
-local GutterBlamePreview = require('vgit.previews.GutterBlamePreview')
-local preview_store = require('vgit.stores.preview_store')
-local HistoryPreview = require('vgit.previews.HistoryPreview')
-local HunkPreview = require('vgit.previews.HunkPreview')
-local BlamePreview = require('vgit.previews.BlamePreview')
-local ProjectDiffPreview = require('vgit.previews.ProjectDiffPreview')
-local virtual_text = require('vgit.virtual_text')
-local buffer = require('vgit.buffer')
-local sign = require('vgit.sign')
-local scheduler = require('plenary.async.util').scheduler
-local void = require('plenary.async.async').void
-
-local M = {}
-
-local current_hunk_mark_timer_id = nil
-
-M.constants = utils.readonly({
- blame_ns_id = vim.api.nvim_create_namespace('tanvirtin/vgit.nvim/blame'),
- current_hunk_mark_ns_id = vim.api.nvim_create_namespace(
- 'tanvirtin/vgit.nvim/current_hunk_mark_ns_id'
- ),
- blame_line_id = 1,
-})
-
-M.render_current_hunk_mark = function(buf, selected, num_hunks)
- M.hide_current_hunk_mark(buf)
- scheduler()
- local epoch = 1000
- if current_hunk_mark_timer_id then
- vim.fn.timer_stop(current_hunk_mark_timer_id)
- current_hunk_mark_timer_id = nil
- end
- local cursor = vim.api.nvim_win_get_cursor(0)
- scheduler()
- virtual_text.transpose_text(
- buf,
- string.format(' %s/%s Changes ', selected, num_hunks),
- M.constants.current_hunk_mark_ns_id,
- 'Comment',
- cursor[1] - 1,
- 0,
- 'right_align'
- )
- current_hunk_mark_timer_id = vim.fn.timer_start(
- epoch,
- void(function()
- if buffer.is_valid(buf) then
- scheduler()
- virtual_text.clear(buf, M.constants.current_hunk_mark_ns_id)
- end
- vim.fn.timer_stop(current_hunk_mark_timer_id)
- current_hunk_mark_timer_id = nil
- end)
- )
-end
-
-M.render_blame_line = function(buf, blame, lnum, git_config)
- if buffer.is_valid(buf) then
- local virt_text = render_store.get('line_blame').format(blame, git_config)
- if type(virt_text) == 'string' then
- pcall(virtual_text.add, buf, M.constants.blame_ns_id, lnum - 1, 0, {
- id = M.constants.blame_line_id,
- virt_text = { { virt_text, render_store.get('line_blame').hl } },
- virt_text_pos = 'eol',
- hl_mode = 'combine',
- })
- end
- end
-end
-
-M.render_hunk_signs = function(buf, hunks)
- scheduler()
- if buffer.is_valid(buf) then
- for i = 1, #hunks do
- scheduler()
- local hunk = hunks[i]
- for j = hunk.start, hunk.finish do
- sign.place(
- buf,
- (hunk.type == 'remove' and j == 0) and 1 or j,
- render_store.get('sign').hls[hunk.type],
- render_store.get('sign').priority
- )
- scheduler()
- end
- scheduler()
- end
- end
-end
-
-M.render_blame_preview = function(fetch)
- preview_store.clear()
- local blame_preview = BlamePreview:new()
- preview_store.set(blame_preview)
- blame_preview:mount()
- blame_preview:set_loading(true)
- scheduler()
- local err, data = fetch()
- scheduler()
- blame_preview:set_loading(false)
- scheduler()
- blame_preview.err = err
- blame_preview.data = data
- blame_preview:render()
- scheduler()
-end
-
-M.render_gutter_blame_preview = function(fetch, filetype)
- preview_store.clear()
- local gutter_blame_preview = GutterBlamePreview:new({ filetype = filetype })
- preview_store.set(gutter_blame_preview)
- gutter_blame_preview:mount()
- gutter_blame_preview:set_loading(true)
- scheduler()
- local err, data = fetch()
- scheduler()
- gutter_blame_preview:set_loading(false)
- scheduler()
- gutter_blame_preview.err = err
- gutter_blame_preview.data = data
- gutter_blame_preview:render()
- scheduler()
-end
-
-M.render_hunk_preview = function(fetch, filetype)
- preview_store.clear()
- local current_lnum = vim.api.nvim_win_get_cursor(0)[1]
- local hunk_preview = HunkPreview:new({ filetype = filetype })
- preview_store.set(hunk_preview)
- hunk_preview:mount()
- hunk_preview:set_loading(true)
- scheduler()
- local err, data = fetch()
- scheduler()
- hunk_preview:set_loading(false)
- scheduler()
- hunk_preview.err = err
- hunk_preview.data = data
- hunk_preview.selected = current_lnum
- hunk_preview:render()
- scheduler()
-end
-
-M.render_diff_preview = function(fetch, filetype, layout_type)
- preview_store.clear()
- local current_lnum = vim.api.nvim_win_get_cursor(0)[1]
- local diff_preview = DiffPreview:new({
- filetype = filetype,
- layout_type = layout_type,
- temporary = layout_type == 'horizontal',
- })
- preview_store.set(diff_preview)
- diff_preview:mount()
- diff_preview:set_loading(true)
- scheduler()
- local err, data = fetch()
- scheduler()
- diff_preview:set_loading(false)
- scheduler()
- diff_preview.err = err
- diff_preview.data = data
- diff_preview.selected = current_lnum
- diff_preview:render()
- scheduler()
-end
-
-M.render_history_preview = function(fetch, filetype, layout_type)
- preview_store.clear()
- local history_preview = HistoryPreview:new({
- filetype = filetype,
- layout_type = layout_type,
- selected = 0,
- })
- preview_store.set(history_preview)
- history_preview:mount()
- history_preview:set_loading(true)
- scheduler()
- local err, data = fetch()
- scheduler()
- history_preview:set_loading(false)
- scheduler()
- history_preview.err = err
- history_preview.data = data
- history_preview:render()
- scheduler()
-end
-
-M.rerender_history_preview = function(fetch, selected)
- selected = selected - 1
- local history_preview = preview_store.get()
- scheduler()
- if history_preview.selected == selected then
- return
- end
- scheduler()
- history_preview:set_loading(true)
- scheduler()
- local err, data = fetch()
- scheduler()
- history_preview:set_loading(false)
- scheduler()
- history_preview.err = err
- history_preview.data = data
- history_preview.selected = selected
- history_preview:render()
- scheduler()
-end
-
-M.render_project_diff_preview = function(fetch, layout_type)
- preview_store.clear()
- local project_diff_preview = ProjectDiffPreview:new({
- layout_type = layout_type,
- selected = 0,
- })
- preview_store.set(project_diff_preview)
- project_diff_preview:mount()
- project_diff_preview:set_loading(true)
- scheduler()
- local err, data = fetch()
- scheduler()
- project_diff_preview:set_loading(false)
- scheduler()
- project_diff_preview.err = err
- project_diff_preview.data = data
- project_diff_preview:render()
- scheduler()
-end
-
-M.rerender_project_diff_preview = function(fetch, selected)
- selected = selected - 1
- local project_diff_preview = preview_store.get()
- scheduler()
- if project_diff_preview.selected == selected then
- local data = project_diff_preview.data
- if not data then
- return
- end
- local changed_files = data.changed_files
- if not changed_files then
- return
- end
- local changed_file = changed_files[selected + 1]
- if not changed_file then
- return
- end
- local invalid_status = {
- ['AD'] = true,
- [' D'] = true,
- }
- if invalid_status[changed_file.status] then
- return
- end
- M.hide_preview()
- vim.cmd(string.format('e %s', changed_file.filename))
- return
- end
- project_diff_preview:set_loading(true)
- scheduler()
- local err, data = fetch()
- scheduler()
- project_diff_preview:set_loading(false)
- scheduler()
- project_diff_preview.err = err
- project_diff_preview.data = data
- project_diff_preview.selected = selected
- project_diff_preview:render()
- scheduler()
-end
-
-M.hide_current_hunk_mark = function(buf)
- if current_hunk_mark_timer_id then
- vim.fn.timer_stop(current_hunk_mark_timer_id)
- current_hunk_mark_timer_id = nil
- if buffer.is_valid(buf) then
- virtual_text.clear(buf, M.constants.current_hunk_mark_ns_id)
- end
- end
-end
-
-M.hide_blame_line = function(buf)
- if buffer.is_valid(buf) then
- pcall(
- virtual_text.delete,
- buf,
- M.constants.blame_ns_id,
- M.constants.blame_line_id
- )
- end
-end
-
-M.hide_hunk_signs = function(buf)
- scheduler()
- if buffer.is_valid(buf) then
- sign.unplace(buf)
- scheduler()
- end
-end
-
-M.hide_preview = function()
- local preview = preview_store.get()
- if not vim.tbl_isempty(preview) then
- preview:unmount()
- preview_store.set({})
- end
-end
-
-M.hide_windows = function(wins)
- local preview = preview_store.get()
- if not vim.tbl_isempty(preview) then
- preview_store.clear()
- end
- local existing_wins = vim.api.nvim_list_wins()
- for i = 1, #wins do
- local win = wins[i]
- if
- vim.api.nvim_win_is_valid(win) and vim.tbl_contains(existing_wins, win)
- then
- pcall(vim.api.nvim_win_close, win, true)
- end
- end
-end
-
-return M
diff --git a/lua/vgit/settings/hls.lua b/lua/vgit/settings/hls.lua
new file mode 100644
index 00000000..653de89d
--- /dev/null
+++ b/lua/vgit/settings/hls.lua
@@ -0,0 +1,52 @@
+local Config = require('vgit.core.Config')
+
+return Config:new({
+ GitBackgroundPrimary = 'NormalFloat',
+ GitBackgroundSecondary = {
+ gui = nil,
+ fg = nil,
+ bg = nil,
+ sp = nil,
+ override = false,
+ },
+ GitBorder = 'LineNr',
+ GitLineNr = 'LineNr',
+ GitComment = 'Comment',
+ GitSignsAdd = {
+ gui = nil,
+ fg = '#d7ffaf',
+ bg = nil,
+ sp = nil,
+ override = false,
+ },
+ GitSignsChange = {
+ gui = nil,
+ fg = '#7AA6DA',
+ bg = nil,
+ sp = nil,
+ override = false,
+ },
+ GitSignsDelete = {
+ gui = nil,
+ fg = '#e95678',
+ bg = nil,
+ sp = nil,
+ override = false,
+ },
+ GitSignsAddLn = 'DiffAdd',
+ GitSignsDeleteLn = 'DiffDelete',
+ GitWordAdd = {
+ gui = nil,
+ fg = nil,
+ bg = '#5d7a22',
+ sp = nil,
+ override = false,
+ },
+ GitWordDelete = {
+ gui = nil,
+ fg = nil,
+ bg = '#960f3d',
+ sp = nil,
+ override = false,
+ },
+})
diff --git a/lua/vgit/settings/live_blame.lua b/lua/vgit/settings/live_blame.lua
new file mode 100644
index 00000000..4b32c3c0
--- /dev/null
+++ b/lua/vgit/settings/live_blame.lua
@@ -0,0 +1,46 @@
+local Config = require('vgit.core.Config')
+local utils = require('vgit.core.utils')
+
+return Config:new({
+ enabled = true,
+ debounce_ms = 300,
+ format = function(blame, git_config)
+ local config_author = git_config['user.name']
+ local author = blame.author
+ if config_author == author then
+ author = 'You'
+ end
+ local time = os.difftime(os.time(), blame.author_time) / (24 * 60 * 60)
+ local time_format = string.format('%s days ago', utils.round(time))
+ local time_divisions = {
+ { 24, 'hours' },
+ { 60, 'minutes' },
+ { 60, 'seconds' },
+ }
+ local division_counter = 1
+ while time < 1 and division_counter ~= #time_divisions do
+ local division = time_divisions[division_counter]
+ time = time * division[1]
+ time_format = string.format('%s %s ago', utils.round(time), division[2])
+ division_counter = division_counter + 1
+ end
+ local commit_message = blame.commit_message
+ if not blame.committed then
+ author = 'You'
+ commit_message = 'Uncommitted changes'
+ local info = string.format('%s • %s', author, commit_message)
+ return string.format(' %s', info)
+ end
+ local max_commit_message_length = 255
+ if #commit_message > max_commit_message_length then
+ commit_message = commit_message:sub(1, max_commit_message_length) .. '...'
+ end
+ local info = string.format(
+ '%s, %s • %s',
+ author,
+ time_format,
+ commit_message
+ )
+ return string.format(' %s', info)
+ end,
+})
diff --git a/lua/vgit/settings/live_gutter.lua b/lua/vgit/settings/live_gutter.lua
new file mode 100644
index 00000000..49135515
--- /dev/null
+++ b/lua/vgit/settings/live_gutter.lua
@@ -0,0 +1,6 @@
+local Config = require('vgit.core.Config')
+
+return Config:new({
+ enabled = true,
+ debounce_ms = 50,
+})
diff --git a/lua/vgit/settings/scene.lua b/lua/vgit/settings/scene.lua
new file mode 100644
index 00000000..7420fb49
--- /dev/null
+++ b/lua/vgit/settings/scene.lua
@@ -0,0 +1,5 @@
+local Config = require('vgit.core.Config')
+
+return Config:new({
+ diff_preference = 'unified',
+})
diff --git a/lua/vgit/settings/signs.lua b/lua/vgit/settings/signs.lua
new file mode 100644
index 00000000..7500bc5c
--- /dev/null
+++ b/lua/vgit/settings/signs.lua
@@ -0,0 +1,53 @@
+local Config = require('vgit.core.Config')
+
+return Config:new({
+ priority = 10,
+ definitions = {
+ GitSignsAddLn = {
+ linehl = 'GitSignsAddLn',
+ texthl = nil,
+ numhl = nil,
+ icon = nil,
+ text = '',
+ },
+ GitSignsDeleteLn = {
+ linehl = 'GitSignsDeleteLn',
+ texthl = nil,
+ numhl = nil,
+ icon = nil,
+ text = '',
+ },
+ GitSignsAdd = {
+ texthl = 'GitSignsAdd',
+ numhl = nil,
+ icon = nil,
+ linehl = nil,
+ text = '┃',
+ },
+ GitSignsDelete = {
+ texthl = 'GitSignsDelete',
+ numhl = nil,
+ icon = nil,
+ linehl = nil,
+ text = '┃',
+ },
+ GitSignsChange = {
+ texthl = 'GitSignsChange',
+ numhl = nil,
+ icon = nil,
+ linehl = nil,
+ text = '┃',
+ },
+ },
+ usage = {
+ scene = {
+ add = 'GitSignsAddLn',
+ remove = 'GitSignsDeleteLn',
+ },
+ main = {
+ add = 'GitSignsAdd',
+ remove = 'GitSignsDelete',
+ change = 'GitSignsChange',
+ },
+ },
+})
diff --git a/lua/vgit/settings/symbols.lua b/lua/vgit/settings/symbols.lua
new file mode 100644
index 00000000..4484b1f7
--- /dev/null
+++ b/lua/vgit/settings/symbols.lua
@@ -0,0 +1,7 @@
+local Config = require('vgit.core.Config')
+
+return Config:new({
+ symbols = {
+ void = '⣿',
+ },
+})
diff --git a/lua/vgit/sign.lua b/lua/vgit/sign.lua
deleted file mode 100644
index 5a457c11..00000000
--- a/lua/vgit/sign.lua
+++ /dev/null
@@ -1,102 +0,0 @@
-local utils = require('vgit.utils')
-local render_store = require('vgit.stores.render_store')
-local Interface = require('vgit.Interface')
-
-local M = {}
-
-M.constants = utils.readonly({
- ns_id = 'tanvirtin/vgit.nvim/hunk/signs',
-})
-
-M.state = Interface:new({
- VGitViewSignAdd = {
- name = render_store.get('preview').sign.hls.add,
- line_hl = render_store.get('preview').sign.hls.add,
- text_hl = nil,
- num_hl = nil,
- icon = nil,
- text = '',
- },
- VGitViewSignRemove = {
- name = render_store.get('preview').sign.hls.remove,
- line_hl = render_store.get('preview').sign.hls.remove,
- text_hl = nil,
- num_hl = nil,
- icon = nil,
- text = '',
- },
- VGitSignAdd = {
- name = render_store.get('sign').hls.add,
- text_hl = render_store.get('sign').hls.add,
- num_hl = nil,
- icon = nil,
- line_hl = nil,
- text = '┃',
- },
- VGitSignRemove = {
- name = render_store.get('sign').hls.remove,
- text_hl = render_store.get('sign').hls.remove,
- num_hl = nil,
- icon = nil,
- line_hl = nil,
- text = '┃',
- },
- VGitSignChange = {
- name = render_store.get('sign').hls.change,
- text_hl = render_store.get('sign').hls.change,
- num_hl = nil,
- icon = nil,
- line_hl = nil,
- text = '┃',
- },
-})
-
-M.setup = function(config)
- M.state:assign((config and config.signs) or config)
- for _, action in pairs(M.state.data) do
- M.define(action)
- end
-end
-
-M.define = function(config)
- vim.fn.sign_define(config.name, {
- text = config.text,
- texthl = config.text_hl,
- numhl = config.num_hl,
- icon = config.icon,
- linehl = config.line_hl,
- })
-end
-
-M.place = function(buf, lnum, type, priority)
- vim.fn.sign_place(
- lnum,
- string.format('%s/%s', M.constants.ns_id, buf),
- type,
- buf,
- {
- id = lnum,
- lnum = lnum,
- priority = priority,
- }
- )
-end
-
-M.unplace = function(buf)
- vim.fn.sign_unplace(string.format('%s/%s', M.constants.ns_id, buf))
-end
-
-M.get = function(buf, lnum)
- local signs = vim.fn.sign_getplaced(buf, {
- group = string.format('%s/%s', M.constants.ns_id, buf),
- id = lnum,
- })[1].signs
- local result = {}
- for i = 1, #signs do
- local sign = signs[i]
- result[i] = sign.name
- end
- return result
-end
-
-return M
diff --git a/lua/vgit/stores/buffer_store.lua b/lua/vgit/stores/buffer_store.lua
deleted file mode 100644
index 5d2ad50f..00000000
--- a/lua/vgit/stores/buffer_store.lua
+++ /dev/null
@@ -1,70 +0,0 @@
-local assert = require('vgit.assertion').assert
-local Interface = require('vgit.Interface')
-
-local M = {}
-
-M.state = Interface:new({
- data = {},
-})
-
-M.contains = function(buf)
- assert(type(buf) == 'number', 'type error :: expected number')
- return M.state:get('data')[buf] ~= nil
-end
-
-M.add = function(buf)
- assert(type(buf) == 'number', 'type error :: expected number')
- M.state:get('data')[buf] = Interface:new({
- filename = '',
- filetype = '',
- tracked_filename = '',
- tracked_remote_filename = '',
- logs = {},
- hunks = {},
- blames = {},
- disabled = false,
- last_lnum_blamed = 1,
- temp_lines = {},
- untracked = false,
- })
-end
-
-M.remove = function(buf)
- assert(type(buf) == 'number', 'type error :: expected number')
- local bcache = M.state:get('data')[buf]
- assert(bcache ~= nil, 'untracked buffer')
- M.state:get('data')[buf] = nil
-end
-
-M.get = function(buf, key)
- assert(type(buf) == 'number', 'type error :: expected number')
- assert(type(key) == 'string', 'type error :: expected string')
- local bcache = M.state:get('data')[buf]
- assert(bcache ~= nil, 'untracked buffer')
- return bcache:get(key)
-end
-
-M.set = function(buf, key, value)
- assert(type(buf) == 'number', 'type error :: expected number')
- assert(type(key) == 'string', 'type error :: expected string')
- local bcache = M.state:get('data')[buf]
- assert(bcache ~= nil, 'untracked buffer')
- bcache:set(key, value)
-end
-
-M.for_each = function(fn)
- assert(type(fn) == 'function', 'type error :: expected function')
- for key, value in pairs(M.state:get('data')) do
- fn(key, value)
- end
-end
-
-M.get_data = function()
- return M.state:get('data')
-end
-
-M.size = function()
- return #M.state:get('data')
-end
-
-return M
diff --git a/lua/vgit/stores/controller_store.lua b/lua/vgit/stores/controller_store.lua
deleted file mode 100644
index 88092b67..00000000
--- a/lua/vgit/stores/controller_store.lua
+++ /dev/null
@@ -1,32 +0,0 @@
-local Interface = require('vgit.Interface')
-
-local M = {}
-
-M.state = Interface:new({
- config = {},
- disabled = false,
- hunks_enabled = true,
- blames_enabled = true,
- diff_strategy = 'index',
- diff_preference = 'horizontal',
- predict_hunk_signs = true,
- action_delay_ms = 300,
- predict_hunk_throttle_ms = 300,
- predict_hunk_max_lines = 50000,
- blame_line_throttle_ms = 150,
-})
-
-M.setup = function(config)
- config = config or {}
- M.state:assign(config.controller)
-end
-
-M.get = function(key)
- return M.state:get(key)
-end
-
-M.set = function(key, value)
- return M.state:set(key, value)
-end
-
-return M
diff --git a/lua/vgit/stores/preview_store.lua b/lua/vgit/stores/preview_store.lua
deleted file mode 100644
index a9edb73d..00000000
--- a/lua/vgit/stores/preview_store.lua
+++ /dev/null
@@ -1,29 +0,0 @@
-local Interface = require('vgit.Interface')
-
-local M = {}
-
-M.state = Interface:new({
- current = {},
-})
-
-M.get = function()
- return M.state:get('current')
-end
-
-M.set = function(component)
- assert(type(component) == 'table', 'type error :: expected table')
- M.state:set('current', component)
-end
-
-M.exists = function()
- return not vim.tbl_isempty(M.get())
-end
-
-M.clear = function()
- if M.exists() then
- M.get():unmount()
- M.state:set('current', {})
- end
-end
-
-return M
diff --git a/lua/vgit/stores/render_store.lua b/lua/vgit/stores/render_store.lua
deleted file mode 100644
index 75edc35c..00000000
--- a/lua/vgit/stores/render_store.lua
+++ /dev/null
@@ -1,91 +0,0 @@
-local utils = require('vgit.utils')
-local Interface = require('vgit.Interface')
-
-local M = {}
-
-local virtual_line_nr_width = 6
-
-M.state = Interface:new({
- layout = require('vgit.layouts.default'),
- preview = {
- indicator_hl = 'VGitIndicator',
- virtual_line_nr_width = virtual_line_nr_width,
- sign = {
- priority = 10,
- hls = {
- add = 'DiffAdd',
- remove = 'DiffDelete',
- },
- },
- symbols = {
- void = '⣿',
- indicator = ' ❯',
- },
- },
- sign = {
- priority = 10,
- hls = {
- add = 'VGitSignAdd',
- remove = 'VGitSignRemove',
- change = 'VGitSignChange',
- },
- },
- line_blame = {
- hl = 'Comment',
- format = function(blame, git_config)
- local config_author = git_config['user.name']
- local author = blame.author
- if config_author == author then
- author = 'You'
- end
- local time = os.difftime(os.time(), blame.author_time) / (24 * 60 * 60)
- local time_format = string.format('%s days ago', utils.round(time))
- local time_divisions = {
- { 24, 'hours' },
- { 60, 'minutes' },
- { 60, 'seconds' },
- }
- local division_counter = 1
- while time < 1 and division_counter ~= #time_divisions do
- local division = time_divisions[division_counter]
- time = time * division[1]
- time_format = string.format('%s %s ago', utils.round(time), division[2])
- division_counter = division_counter + 1
- end
- local commit_message = blame.commit_message
- if not blame.committed then
- author = 'You'
- commit_message = 'Uncommitted changes'
- local info = string.format('%s • %s', author, commit_message)
- return string.format(' %s', info)
- end
- local max_commit_message_length = 255
- if #commit_message > max_commit_message_length then
- commit_message = commit_message:sub(1, max_commit_message_length)
- .. '...'
- end
- local info = string.format(
- '%s, %s • %s',
- author,
- time_format,
- commit_message
- )
- return string.format(' %s', info)
- end,
- },
-})
-
-M.setup = function(config)
- config = config or {}
- M.state:assign(config.render)
-end
-
-M.get = function(key)
- return M.state:get(key)
-end
-
-M.set = function(key, value)
- return M.state:set(key, value)
-end
-
-return M
diff --git a/lua/vgit/themes/init.lua b/lua/vgit/themes/init.lua
deleted file mode 100644
index 03dd68cf..00000000
--- a/lua/vgit/themes/init.lua
+++ /dev/null
@@ -1,4 +0,0 @@
-return {
- monokai = require('vgit.themes.monokai'),
- tokyonight = require('vgit.themes.tokyonight'),
-}
diff --git a/lua/vgit/themes/monokai.lua b/lua/vgit/themes/monokai.lua
deleted file mode 100644
index ee245d4b..00000000
--- a/lua/vgit/themes/monokai.lua
+++ /dev/null
@@ -1,34 +0,0 @@
-return {
- VGitViewWordAdd = {
- bg = '#5d7a22',
- fg = nil,
- },
- VGitViewWordRemove = {
- bg = '#960f3d',
- fg = nil,
- },
- VGitSignAdd = {
- fg = '#d7ffaf',
- bg = nil,
- },
- VGitSignChange = {
- fg = '#7AA6DA',
- bg = nil,
- },
- VGitSignRemove = {
- fg = '#e95678',
- bg = nil,
- },
- VGitIndicator = {
- fg = '#f92672',
- bg = nil,
- },
- VGitStatus = {
- fg = '#bb9af7',
- bg = '#3b4261',
- },
- VGitBorder = {
- fg = '#504945',
- bg = nil,
- },
-}
diff --git a/lua/vgit/themes/tokyonight.lua b/lua/vgit/themes/tokyonight.lua
deleted file mode 100644
index 7cec5b21..00000000
--- a/lua/vgit/themes/tokyonight.lua
+++ /dev/null
@@ -1,34 +0,0 @@
-return {
- VGitViewWordAdd = {
- bg = '#344e5e',
- fg = nil,
- },
- VGitViewWordRemove = {
- bg = '#543243',
- fg = nil,
- },
- VGitSignAdd = {
- fg = '#266d6a',
- bg = nil,
- },
- VGitSignChange = {
- fg = '#536c9e',
- bg = nil,
- },
- VGitSignRemove = {
- fg = '#b2555b',
- bg = nil,
- },
- VGitIndicator = {
- fg = '#f92672',
- bg = nil,
- },
- VGitStatus = {
- fg = '#bb9af7',
- bg = '#3b4261',
- },
- VGitBorder = {
- fg = '#536c9e',
- bg = nil,
- },
-}
diff --git a/lua/vgit/ui/Component.lua b/lua/vgit/ui/Component.lua
new file mode 100644
index 00000000..0be672b9
--- /dev/null
+++ b/lua/vgit/ui/Component.lua
@@ -0,0 +1,214 @@
+local dimensions = require('vgit.ui.dimensions')
+local assertion = require('vgit.core.assertion')
+local utils = require('vgit.core.utils')
+local Object = require('vgit.core.Object')
+
+local Component = Object:extend()
+
+function Component:new(options)
+ options = options or {}
+ return setmetatable(
+ utils.object_assign({
+ buffer = nil,
+ window = nil,
+ ns_id = vim.api.nvim_create_namespace(''),
+ cache = {},
+ -- Elements are mini components which decorate a component.
+ elements = {},
+ -- The properties that can be used to align components with each other.
+ window_props = {},
+ config = {
+ filetype = '',
+ border = {
+ hl = 'GitBorder',
+ chars = { '', '', '', '', '', '', '', '' },
+ },
+ buf_options = {
+ modifiable = false,
+ buflisted = false,
+ bufhidden = 'wipe',
+ },
+ win_options = {
+ wrap = false,
+ number = false,
+ winhl = 'Normal:VGitBackgroundPrimary',
+ cursorline = false,
+ cursorbind = false,
+ scrollbind = false,
+ signcolumn = 'auto',
+ },
+ window_props = {
+ style = 'minimal',
+ relative = 'editor',
+ height = 20,
+ width = dimensions.global_width(),
+ row = 0,
+ col = 0,
+ focusable = true,
+ focus = true,
+ zindex = 60,
+ },
+ locked = false,
+ },
+ }, options),
+ Component
+ )
+end
+
+function Component:set_width(width)
+ self.window:set_width(width)
+ return self
+end
+
+function Component:set_height(height)
+ self.window:set_height(height)
+ return self
+end
+
+function Component:set_window_props(window_props)
+ self.window:set_window_props(window_props)
+ return self
+end
+
+function Component:is_focused()
+ return self.window:is_focused()
+end
+
+function Component:make_border(config)
+ if config.hl then
+ local new_border = {}
+ for _, char in pairs(config.chars) do
+ if type(char) == 'table' then
+ char[2] = config.hl
+ new_border[#new_border + 1] = char
+ else
+ new_border[#new_border + 1] = { char, config.hl }
+ end
+ end
+ return new_border
+ end
+ return config.chars
+end
+
+function Component:mount()
+ assertion.assert('Not yet implemented', debug.traceback())
+end
+
+function Component:unmount()
+ assertion.assert('Not yet implemented', debug.traceback())
+end
+
+function Component:set_keymap(mode, key, action)
+ self.buffer:set_keymap(mode, key, action)
+ return self
+end
+
+function Component:set_cursor(cursor)
+ if not self.locked then
+ self.window:set_cursor(cursor)
+ end
+ return self
+end
+
+function Component:set_lnum(lnum)
+ if not self.locked then
+ self.window:set_lnum(lnum)
+ end
+ return self
+end
+
+function Component:reset_cursor()
+ return self.window:set_cursor({ 1, 1 })
+end
+
+function Component:get_lnum()
+ return self.window:get_lnum()
+end
+
+function Component:get_line_count()
+ return self.buffer:get_line_count()
+end
+
+function Component:set_filetype(filetype)
+ self.config.filetype = filetype
+ return self
+end
+
+function Component:get_filetype()
+ return self.config.filetype
+end
+
+function Component:add_syntax_highlights()
+ local buffer = self.buffer
+ local config = self.config
+ local filetype = config.filetype
+ if not filetype or filetype == '' then
+ return self
+ end
+ local has_ts = false
+ local ts_highlight = nil
+ local ts_parsers = nil
+ if not has_ts then
+ has_ts, _ = pcall(require, 'nvim-treesitter')
+ if has_ts then
+ _, ts_highlight = pcall(require, 'nvim-treesitter.highlight')
+ _, ts_parsers = pcall(require, 'nvim-treesitter.parsers')
+ end
+ end
+ if has_ts and filetype and filetype ~= '' then
+ local lang = ts_parsers.ft_to_lang(filetype)
+ if ts_parsers.has_parser(lang) then
+ pcall(ts_highlight.attach, buffer.bufnr, lang)
+ else
+ buffer:set_option('syntax', filetype)
+ end
+ end
+ return self
+end
+
+function Component:clear_syntax_highlights()
+ local has_ts = false
+ local buffer = self.buffer
+ if not has_ts then
+ has_ts, _ = pcall(require, 'nvim-treesitter')
+ end
+ if has_ts then
+ local active_buf = vim.treesitter.highlighter.active[buffer.bufnr]
+ if active_buf then
+ active_buf:destroy()
+ else
+ buffer:set_option('syntax', '')
+ end
+ end
+ return self
+end
+
+function Component:set_lines(lines, force)
+ if self.locked and not force then
+ return self
+ end
+ self.buffer:set_lines(lines)
+ return self
+end
+
+function Component:call(callback)
+ self.window:call(callback)
+ return self
+end
+
+function Component:lock()
+ self.locked = true
+ return self
+end
+
+function Component:unlock()
+ self.locked = false
+ return self
+end
+
+function Component:focus()
+ self.window:focus()
+ return self
+end
+
+return Component
diff --git a/lua/vgit/ui/Scene.lua b/lua/vgit/ui/Scene.lua
new file mode 100644
index 00000000..0dd47b01
--- /dev/null
+++ b/lua/vgit/ui/Scene.lua
@@ -0,0 +1,82 @@
+local Window = require('vgit.core.Window')
+local Object = require('vgit.core.Object')
+
+local Scene = Object:extend()
+
+function Scene:new(components)
+ return setmetatable({
+ components = components,
+ win_toggle_queue = {},
+ }, Scene)
+end
+
+function Scene:get_windows()
+ local windows = {}
+ for _, component in pairs(self.components) do
+ windows[#windows + 1] = component.window
+ end
+ return windows
+end
+
+function Scene:get_focused_component()
+ for _, component in pairs(self.components) do
+ if component:is_focused() then
+ return component
+ end
+ end
+end
+
+function Scene:get_focused_component_name()
+ for name, component in pairs(self.components) do
+ if component:is_focused() then
+ return name
+ end
+ end
+end
+
+function Scene:keep_focused()
+ local windows = self:get_windows()
+ local current_window = Window:new(0)
+ if #windows > 1 then
+ local found = false
+ for i = 1, #windows do
+ local window = windows[i]
+ if current_window:is_same(window) then
+ found = true
+ break
+ end
+ end
+ if not found then
+ if vim.tbl_isempty(self.win_toggle_queue) then
+ self.win_toggle_queue = self:get_windows()
+ end
+ local window = table.remove(self.win_toggle_queue)
+ if window:is_valid() then
+ window:focus()
+ end
+ else
+ self.win_toggle_queue = self:get_windows()
+ end
+ else
+ local window = windows[1]
+ if window:is_valid() then
+ window:focus()
+ end
+ end
+end
+
+function Scene:mount()
+ for _, component in pairs(self.components) do
+ component:mount()
+ end
+ return self
+end
+
+function Scene:unmount()
+ for _, component in pairs(self.components) do
+ component:unmount()
+ end
+ return self
+end
+
+return Scene
diff --git a/lua/vgit/ui/abstract_scenes/CodeDataScene.lua b/lua/vgit/ui/abstract_scenes/CodeDataScene.lua
new file mode 100644
index 00000000..ed4ef67d
--- /dev/null
+++ b/lua/vgit/ui/abstract_scenes/CodeDataScene.lua
@@ -0,0 +1,56 @@
+local console = require('vgit.core.console')
+local loop = require('vgit.core.loop')
+local CodeScene = require('vgit.ui.abstract_scenes.CodeScene')
+
+local CodeDataScene = CodeScene:extend()
+
+function CodeDataScene:new(...)
+ return setmetatable(CodeScene:new(...), CodeDataScene)
+end
+
+CodeDataScene.update = loop.debounce(
+ loop.async(function(self, selected)
+ local cache = self.cache
+ cache.last_selected = selected
+ self:fetch(selected)
+ loop.await_fast_event()
+ if cache.err then
+ console.error(cache.err)
+ return self
+ end
+ self:reset():set_title(cache.title, {
+ filename = cache.data.filename,
+ filetype = cache.data.filetype,
+ stat = cache.data.dto.stat,
+ })
+ self:make():paint():set_cursor_on_mark(1)
+ end),
+ 50
+)
+
+function CodeDataScene:table_move(direction)
+ self:clear_cached_err()
+ local components = self.scene.components
+ local table = components.table
+ loop.await_fast_event()
+ local selected = table:get_lnum()
+ if direction == 'up' then
+ selected = selected - 1
+ elseif direction == 'down' then
+ selected = selected + 1
+ end
+ local total_line_count = table:get_line_count()
+ if selected > total_line_count then
+ selected = 1
+ elseif selected < 1 then
+ selected = total_line_count
+ end
+ if self.cache.last_selected == selected then
+ return
+ end
+ loop.await_fast_event()
+ table:unlock():set_lnum(selected):lock()
+ self:update(selected)
+end
+
+return CodeDataScene
diff --git a/lua/vgit/ui/abstract_scenes/CodeScene.lua b/lua/vgit/ui/abstract_scenes/CodeScene.lua
new file mode 100644
index 00000000..9f08a171
--- /dev/null
+++ b/lua/vgit/ui/abstract_scenes/CodeScene.lua
@@ -0,0 +1,466 @@
+local sign = require('vgit.core.sign')
+local Diff = require('vgit.Diff')
+local LineNumberElement = require('vgit.ui.elements.LineNumberElement')
+local symbols_setting = require('vgit.settings.symbols')
+local assertion = require('vgit.core.assertion')
+local signs_setting = require('vgit.settings.signs')
+local virtual_text = require('vgit.core.virtual_text')
+local Object = require('vgit.core.Object')
+
+local CodeScene = Object:extend()
+
+function CodeScene:new(buffer_hunks, navigation, git_store, git)
+ return setmetatable({
+ buffer_hunks = buffer_hunks,
+ git_store = git_store,
+ navigation = navigation,
+ git = git,
+ layout_type = 'unified',
+ scene = nil,
+ cache = {
+ mark_index = 1,
+ ns_id = nil,
+ current_lines_metadata = {},
+ previous_lines_metadata = {},
+ },
+ }, CodeScene)
+end
+
+function CodeScene:generate_diff(hunks, lines)
+ local diff
+ if self.layout_type == 'unified' then
+ diff = Diff:new(hunks):unified(lines)
+ else
+ diff = Diff:new(hunks):split(lines)
+ end
+ return diff
+end
+
+function CodeScene:get_scene_options(...)
+ local scene_options = nil
+ if self.layout_type == 'unified' then
+ scene_options = self:get_unified_scene_options(...)
+ else
+ scene_options = self:get_split_scene_options(...)
+ end
+ return scene_options
+end
+
+function CodeScene:set_filetype()
+ local components = self.scene.components
+ local filetype = self.cache.data.filetype
+ if not filetype then
+ return self
+ end
+ local current = components.current
+ if current:get_filetype() == filetype then
+ return self
+ end
+ current
+ :clear_syntax_highlights()
+ :set_filetype(filetype)
+ :add_syntax_highlights()
+ if self.layout_type == 'split' then
+ components.previous
+ :clear_syntax_highlights()
+ :set_filetype(filetype)
+ :add_syntax_highlights()
+ end
+ return self
+end
+
+function CodeScene:reset()
+ self:reset_cursor()
+ return self
+end
+
+function CodeScene:reset_cursor()
+ local components = self.scene.components
+ components.current:reset_cursor()
+ if self.layout_type == 'split' then
+ components.previous:reset_cursor()
+ end
+ return self
+end
+
+function CodeScene:set_title(title, options)
+ local components = self.scene.components
+ if self.layout_type == 'split' then
+ components.previous:set_title(title, options)
+ else
+ components.current:set_title(title, options)
+ end
+ return self
+end
+
+function CodeScene:notify(text)
+ local components = self.scene.components
+ if self.layout_type == 'split' then
+ components.previous:notify(text)
+ else
+ components.current:notify(text)
+ end
+ return self
+end
+
+function CodeScene:navigate(direction)
+ local data = self.cache.data
+ if not data then
+ return self
+ end
+ if self.cache.err then
+ return self
+ end
+ local marks = data.dto.marks
+ local mark_index = nil
+ local components = self.scene.components
+ if #marks == 0 then
+ self:notify('There are no changes')
+ return self
+ end
+ local focused_component_name = self.scene:get_focused_component_name()
+ local component = components.current
+ local window = component.window
+ local buffer = component.buffer
+ if focused_component_name == 'table' then
+ local selected = self.cache.mark_index
+ if direction == 'up' then
+ selected = selected - 1
+ end
+ if direction == 'down' then
+ selected = selected + 1
+ end
+ if selected > #marks then
+ selected = 1
+ end
+ if selected < 1 then
+ selected = #marks
+ end
+ mark_index = self.navigation:mark_select(component, selected, marks, 'top')
+ else
+ if direction == 'up' then
+ mark_index = self.navigation:mark_up(window, buffer, marks)
+ end
+ if direction == 'down' then
+ mark_index = self.navigation:mark_down(window, buffer, marks)
+ end
+ end
+ if mark_index then
+ self.cache.mark_index = mark_index
+ self:notify(
+ string.format('%s%s/%s Changes', string.rep(' ', 1), mark_index, #marks)
+ )
+ return self
+ end
+ return self
+end
+
+function CodeScene:has_active_scene()
+ local scene = self.scene
+ return scene ~= nil and scene.mounted == true
+end
+
+function CodeScene:set_cursor(cursor)
+ local components = self.scene.components
+ components.current:set_cursor(cursor)
+ if self.layout_type == 'split' then
+ components.previous:set_cursor(cursor)
+ end
+ return self
+end
+
+function CodeScene:set_cursor_on_mark(selected, position)
+ if not position then
+ position = 'top'
+ end
+ local data = self.cache.data
+ if not data then
+ return self
+ end
+ if self.cache.err then
+ return self
+ end
+ local marks = data.dto.marks
+ if #marks == 0 then
+ self:notify('There are no changes')
+ return self
+ end
+ local component = self.scene.components.current
+ if not selected or selected > #marks then
+ selected = 1
+ end
+ local mark_index = self.navigation:mark_select(
+ component,
+ selected,
+ marks,
+ position
+ )
+ if mark_index then
+ self.cache.mark_index = selected
+ self:notify(
+ string.format('%s%s/%s Changes', string.rep(' ', 1), mark_index, #marks)
+ )
+ return self
+ end
+ return self
+end
+
+function CodeScene:make()
+ return self:make_lines():make_line_numbers()
+end
+
+function CodeScene:make_lines()
+ local components = self.scene.components
+ local dto = self.cache.data.dto
+ if self.layout_type == 'unified' then
+ components.current:set_lines(dto.lines)
+ else
+ components.previous:set_lines(dto.previous_lines)
+ components.current:set_lines(dto.current_lines)
+ end
+ self:set_filetype()
+ return self
+end
+
+function CodeScene:make_line_numbers()
+ local dto = self.cache.data.dto
+ local components = self.scene.components
+ local layout_type = self.layout_type or 'unified'
+ local line_metadata = {}
+ local lines = {}
+ local line_count = 1
+ if layout_type == 'unified' then
+ local component = components.current
+ local lnum_change_map = {}
+ for i = 1, #dto.lnum_changes do
+ local lnum_change = dto.lnum_changes[i]
+ lnum_change_map[lnum_change.lnum] = lnum_change
+ end
+ for i = 1, #dto.lines do
+ local line
+ local lnum_change = lnum_change_map[i]
+ if lnum_change and lnum_change.type == 'remove' then
+ line = ''
+ lines[#lines + 1] = line
+ else
+ line = string.format('%s ', line_count)
+ lines[#lines + 1] = line
+ line_count = line_count + 1
+ end
+ line_metadata[#line_metadata + 1] = {
+ lnum_change = lnum_change,
+ number_line = line,
+ }
+ end
+ self.cache.current_lines_metadata = line_metadata
+ component:make_line_numbers(lines)
+ elseif layout_type == 'split' then
+ local previous_component = components.previous
+ local current_component = components.current
+ local current_lnum_change_map = {}
+ local previous_lnum_change_map = {}
+ for i = 1, #dto.lnum_changes do
+ local lnum_change = dto.lnum_changes[i]
+ if lnum_change.buftype == 'current' then
+ current_lnum_change_map[lnum_change.lnum] = lnum_change
+ elseif lnum_change.buftype == 'previous' then
+ previous_lnum_change_map[lnum_change.lnum] = lnum_change
+ end
+ end
+ for i = 1, #dto.current_lines do
+ local line
+ local lnum_change = current_lnum_change_map[i]
+ if
+ lnum_change
+ and (lnum_change.type == 'remove' or lnum_change.type == 'void')
+ then
+ line = string.rep(
+ symbols_setting:get('symbols').void,
+ LineNumberElement:get_width()
+ )
+ lines[#lines + 1] = line
+ else
+ line = string.format('%s ', line_count)
+ lines[#lines + 1] = line
+ line_count = line_count + 1
+ end
+ line_metadata[#line_metadata + 1] = {
+ lnum_change = lnum_change,
+ number_line = line,
+ }
+ end
+ self.cache.current_lines_metadata = line_metadata
+ current_component:make_line_numbers(lines)
+ line_metadata = {}
+ lines = {}
+ line_count = 1
+ for i = 1, #dto.previous_lines do
+ local line
+ local lnum_change = previous_lnum_change_map[i]
+ if
+ lnum_change
+ and (lnum_change.type == 'add' or lnum_change.type == 'void')
+ then
+ line = string.rep(
+ symbols_setting:get('symbols').void,
+ LineNumberElement:get_width()
+ )
+ lines[#lines + 1] = line
+ else
+ line = string.format('%s ', line_count)
+ lines[#lines + 1] = line
+ line_count = line_count + 1
+ end
+ line_metadata[#line_metadata + 1] = {
+ lnum_change = lnum_change,
+ number_line = line,
+ }
+ end
+ self.cache.previous_lines_metadata = line_metadata
+ previous_component:make_line_numbers(lines)
+ end
+ return self
+end
+
+-- TODO: We can do our own optimization here.
+function CodeScene:paint_operation(lnum, metadata, component_type)
+ local lnum_change = metadata.lnum_change
+ local number_line = metadata.number_line
+ -- Line number operation
+ local line_number_hl = 'GitLineNr'
+ local signs_usage_setting = signs_setting:get('usage')
+ local scene_signs = signs_usage_setting.scene
+ local main_signs = signs_usage_setting.main
+ if lnum_change then
+ if lnum_change.type ~= 'void' then
+ line_number_hl = main_signs[lnum_change.type]
+ end
+ end
+ virtual_text.transpose_line(
+ self.scene.components[component_type].elements.line_number.buffer,
+ { { number_line, line_number_hl } },
+ self.scene.components[component_type].elements.line_number.ns_id,
+ lnum - 1,
+ 'right_align'
+ )
+ if not lnum_change then
+ return
+ end
+ local component = self.scene.components[lnum_change.buftype]
+ -- Line number operation
+ local defined_line_number_sign = scene_signs[lnum_change.type]
+ if defined_line_number_sign then
+ sign.place(
+ component.elements.line_number.buffer,
+ lnum,
+ defined_line_number_sign,
+ sign.priority
+ )
+ end
+ local ns_id = self.cache.ns_id
+ local buffer = component.buffer
+ local type = lnum_change.type
+ local word_diff = lnum_change.word_diff
+ lnum = lnum_change.lnum
+ local defined_sign = scene_signs[type]
+ if defined_sign then
+ sign.place(buffer, lnum, defined_sign, sign.priority)
+ end
+ if type == 'void' then
+ local void_line = string.rep(
+ symbols_setting:get('symbols').void,
+ component.window:get_width()
+ )
+ virtual_text.add(buffer, ns_id, lnum - 1, 0, {
+ id = lnum,
+ virt_text = { { void_line, line_number_hl } },
+ virt_text_pos = 'overlay',
+ })
+ end
+ -- Highlighting the word diff text here.
+ local texts = {}
+ if word_diff then
+ local offset = 0
+ for j = 1, #word_diff do
+ local segment = word_diff[j]
+ local operation, fragment = unpack(segment)
+ if operation == -1 then
+ local hl = type == 'remove' and 'GitWordDelete' or 'GitWordAdd'
+ texts[#texts + 1] = { fragment, hl }
+ elseif operation == 0 then
+ texts[#texts + 1] = {
+ fragment,
+ nil,
+ }
+ end
+ if operation == 0 or operation == -1 then
+ offset = offset + #fragment
+ end
+ end
+ virtual_text.transpose_line(buffer, texts, ns_id, lnum - 1)
+ end
+end
+
+function CodeScene:paint()
+ local layout_type = self.layout_type or 'unified'
+ self.cache.ns_id = vim.api.nvim_create_namespace('')
+ local current_lines_metadata = self.cache.current_lines_metadata
+ local previous_lines_metadata = self.cache.previous_lines_metadata
+ for i = 1, #current_lines_metadata do
+ self:paint_operation(i, current_lines_metadata[i], 'current')
+ if layout_type == 'split' then
+ self:paint_operation(i, previous_lines_metadata[i], 'previous')
+ end
+ end
+ return self
+end
+
+function CodeScene:show()
+ assertion.assert('Not yet implemented', debug.traceback())
+end
+
+function CodeScene:hide()
+ if self.scene then
+ self.scene:unmount()
+ end
+ self.scene = nil
+ return self
+end
+
+function CodeScene:clear_cached_err()
+ self.cache.err = nil
+ return self
+end
+
+function CodeScene:clear_cached_data()
+ self.cache.data = nil
+ return self
+end
+
+function CodeScene:clear_cache()
+ self.cache = {}
+ return self
+end
+
+function CodeScene:destroy()
+ self:hide()
+ self:clear_cache()
+ return self
+end
+
+function CodeScene:keep_focused()
+ if self.scene then
+ self.scene:keep_focused()
+ end
+ return self
+end
+
+function CodeScene:get_unified_scene_options()
+ assertion.assert('Not yet implemented', debug.traceback())
+end
+
+function CodeScene:get_split_scene_options()
+ assertion.assert('Not yet implemented', debug.traceback())
+end
+
+return CodeScene
diff --git a/lua/vgit/ui/active_scene.lua b/lua/vgit/ui/active_scene.lua
new file mode 100644
index 00000000..c43ad346
--- /dev/null
+++ b/lua/vgit/ui/active_scene.lua
@@ -0,0 +1,251 @@
+local Git = require('vgit.cli.Git')
+local scene_setting = require('vgit.settings.scene')
+local DiffScene = require('vgit.features.scenes.DiffScene')
+local HistoryScene = require('vgit.features.scenes.HistoryScene')
+local StagedDiffScene = require('vgit.features.scenes.StagedDiffScene')
+local ProjectDiffScene = require('vgit.features.scenes.ProjectDiffScene')
+local ProjectHunksScene = require('vgit.features.scenes.ProjectHunksScene')
+local GutterBlameScene = require('vgit.features.scenes.GutterBlameScene')
+local LineBlameScene = require('vgit.features.scenes.LineBlameScene')
+
+local current_scene
+local diff_scene
+local staged_diff_scene
+local history_scene
+local project_diff_scene
+local project_hunks_scene
+local gutter_blame_scene
+local line_blame_scene
+
+local active_scene = {}
+
+-- Factory and dependency injection
+active_scene.inject = function(buffer_hunks, navigation, git_store)
+ diff_scene = DiffScene:new(buffer_hunks, navigation, git_store, Git:new())
+ staged_diff_scene = StagedDiffScene:new(
+ buffer_hunks,
+ navigation,
+ git_store,
+ Git:new()
+ )
+ history_scene = HistoryScene:new(
+ buffer_hunks,
+ navigation,
+ git_store,
+ Git:new()
+ )
+ project_diff_scene = ProjectDiffScene:new(
+ buffer_hunks,
+ navigation,
+ git_store,
+ Git:new()
+ )
+ project_hunks_scene = ProjectHunksScene:new(
+ buffer_hunks,
+ navigation,
+ git_store,
+ Git:new()
+ )
+ gutter_blame_scene = GutterBlameScene:new(
+ buffer_hunks,
+ navigation,
+ git_store,
+ Git:new()
+ )
+ line_blame_scene = LineBlameScene:new(
+ buffer_hunks,
+ navigation,
+ git_store,
+ Git:new()
+ )
+end
+
+active_scene.diff_scene = function()
+ if active_scene.exists() then
+ return
+ end
+ diff_scene.layout_type = scene_setting:get('diff_preference')
+ local success = diff_scene:show('Diff')
+ if success then
+ current_scene = diff_scene
+ end
+end
+
+active_scene.staged_diff_scene = function()
+ if active_scene.exists() then
+ return
+ end
+ staged_diff_scene.layout_type = scene_setting:get('diff_preference')
+ local success = staged_diff_scene:show('Staged Diff')
+ if success then
+ current_scene = staged_diff_scene
+ end
+end
+
+active_scene.hunk_scene = function()
+ if active_scene.exists() then
+ return
+ end
+ diff_scene.layout_type = scene_setting:get('diff_preference')
+ local success = diff_scene:show('Hunk', {
+ config = {
+ window_props = {
+ relative = 'cursor',
+ height = 20,
+ },
+ },
+ })
+ if success then
+ current_scene = diff_scene
+ end
+end
+
+active_scene.staged_hunk_scene = function()
+ if active_scene.exists() then
+ return
+ end
+ staged_diff_scene.layout_type = scene_setting:get('diff_preference')
+ local success = staged_diff_scene:show('Staged Hunk', {
+ config = {
+ window_props = {
+ relative = 'cursor',
+ height = 20,
+ },
+ },
+ })
+ if success then
+ current_scene = staged_diff_scene
+ end
+end
+
+active_scene.project_diff_scene = function()
+ if active_scene.exists() then
+ return
+ end
+ project_diff_scene.layout_type = scene_setting:get('diff_preference')
+ local success = project_diff_scene:show('Project Diff')
+ if success then
+ current_scene = project_diff_scene
+ end
+end
+
+active_scene.project_hunks_scene = function()
+ if active_scene.exists() then
+ return
+ end
+ project_hunks_scene.layout_type = scene_setting:get('diff_preference')
+ local success = project_hunks_scene:show('Project Hunks')
+ if success then
+ current_scene = project_hunks_scene
+ end
+end
+
+active_scene.gutter_blame_scene = function()
+ if active_scene.exists() then
+ return
+ end
+ local success = gutter_blame_scene:show('Gutter Blame')
+ if success then
+ current_scene = gutter_blame_scene
+ end
+end
+
+active_scene.history_scene = function()
+ if active_scene.exists() then
+ return
+ end
+ history_scene.layout_type = scene_setting:get('diff_preference')
+ local success = history_scene:show('History')
+ if success then
+ current_scene = history_scene
+ end
+end
+
+active_scene.line_blame_scene = function()
+ if active_scene.exists() then
+ return
+ end
+ local success = line_blame_scene:show('History')
+ if success then
+ current_scene = line_blame_scene
+ end
+end
+
+active_scene.has_action = function(action)
+ return type(current_scene[action]) == 'function'
+end
+
+active_scene.on_enter = function()
+ if active_scene.has_action('table_change') then
+ current_scene:table_change()
+ end
+ if active_scene.has_action('open_file') then
+ current_scene:open_file()
+ end
+end
+
+active_scene.on_j = function()
+ if active_scene.has_action('table_move') then
+ current_scene:table_move('down')
+ end
+end
+
+active_scene.on_k = function()
+ if active_scene.has_action('table_move') then
+ current_scene:table_move('up')
+ end
+end
+
+active_scene.git_stage = function()
+ if active_scene.has_action('git_stage') then
+ current_scene:git_stage()
+ end
+end
+
+active_scene.git_unstage = function()
+ if active_scene.has_action('git_unstage') then
+ current_scene:git_unstage()
+ end
+end
+
+active_scene.git_reset = function()
+ if active_scene.has_action('git_reset') then
+ current_scene:git_reset()
+ end
+end
+
+active_scene.refresh = function()
+ if active_scene.has_action('refresh') then
+ current_scene:refresh()
+ end
+end
+
+active_scene.navigate = function(direction)
+ if active_scene.has_action('navigate') then
+ current_scene:navigate(direction)
+ end
+end
+
+active_scene.keep_focused = function()
+ current_scene:keep_focused()
+end
+
+active_scene.exists = function()
+ return current_scene ~= nil
+end
+
+active_scene.destroy = function()
+ current_scene:destroy()
+ current_scene = nil
+end
+
+active_scene.toggle_diff_preference = function()
+ local diff_preference = scene_setting:get('diff_preference')
+ if diff_preference == 'unified' then
+ scene_setting:set('diff_preference', 'split')
+ elseif diff_preference == 'split' then
+ scene_setting:set('diff_preference', 'unified')
+ end
+end
+
+return active_scene
diff --git a/lua/vgit/ui/components/CodeComponent.lua b/lua/vgit/ui/components/CodeComponent.lua
new file mode 100644
index 00000000..c88b249a
--- /dev/null
+++ b/lua/vgit/ui/components/CodeComponent.lua
@@ -0,0 +1,280 @@
+local highlighter = require('vgit.core.highlighter')
+local loop = require('vgit.core.loop')
+local icons = require('vgit.core.icons')
+local utils = require('vgit.core.utils')
+local LineNumberElement = require('vgit.ui.elements.LineNumberElement')
+local HeaderElement = require('vgit.ui.elements.HeaderElement')
+local HorizontalBorderElement = require(
+ 'vgit.ui.elements.HorizontalBorderElement'
+)
+local Component = require('vgit.ui.Component')
+local Window = require('vgit.core.Window')
+local dimensions = require('vgit.ui.dimensions')
+local Buffer = require('vgit.core.Buffer')
+
+local CodeComponent = Component:extend()
+
+function CodeComponent:new(options)
+ return setmetatable(
+ Component:new(utils.object_assign(options, {
+ elements = {
+ header = nil,
+ line_number = nil,
+ horizontal_border = nil,
+ },
+ })),
+ CodeComponent
+ )
+end
+
+function CodeComponent:set_cursor(cursor)
+ self.window:set_cursor(cursor)
+ self.elements.line_number:set_cursor(cursor)
+ return self
+end
+
+function CodeComponent:set_lnum(lnum)
+ self.window:set_lnum(lnum)
+ self.elements.line_number:set_lnum(lnum)
+ return self
+end
+
+function CodeComponent:call(callback)
+ self.window:call(callback)
+ self.elements.line_number:call(callback)
+ return self
+end
+
+function CodeComponent:reset_cursor()
+ self.window:set_cursor({ 1, 1 })
+ self.elements.line_number:reset_cursor()
+ return self
+end
+
+function CodeComponent:get_dimensions(window_props)
+ local is_at_cursor = window_props.relative == 'cursor'
+ local global_height = dimensions.global_height()
+ -- Element window props, these props will get modified below accordingly
+ local header_window_props = {
+ row = window_props.row,
+ col = window_props.col,
+ width = window_props.width,
+ }
+ local line_number_window_props = {
+ row = window_props.row,
+ col = window_props.col,
+ height = window_props.height,
+ }
+ local horizontal_border_window_props = {
+ row = window_props.row,
+ col = window_props.col,
+ width = window_props.width,
+ }
+
+ if is_at_cursor then
+ window_props.relative = 'editor'
+ window_props.row = vim.fn.screenrow()
+ header_window_props.row = window_props.row
+ end
+
+ if window_props.row + window_props.height >= global_height then
+ window_props.row = window_props.row
+ - (window_props.row + window_props.height - global_height)
+ header_window_props.row = window_props.row
+ if is_at_cursor then
+ local horizontal_border_height = HorizontalBorderElement:get_height()
+ window_props.row = window_props.row - horizontal_border_height
+ header_window_props.row = window_props.row
+ end
+ end
+
+ -- Height
+ local header_height = HeaderElement:get_height()
+ if window_props.height - header_height > 1 then
+ window_props.height = window_props.height - header_height
+ line_number_window_props.height = line_number_window_props.height
+ - header_height
+ end
+ local height = header_height + window_props.height
+ if is_at_cursor then
+ local horizontal_border_height = HorizontalBorderElement:get_height()
+ height = height + horizontal_border_height
+ window_props.height = window_props.height - horizontal_border_height
+ line_number_window_props.height = line_number_window_props.height
+ - horizontal_border_height
+ end
+
+ -- Width
+ local line_number_width = LineNumberElement:get_width()
+ window_props.width = window_props.width - line_number_width
+ local width = line_number_width + window_props.width
+
+ -- Row
+ window_props.row = window_props.row + header_height
+ line_number_window_props.row = window_props.row
+ horizontal_border_window_props.row = window_props.row
+ horizontal_border_window_props.row = horizontal_border_window_props.row
+ + window_props.height
+
+ -- Col
+ window_props.col = window_props.col + line_number_width
+
+ return {
+ is_at_cursor = is_at_cursor,
+ window_props = window_props,
+ header_window_props = header_window_props,
+ line_number_window_props = line_number_window_props,
+ horizontal_border_window_props = horizontal_border_window_props,
+ global_window_props = {
+ row = header_window_props.row,
+ col = line_number_window_props.col,
+ height = height,
+ width = width,
+ },
+ }
+end
+
+function CodeComponent:mount()
+ if self.mounted then
+ return self
+ end
+ local config = self.config
+ local component_dimensions = self:get_dimensions(config.window_props)
+ local window_props = component_dimensions.window_props
+ local header_window_props = component_dimensions.header_window_props
+ local line_number_window_props = component_dimensions.line_number_window_props
+ local horizontal_border_window_props =
+ component_dimensions.horizontal_border_window_props
+ local is_at_cursor = component_dimensions.is_at_cursor
+
+ self.buffer = Buffer:new():create():assign_options(config.buf_options)
+ local buffer = self.buffer
+
+ self.window = Window
+ :open(buffer, window_props)
+ :assign_options(config.win_options)
+ self.elements.header = HeaderElement:new():mount(header_window_props)
+ self.elements.line_number = LineNumberElement
+ :new()
+ :mount(line_number_window_props)
+ if is_at_cursor then
+ self.elements.horizontal_border = HorizontalBorderElement
+ :new()
+ :mount(horizontal_border_window_props)
+ end
+
+ self.mounted = true
+ self.component_dimensions = component_dimensions
+
+ self:add_syntax_highlights()
+
+ return self
+end
+
+function CodeComponent:unmount()
+ local header = self.elements.header
+ local line_number = self.elements.line_number
+ local horizontal_border = self.elements.horizontal_border
+ self.window:close()
+ if header then
+ header:unmount()
+ end
+ if line_number then
+ line_number:unmount()
+ end
+ if horizontal_border then
+ horizontal_border:unmount()
+ end
+ return self
+end
+
+function CodeComponent:set_title(title, opts)
+ opts = opts or {}
+ local filename = opts.filename
+ local filetype = opts.filetype
+ local stat = opts.stat
+ local header = self.elements.header
+ local text = title
+ if filename or filetype or stat then
+ text = utils.accumulate_string(title, ': ')
+ end
+ local hl_range_infos = {}
+ if filename then
+ text = utils.accumulate_string(text, filename)
+ text = utils.accumulate_string(text, ' ')
+ end
+ if filetype then
+ local icon, icon_hl = icons.file_icon(filename, filetype)
+ local new_text, hl_range = utils.accumulate_string(text, icon)
+ text = utils.accumulate_string(new_text, ' ')
+ hl_range_infos[#hl_range_infos + 1] = {
+ hl = icon_hl,
+ range = hl_range,
+ }
+ end
+ if stat then
+ local more_added = stat.added > stat.removed
+ local more_removed = stat.removed > stat.added
+ local new_text, hl_range = utils.accumulate_string(
+ text,
+ more_added and '++' or '+'
+ )
+ text = new_text
+ hl_range_infos[#hl_range_infos + 1] = {
+ hl = 'GitSignsAdd',
+ range = hl_range,
+ }
+ text = utils.accumulate_string(text, tostring(stat.added))
+ text = utils.accumulate_string(text, ' ')
+ new_text, hl_range = utils.accumulate_string(
+ text,
+ more_removed and '--' or '-'
+ )
+ text = new_text
+ hl_range_infos[#hl_range_infos + 1] = {
+ hl = 'GitSignsDelete',
+ range = hl_range,
+ }
+ text = utils.accumulate_string(text, tostring(stat.removed))
+ end
+ header:set_lines({ text })
+ for _, range_info in ipairs(hl_range_infos) do
+ local hl = range_info.hl
+ local range = range_info.range
+ highlighter.add(header.buffer, -1, hl, 0, range.start, range.finish)
+ end
+ return self
+end
+
+function CodeComponent:make_line_numbers(lines)
+ local line_number = self.elements.line_number
+ line_number:clear_hls()
+ line_number:make_lines(lines)
+ return self
+end
+
+function CodeComponent:clear_timer()
+ if self.timer_id then
+ vim.fn.timer_stop(self.timer_id)
+ self.timer_id = nil
+ end
+end
+
+function CodeComponent:notify(text)
+ local epoch = 2000
+ local header = self.elements.header
+ self:clear_timer()
+ header:transpose_virtual_text({ text, 'GitComment' }, 0, 0, 'eol')
+ self.timer_id = vim.fn.timer_start(
+ epoch,
+ loop.async(function()
+ if self.buffer:is_valid() then
+ header:clear_virtual_text()
+ end
+ self:clear_timer()
+ end)
+ )
+ return self
+end
+
+return CodeComponent
diff --git a/lua/vgit/ui/components/PopupComponent.lua b/lua/vgit/ui/components/PopupComponent.lua
new file mode 100644
index 00000000..4cdf5fbe
--- /dev/null
+++ b/lua/vgit/ui/components/PopupComponent.lua
@@ -0,0 +1,58 @@
+local utils = require('vgit.core.utils')
+local Component = require('vgit.ui.Component')
+local Window = require('vgit.core.Window')
+local Buffer = require('vgit.core.Buffer')
+
+local PopupComponent = Component:extend()
+
+function PopupComponent:new(options)
+ return setmetatable(Component:new(options), PopupComponent)
+end
+
+function PopupComponent:call(callback)
+ self.window:call(callback)
+ return self
+end
+
+function PopupComponent:get_dimensions(window_props)
+ return {
+ window_props = utils.object_assign(window_props, {
+ relative = 'cursor',
+ }),
+ global_window_props = window_props,
+ }
+end
+
+function PopupComponent:mount()
+ if self.mounted then
+ return self
+ end
+ local config = self.config
+ local component_dimensions = self:get_dimensions(config.window_props)
+ local window_props = component_dimensions.window_props
+
+ self.buffer = Buffer:new():create():assign_options(config.buf_options)
+
+ window_props.border = self:make_border({
+ hl = 'GitBorder',
+ chars = { '╭', '─', '╮', '│', '╯', '─', '╰', '│' },
+ })
+
+ self.window = Window
+ :open(self.buffer, window_props)
+ :assign_options(config.win_options)
+
+ self.mounted = true
+ self.component_dimensions = component_dimensions
+
+ self:add_syntax_highlights()
+
+ return self
+end
+
+function PopupComponent:unmount()
+ self.window:close()
+ return self
+end
+
+return PopupComponent
diff --git a/lua/vgit/ui/components/PresentationalComponent.lua b/lua/vgit/ui/components/PresentationalComponent.lua
new file mode 100644
index 00000000..6171f3d8
--- /dev/null
+++ b/lua/vgit/ui/components/PresentationalComponent.lua
@@ -0,0 +1,236 @@
+local highlighter = require('vgit.core.highlighter')
+local icons = require('vgit.core.icons')
+local utils = require('vgit.core.utils')
+local HeaderElement = require('vgit.ui.elements.HeaderElement')
+local HorizontalBorderElement = require(
+ 'vgit.ui.elements.HorizontalBorderElement'
+)
+local Component = require('vgit.ui.Component')
+local Window = require('vgit.core.Window')
+local dimensions = require('vgit.ui.dimensions')
+local Buffer = require('vgit.core.Buffer')
+
+local PresentationalComponent = Component:extend()
+
+function PresentationalComponent:new(options)
+ return setmetatable(
+ Component:new(utils.object_assign(options, {
+ elements = {
+ header = nil,
+ horizontal_border = nil,
+ },
+ })),
+ PresentationalComponent
+ )
+end
+
+function PresentationalComponent:set_cursor(cursor)
+ self.window:set_cursor(cursor)
+ return self
+end
+
+function PresentationalComponent:set_lnum(lnum)
+ self.window:set_lnum(lnum)
+ return self
+end
+
+function PresentationalComponent:call(callback)
+ self.window:call(callback)
+ return self
+end
+
+function PresentationalComponent:reset_cursor()
+ self.window:set_cursor({ 1, 1 })
+ return self
+end
+
+function PresentationalComponent:get_dimensions(window_props)
+ local is_at_cursor = window_props.relative == 'cursor'
+ local global_height = dimensions.global_height()
+ -- Element window props, these props will get modified below accordingly
+ local header_window_props = {
+ row = window_props.row,
+ col = window_props.col,
+ width = window_props.width,
+ }
+ local horizontal_border_window_props = {
+ row = window_props.row,
+ col = window_props.col,
+ width = window_props.width,
+ }
+
+ if is_at_cursor then
+ window_props.relative = 'editor'
+ window_props.row = vim.fn.screenrow()
+ header_window_props.row = window_props.row
+ end
+
+ if window_props.row + window_props.height >= global_height then
+ window_props.row = window_props.row
+ - (window_props.row + window_props.height - global_height)
+ header_window_props.row = window_props.row
+ if is_at_cursor then
+ local horizontal_border_height = HorizontalBorderElement:get_height()
+ window_props.row = window_props.row - horizontal_border_height
+ header_window_props.row = window_props.row
+ end
+ end
+
+ -- Height
+ local header_height = HeaderElement:get_height()
+ if window_props.height - header_height > 1 then
+ window_props.height = window_props.height - header_height
+ end
+ local height = header_height + window_props.height
+ if is_at_cursor then
+ local horizontal_border_height = HorizontalBorderElement:get_height()
+ height = height + horizontal_border_height
+ window_props.height = window_props.height - horizontal_border_height
+ end
+
+ -- Row
+ window_props.row = window_props.row + header_height
+ horizontal_border_window_props.row = window_props.row
+ horizontal_border_window_props.row = horizontal_border_window_props.row
+ + window_props.height
+
+ return {
+ is_at_cursor = is_at_cursor,
+ window_props = window_props,
+ header_window_props = header_window_props,
+ horizontal_border_window_props = horizontal_border_window_props,
+ global_window_props = {
+ row = header_window_props.row,
+ col = window_props.col,
+ height = height,
+ width = window_props.width,
+ },
+ }
+end
+
+function PresentationalComponent:mount()
+ if self.mounted then
+ return self
+ end
+ local config = self.config
+ local component_dimensions = self:get_dimensions(config.window_props)
+ local window_props = component_dimensions.window_props
+ local header_window_props = component_dimensions.header_window_props
+ local horizontal_border_window_props =
+ component_dimensions.horizontal_border_window_props
+ local is_at_cursor = component_dimensions.is_at_cursor
+
+ self.buffer = Buffer:new():create():assign_options(config.buf_options)
+ local buffer = self.buffer
+
+ self.window = Window
+ :open(buffer, window_props)
+ :assign_options(config.win_options)
+ self.elements.header = HeaderElement:new():mount(header_window_props)
+ if is_at_cursor then
+ self.elements.horizontal_border = HorizontalBorderElement
+ :new()
+ :mount(horizontal_border_window_props)
+ end
+
+ self.mounted = true
+ self.component_dimensions = component_dimensions
+
+ self:add_syntax_highlights()
+
+ return self
+end
+
+function PresentationalComponent:unmount()
+ local header = self.elements.header
+ local horizontal_border = self.elements.horizontal_border
+ self.window:close()
+ if header then
+ header:unmount()
+ end
+ if horizontal_border then
+ horizontal_border:unmount()
+ end
+ return self
+end
+
+function PresentationalComponent:set_title(title, opts)
+ opts = opts or {}
+ local filename = opts.filename
+ local filetype = opts.filetype
+ local stat = opts.stat
+ local header = self.elements.header
+ local text = title
+ if filename or filetype or stat then
+ text = utils.accumulate_string(title, ': ')
+ end
+ local hl_range_infos = {}
+ if filename then
+ text = utils.accumulate_string(text, filename)
+ text = utils.accumulate_string(text, ' ')
+ end
+ if filetype then
+ local icon, icon_hl = icons.file_icon(filename, filetype)
+ local new_text, hl_range = utils.accumulate_string(text, icon)
+ text = utils.accumulate_string(new_text, ' ')
+ hl_range_infos[#hl_range_infos + 1] = {
+ hl = icon_hl,
+ range = hl_range,
+ }
+ end
+ if stat then
+ local more_added = stat.added > stat.removed
+ local more_removed = stat.removed > stat.added
+ local new_text, hl_range = utils.accumulate_string(
+ text,
+ more_added and '++' or '+'
+ )
+ text = new_text
+ hl_range_infos[#hl_range_infos + 1] = {
+ hl = 'GitSignsAdd',
+ range = hl_range,
+ }
+ text = utils.accumulate_string(text, tostring(stat.added))
+ text = utils.accumulate_string(text, ' ')
+ new_text, hl_range = utils.accumulate_string(
+ text,
+ more_removed and '--' or '-'
+ )
+ text = new_text
+ hl_range_infos[#hl_range_infos + 1] = {
+ hl = 'GitSignsDelete',
+ range = hl_range,
+ }
+ text = utils.accumulate_string(text, tostring(stat.removed))
+ end
+ header:set_lines({ text })
+ for _, range_info in ipairs(hl_range_infos) do
+ local hl = range_info.hl
+ local range = range_info.range
+ highlighter.add(header.buffer, -1, hl, 0, range.start, range.finish)
+ end
+ return self
+end
+
+function PresentationalComponent:clear_timer()
+ if self.timer_id then
+ vim.fn.timer_stop(self.timer_id)
+ self.timer_id = nil
+ end
+end
+
+function PresentationalComponent:notify(text)
+ local epoch = 2000
+ local header = self.elements.header
+ self:clear_timer()
+ header:transpose_virtual_text({ text, 'GitComment' }, 0, 0, 'eol')
+ self.timer_id = vim.fn.timer_start(epoch, function()
+ if self.buffer:is_valid() then
+ header:clear_virtual_text()
+ end
+ self:clear_timer()
+ end)
+ return self
+end
+
+return PresentationalComponent
diff --git a/lua/vgit/ui/components/TableComponent.lua b/lua/vgit/ui/components/TableComponent.lua
new file mode 100644
index 00000000..596fc968
--- /dev/null
+++ b/lua/vgit/ui/components/TableComponent.lua
@@ -0,0 +1,216 @@
+local highlighter = require('vgit.core.highlighter')
+local utils = require('vgit.core.utils')
+local Component = require('vgit.ui.Component')
+local dimensions = require('vgit.ui.dimensions')
+local table_maker = require('vgit.ui.table_maker')
+local HorizontalBorderElement = require(
+ 'vgit.ui.elements.HorizontalBorderElement'
+)
+local HeaderElement = require('vgit.ui.elements.HeaderElement')
+local Buffer = require('vgit.core.Buffer')
+local Window = require('vgit.core.Window')
+
+local TableComponent = Component:extend()
+
+function TableComponent:new(options)
+ options = options or {}
+ return setmetatable(
+ Component:new(utils.object_assign(options, {
+ column_spacing = 3,
+ max_column_len = 80,
+ paddings = {},
+ elements = {
+ header = nil,
+ horizontal_border = nil,
+ },
+ config = {
+ win_options = {
+ cursorline = true,
+ },
+ },
+ })),
+ TableComponent
+ )
+end
+
+function TableComponent:get_column_ranges()
+ local column_ranges = {}
+ local paddings = self.paddings
+ local last_range = nil
+ for i = 1, #paddings do
+ if i == 1 then
+ column_ranges[#column_ranges + 1] = { 0, paddings[i] }
+ else
+ column_ranges[#column_ranges + 1] = {
+ last_range[2],
+ last_range[2] + paddings[i],
+ }
+ end
+ last_range = column_ranges[#column_ranges]
+ end
+ return column_ranges
+end
+
+function TableComponent:get_dimensions(window_props)
+ local global_height = dimensions.global_height()
+ local is_at_cursor = window_props.relative == 'cursor'
+
+ -- Element window props, these props will get modified below accordingly
+ local header_window_props = {
+ row = window_props.row,
+ col = window_props.col,
+ width = window_props.width,
+ }
+ local horizontal_border_window_props = {
+ row = window_props.row,
+ col = window_props.col,
+ width = window_props.width,
+ }
+
+ if is_at_cursor then
+ window_props.relative = 'editor'
+ window_props.row = vim.fn.screenrow()
+ header_window_props.row = window_props.row
+ end
+
+ if window_props.row + window_props.height >= global_height then
+ window_props.row = window_props.row
+ - (window_props.row + window_props.height - global_height)
+ header_window_props.row = window_props.row
+ if is_at_cursor then
+ local horizontal_border_height = HorizontalBorderElement:get_height()
+ window_props.row = window_props.row - horizontal_border_height
+ header_window_props.row = window_props.row
+ end
+ end
+
+ -- Height
+ local header_height = HeaderElement:get_height()
+ if window_props.height - header_height > 1 then
+ window_props.height = window_props.height - header_height
+ end
+ local height = header_height + window_props.height
+
+ -- Row
+ window_props.row = window_props.row + header_height
+ horizontal_border_window_props.row = window_props.row
+ horizontal_border_window_props.row = horizontal_border_window_props.row
+ + window_props.height
+
+ return {
+ is_at_cursor = is_at_cursor,
+ window_props = window_props,
+ header_window_props = header_window_props,
+ horizontal_border_window_props = horizontal_border_window_props,
+ global_window_props = {
+ row = header_window_props.row,
+ column = window_props.column,
+ height = height,
+ width = window_props.width,
+ },
+ }
+end
+
+function TableComponent:paint(hls)
+ for i = 1, #hls do
+ local hl_info = hls[i]
+ local hl = hl_info.hl
+ local range = hl_info.range
+ highlighter.add(
+ self.buffer,
+ -1,
+ hl,
+ hl_info.row - 1,
+ range.start,
+ range.finish
+ )
+ end
+end
+
+function TableComponent:set_lines(lines, force)
+ if self.locked and not force then
+ return self
+ end
+ local buffer = self.buffer
+ local column_spacing = self.column_spacing
+ local max_column_len = self.max_column_len
+ local header = self.header
+ local paddings = table_maker.make_paddings(
+ lines,
+ header,
+ column_spacing,
+ max_column_len
+ )
+ local column_header, _ = table_maker.make_heading(
+ header,
+ paddings,
+ column_spacing,
+ max_column_len
+ )
+ local rows, hls = table_maker.make_rows(
+ lines,
+ paddings,
+ column_spacing,
+ max_column_len
+ )
+ buffer:set_lines(rows)
+ self:paint(hls)
+ self.elements.header:set_lines(column_header)
+ self.paddings = paddings
+ return self
+end
+
+function TableComponent:make_rows(rows, format)
+ local formatted_row = {}
+ for i = 1, #rows do
+ local row = rows[i]
+ formatted_row[#formatted_row + 1] = format(row, i)
+ end
+ self:set_lines(formatted_row)
+ return self
+end
+
+function TableComponent:mount()
+ if self.mounted then
+ return self
+ end
+ local config = self.config
+ local component_dimensions = self:get_dimensions(config.window_props)
+ local is_at_cursor = component_dimensions.is_at_cursor
+ local window_props = component_dimensions.window_props
+ local header_window_props = component_dimensions.header_window_props
+ local horizontal_border_window_props =
+ component_dimensions.horizontal_border_window_props
+
+ self.buffer = Buffer:new():create():assign_options(config.buf_options)
+ local buffer = self.buffer
+
+ self.window = Window
+ :open(buffer, window_props)
+ :assign_options(config.win_options)
+ self.elements.header = HeaderElement:new():mount(header_window_props)
+ if is_at_cursor then
+ self.elements.horizontal_border = HorizontalBorderElement
+ :new()
+ :mount(horizontal_border_window_props)
+ end
+
+ self.mounted = true
+ self.component_dimensions = component_dimensions
+
+ return self
+end
+
+function TableComponent:unmount()
+ local header = self.elements.header
+ local horizontal_border = self.elements.horizontal_border
+ self.window:close()
+ if header then
+ header:unmount()
+ end
+ if horizontal_border then
+ horizontal_border:unmount()
+ end
+end
+
+return TableComponent
diff --git a/lua/vgit/dimensions.lua b/lua/vgit/ui/dimensions.lua
similarity index 91%
rename from lua/vgit/dimensions.lua
rename to lua/vgit/ui/dimensions.lua
index 743a147e..922087c2 100644
--- a/lua/vgit/dimensions.lua
+++ b/lua/vgit/ui/dimensions.lua
@@ -5,7 +5,7 @@ M.global_width = function()
end
M.global_height = function()
- return vim.o.lines
+ return vim.o.lines - 1
end
M.calculate_text_center = function(text, width)
diff --git a/lua/vgit/ui/elements/HeaderElement.lua b/lua/vgit/ui/elements/HeaderElement.lua
new file mode 100644
index 00000000..03629ab3
--- /dev/null
+++ b/lua/vgit/ui/elements/HeaderElement.lua
@@ -0,0 +1,101 @@
+local Buffer = require('vgit.core.Buffer')
+local Window = require('vgit.core.Window')
+local Object = require('vgit.core.Object')
+local virtual_text = require('vgit.core.virtual_text')
+
+local HeaderElement = Object:extend()
+
+function HeaderElement:new()
+ return setmetatable({
+ buffer = nil,
+ window = nil,
+ ns_id = vim.api.nvim_create_namespace(''),
+ }, HeaderElement)
+end
+
+function HeaderElement:make_border(c)
+ if c.hl then
+ local new_border = {}
+ for _, char in pairs(c.chars) do
+ if type(char) == 'table' then
+ char[2] = c.hl
+ new_border[#new_border + 1] = char
+ else
+ new_border[#new_border + 1] = { char, c.hl }
+ end
+ end
+ return new_border
+ end
+ return c.chars
+end
+
+function HeaderElement:mount(options)
+ self.buffer = Buffer:new():create()
+ local buffer = self.buffer
+ buffer:assign_options({
+ modifiable = false,
+ bufhidden = 'wipe',
+ buflisted = false,
+ })
+ self.window = Window
+ :open(buffer, {
+ border = self:make_border({
+ chars = { '─', '─', '─', ' ', '─', '─', '─', ' ' },
+ hl = 'GitBorder',
+ }),
+ style = 'minimal',
+ focusable = false,
+ relative = 'editor',
+ row = options.row,
+ col = options.col,
+ width = options.width - 2,
+ height = 1,
+ zindex = 100,
+ })
+ :assign_options({
+ cursorbind = false,
+ scrollbind = false,
+ winhl = 'Normal:VGitBackgroundSecondary',
+ })
+ return self
+end
+
+function HeaderElement:get_height()
+ return 3
+end
+
+function HeaderElement:unmount()
+ self.window:close()
+ return self
+end
+
+function HeaderElement:set_lines(lines)
+ self.buffer:set_lines(lines)
+ return self
+end
+
+function HeaderElement:transpose_virtual_text(text, row, col, pos)
+ virtual_text.transpose_text(
+ self.buffer,
+ text[1],
+ self.ns_id,
+ text[2],
+ row,
+ col,
+ pos
+ )
+ return self
+end
+
+function HeaderElement:clear_virtual_text()
+ virtual_text.clear(self.buffer, self.ns_id)
+ return self
+end
+
+function HeaderElement:clear()
+ self:set_lines({})
+ self:clear_virtual_text()
+ return self
+end
+
+return HeaderElement
diff --git a/lua/vgit/ui/elements/HorizontalBorderElement.lua b/lua/vgit/ui/elements/HorizontalBorderElement.lua
new file mode 100644
index 00000000..20a58eaa
--- /dev/null
+++ b/lua/vgit/ui/elements/HorizontalBorderElement.lua
@@ -0,0 +1,56 @@
+local Buffer = require('vgit.core.Buffer')
+local Window = require('vgit.core.Window')
+local Object = require('vgit.core.Object')
+
+local HorizontalBorderElement = Object:extend()
+
+function HorizontalBorderElement:new()
+ return setmetatable({
+ buffer = nil,
+ window = nil,
+ }, HorizontalBorderElement)
+end
+
+function HorizontalBorderElement:mount(options)
+ self.buffer = Buffer:new():create()
+ local buffer = self.buffer
+ buffer:assign_options({
+ modifiable = false,
+ bufhidden = 'wipe',
+ buflisted = false,
+ })
+ self.window = Window
+ :open(buffer, {
+ style = 'minimal',
+ focusable = false,
+ relative = 'editor',
+ row = options.row,
+ col = options.col,
+ width = options.width,
+ height = 1,
+ zindex = 100,
+ })
+ :assign_options({
+ cursorbind = false,
+ scrollbind = false,
+ winhl = 'Normal:GitBorder',
+ })
+ self:set_lines({ string.rep('─', options.width) })
+ return self
+end
+
+function HorizontalBorderElement:get_height()
+ return 1
+end
+
+function HorizontalBorderElement:unmount()
+ self.window:close()
+ return self
+end
+
+function HorizontalBorderElement:set_lines(lines)
+ self.buffer:set_lines(lines)
+ return self
+end
+
+return HorizontalBorderElement
diff --git a/lua/vgit/ui/elements/LineNumberElement.lua b/lua/vgit/ui/elements/LineNumberElement.lua
new file mode 100644
index 00000000..ac37fa43
--- /dev/null
+++ b/lua/vgit/ui/elements/LineNumberElement.lua
@@ -0,0 +1,96 @@
+local highlighter = require('vgit.core.highlighter')
+local dimensions = require('vgit.ui.dimensions')
+local Buffer = require('vgit.core.Buffer')
+local Window = require('vgit.core.Window')
+local Object = require('vgit.core.Object')
+
+local LineNumberElement = Object:extend()
+
+function LineNumberElement:new()
+ return setmetatable({
+ buffer = nil,
+ window = nil,
+ ns_id = vim.api.nvim_create_namespace(''),
+ cache = {
+ lines = {},
+ },
+ }, LineNumberElement)
+end
+
+function LineNumberElement:set_lnum(lnum)
+ self.window:set_lnum(lnum)
+ return self
+end
+
+function LineNumberElement:set_cursor(cursor)
+ self.window:set_cursor(cursor)
+ return self
+end
+
+function LineNumberElement:reset_cursor()
+ self.window:set_cursor({ 1, 1 })
+ return self
+end
+
+function LineNumberElement:call(callback)
+ self.window:call(callback)
+ return self
+end
+
+function LineNumberElement:mount(options)
+ self.buffer = Buffer:new():create()
+ local buffer = self.buffer
+ buffer:assign_options({
+ modifiable = false,
+ bufhidden = 'wipe',
+ buflisted = false,
+ })
+ self.window = Window
+ :open(buffer, {
+ style = 'minimal',
+ focusable = false,
+ relative = 'editor',
+ row = options.row,
+ col = options.col,
+ height = options.height,
+ width = LineNumberElement:get_width(),
+ zindex = 50,
+ })
+ :assign_options({
+ cursorline = true,
+ cursorbind = true,
+ scrollbind = true,
+ winhl = 'Normal:VGitBackgroundPrimary',
+ })
+ return self
+end
+
+function LineNumberElement:get_width()
+ return 6
+end
+
+function LineNumberElement:make_lines(lines)
+ local global_height = dimensions.global_height()
+ local actual_lines = {}
+ for _ = 1, #lines do
+ actual_lines[#actual_lines + 1] = ''
+ end
+ for _ = #lines, global_height do
+ actual_lines[#actual_lines + 1] = ''
+ end
+ self.buffer:set_lines(actual_lines)
+ self.cache.lines = lines
+ return self
+end
+
+function LineNumberElement:clear_hls()
+ highlighter.clear(self.buffer, self.ns_id)
+ return self
+end
+
+function LineNumberElement:unmount()
+ self.window:close()
+ return self
+end
+
+return LineNumberElement
diff --git a/lua/vgit/ui/table_maker.lua b/lua/vgit/ui/table_maker.lua
new file mode 100644
index 00000000..6762e5dd
--- /dev/null
+++ b/lua/vgit/ui/table_maker.lua
@@ -0,0 +1,132 @@
+local utils = require('vgit.core.utils')
+
+local table_maker = {}
+
+table_maker.parse_item = function(item, row)
+ local hl = {}
+ local value = item.text
+ if item.icon_before then
+ value = string.format('%s %s', item.icon_before.icon, value)
+ hl[#hl + 1] = {
+ hl = item.icon_before.hl,
+ row = row,
+ range = {
+ start = 1,
+ finish = 1 + #item.icon_before.icon,
+ },
+ }
+ end
+ if item.icon_after then
+ value = string.format('%s %s', value, item.icon_after.icon)
+ hl[#hl + 1] = {
+ hl = item.icon_after.hl,
+ row = row,
+ range = {
+ start = #value - 1,
+ finish = #value - 1 + #item.icon_after.icon,
+ },
+ }
+ end
+ return value, hl
+end
+
+table_maker.make_paddings =
+ function(rows, column_labels, column_spacing, max_column_len)
+ local padding = {}
+ for i = 1, #rows do
+ local items = rows[i]
+ assert(
+ #column_labels == #items,
+ 'number of columns should be the same as number of column_labels'
+ )
+ for j = 1, #items do
+ local item = items[j]
+ local value
+ if type(item) == 'table' then
+ value, _ = table_maker.parse_item(item, i)
+ value = utils.shorten_string(value, max_column_len)
+ else
+ value = utils.shorten_string(item, max_column_len)
+ end
+ if padding[j] then
+ padding[j] = math.max(padding[j], #value + column_spacing)
+ else
+ padding[j] = column_spacing + #value + column_spacing
+ end
+ end
+ end
+ return padding
+ end
+
+table_maker.make_row =
+ function(items, paddings, column_spacing, max_column_len, hls, r)
+ local row = string.format('%s', string.rep(' ', column_spacing))
+ local icon_offset = 0
+ for j = 1, #items do
+ local item = items[j]
+ local value, hl
+ if type(item) == 'table' then
+ icon_offset = 2
+ value, hl = table_maker.parse_item(item, r)
+ value = utils.shorten_string(value, max_column_len)
+ else
+ value = utils.shorten_string(item, max_column_len)
+ end
+ if hl then
+ for i = 1, #hl do
+ local hl_range = hl[i].range
+ hl_range.start = hl_range.start + #row
+ hl_range.finish = hl_range.finish + #row
+ hls[#hls + 1] = hl[i]
+ end
+ end
+ row = string.format(
+ '%s%s%s',
+ row,
+ value,
+ string.rep(' ', paddings[j] - #value + icon_offset)
+ )
+ end
+ return row, hls
+ end
+
+table_maker.make_heading =
+ function(column_labels, paddings, column_spacing, max_column_len)
+ local row = string.format('%s', string.rep(' ', column_spacing))
+ for j = 1, #column_labels do
+ local item = column_labels[j]
+ local value = utils.shorten_string(item, max_column_len)
+ row = string.format(
+ '%s%s%s',
+ row,
+ value,
+ string.rep(' ', paddings[j] - #value)
+ )
+ end
+ return { row }
+ end
+
+table_maker.make_rows = function(rows, paddings, column_spacing, max_column_len)
+ local lines = {}
+ local hls = {}
+ for i = 1, #rows do
+ -- Need to add a extra space in the front to account for the border in the header offset
+ lines[#lines + 1] = ' '
+ .. table_maker.make_row(
+ rows[i],
+ paddings,
+ column_spacing,
+ max_column_len,
+ hls,
+ i
+ )
+ end
+ for i = 1, #hls do
+ local hl_info = hls[i]
+ hl_info.range.start = hl_info.range.start + 1
+ hl_info.range.finish = hl_info.range.finish + 1
+ end
+ return lines, hls
+end
+
+return table_maker
diff --git a/lua/vgit/utils.lua b/lua/vgit/utils.lua
deleted file mode 100644
index 28d162ea..00000000
--- a/lua/vgit/utils.lua
+++ /dev/null
@@ -1,36 +0,0 @@
-local assert = require('vgit.assertion').assert
-local M = {}
-
-M.retrieve = function(cmd, ...)
- if type(cmd) == 'function' then
- return cmd(...)
- end
- return cmd
-end
-
-M.round = function(x)
- return x >= 0 and math.floor(x + 0.5) or math.ceil(x - 0.5)
-end
-
-M.readonly = function(tbl)
- return setmetatable({}, {
- __index = function(_, k)
- return tbl[k]
- end,
- __newindex = function()
- assert(false, 'Table is readonly')
- end,
- __metatable = {},
- __len = function()
- return #tbl
- end,
- __tostring = function()
- return tostring(tbl)
- end,
- __call = function(_, ...)
- return tbl(...)
- end,
- })
-end
-
-return M
diff --git a/lua/vgit/lib/bit.lua b/lua/vgit/vendor/bit.lua
similarity index 100%
rename from lua/vgit/lib/bit.lua
rename to lua/vgit/vendor/bit.lua
diff --git a/lua/vgit/lib/dmp.lua b/lua/vgit/vendor/dmp.lua
similarity index 99%
rename from lua/vgit/lib/dmp.lua
rename to lua/vgit/vendor/dmp.lua
index db5e51be..68896269 100644
--- a/lua/vgit/lib/dmp.lua
+++ b/lua/vgit/vendor/dmp.lua
@@ -29,7 +29,7 @@ local band, bor, lshift
= bit.band, bit.bor, bit.lshift
--]]
-local bit32 = require('vgit.lib.bit').bit32
+local bit32 = require('vgit.vendor.bit').bit32
local band, bor, lshift = bit32.band, bit32.bor, bit32.lshift
local type, setmetatable, ipairs, select = type, setmetatable, ipairs, select
local unpack, tonumber, error = unpack, tonumber, error
diff --git a/lua/vgit/virtual_text.lua b/lua/vgit/virtual_text.lua
deleted file mode 100644
index 6937bc31..00000000
--- a/lua/vgit/virtual_text.lua
+++ /dev/null
@@ -1,29 +0,0 @@
-local M = {}
-
-M.add = vim.api.nvim_buf_set_extmark
-
-M.delete = vim.api.nvim_buf_del_extmark
-
-M.transpose_text = function(buf, text, ns_id, hl_group, row, col_start, pos)
- vim.api.nvim_buf_set_extmark(buf, ns_id, row, col_start, {
- id = row + 1 + col_start,
- virt_text = { { text, hl_group } },
- virt_text_pos = pos or 'overlay',
- hl_mode = 'combine',
- })
-end
-
-M.transpose_line = function(buf, texts, ns_id, lnum, pos)
- vim.api.nvim_buf_set_extmark(buf, ns_id, lnum, 0, {
- id = lnum + 1,
- virt_text = texts,
- virt_text_pos = pos or 'overlay',
- hl_mode = 'combine',
- })
-end
-
-M.clear = function(buf, ns_id)
- vim.api.nvim_buf_clear_namespace(buf, ns_id, 0, -1)
-end
-
-return M
diff --git a/tests/mock/.git_keep/COMMIT_EDITMSG b/tests/mock/.git_keep/COMMIT_EDITMSG
new file mode 100644
index 00000000..b3f9a9f5
--- /dev/null
+++ b/tests/mock/.git_keep/COMMIT_EDITMSG
@@ -0,0 +1 @@
+update file3
diff --git a/tests/mock/.git_keep/HEAD b/tests/mock/.git_keep/HEAD
new file mode 100644
index 00000000..cb089cd8
--- /dev/null
+++ b/tests/mock/.git_keep/HEAD
@@ -0,0 +1 @@
+ref: refs/heads/master
diff --git a/tests/mock/.git_keep/config b/tests/mock/.git_keep/config
new file mode 100644
index 00000000..515f4836
--- /dev/null
+++ b/tests/mock/.git_keep/config
@@ -0,0 +1,5 @@
+[core]
+ repositoryformatversion = 0
+ filemode = true
+ bare = false
+ logallrefupdates = true
diff --git a/tests/mock/.git_keep/description b/tests/mock/.git_keep/description
new file mode 100644
index 00000000..498b267a
--- /dev/null
+++ b/tests/mock/.git_keep/description
@@ -0,0 +1 @@
+Unnamed repository; edit this file 'description' to name the repository.
diff --git a/tests/mock/.git_keep/hooks/applypatch-msg.sample b/tests/mock/.git_keep/hooks/applypatch-msg.sample
new file mode 100755
index 00000000..a5d7b84a
--- /dev/null
+++ b/tests/mock/.git_keep/hooks/applypatch-msg.sample
@@ -0,0 +1,15 @@
+#!/bin/sh
+#
+# An example hook script to check the commit log message taken by
+# applypatch from an e-mail message.
+#
+# The hook should exit with non-zero status after issuing an
+# appropriate message if it wants to stop the commit. The hook is
+# allowed to edit the commit message file.
+#
+# To enable this hook, rename this file to "applypatch-msg".
+
+. git-sh-setup
+commitmsg="$(git rev-parse --git-path hooks/commit-msg)"
+test -x "$commitmsg" && exec "$commitmsg" ${1+"$@"}
+:
diff --git a/tests/mock/.git_keep/hooks/commit-msg.sample b/tests/mock/.git_keep/hooks/commit-msg.sample
new file mode 100755
index 00000000..b58d1184
--- /dev/null
+++ b/tests/mock/.git_keep/hooks/commit-msg.sample
@@ -0,0 +1,24 @@
+#!/bin/sh
+#
+# An example hook script to check the commit log message.
+# Called by "git commit" with one argument, the name of the file
+# that has the commit message. The hook should exit with non-zero
+# status after issuing an appropriate message if it wants to stop the
+# commit. The hook is allowed to edit the commit message file.
+#
+# To enable this hook, rename this file to "commit-msg".
+
+# Uncomment the below to add a Signed-off-by line to the message.
+# Doing this in a hook is a bad idea in general, but the prepare-commit-msg
+# hook is more suited to it.
+#
+# SOB=$(git var GIT_AUTHOR_IDENT | sed -n 's/^\(.*>\).*$/Signed-off-by: \1/p')
+# grep -qs "^$SOB" "$1" || echo "$SOB" >> "$1"
+
+# This example catches duplicate Signed-off-by lines.
+
+test "" = "$(grep '^Signed-off-by: ' "$1" |
+ sort | uniq -c | sed -e '/^[ ]*1[ ]/d')" || {
+ echo >&2 Duplicate Signed-off-by lines.
+ exit 1
+}
diff --git a/tests/mock/.git_keep/hooks/fsmonitor-watchman.sample b/tests/mock/.git_keep/hooks/fsmonitor-watchman.sample
new file mode 100755
index 00000000..ef94fa29
--- /dev/null
+++ b/tests/mock/.git_keep/hooks/fsmonitor-watchman.sample
@@ -0,0 +1,109 @@
+#!/usr/bin/perl
+
+use strict;
+use warnings;
+use IPC::Open2;
+
+# An example hook script to integrate Watchman
+# (https://facebook.github.io/watchman/) with git to speed up detecting
+# new and modified files.
+#
+# The hook is passed a version (currently 1) and a time in nanoseconds
+# formatted as a string and outputs to stdout all files that have been
+# modified since the given time. Paths must be relative to the root of
+# the working tree and separated by a single NUL.
+#
+# To enable this hook, rename this file to "query-watchman" and set
+# 'git config core.fsmonitor .git/hooks/query-watchman'
+#
+my ($version, $time) = @ARGV;
+
+# Check the hook interface version
+
+if ($version == 1) {
+ # convert nanoseconds to seconds
+ # subtract one second to make sure watchman will return all changes
+ $time = int ($time / 1000000000) - 1;
+} else {
+ die "Unsupported query-fsmonitor hook version '$version'.\n" .
+ "Falling back to scanning...\n";
+}
+
+my $git_work_tree;
+if ($^O =~ 'msys' || $^O =~ 'cygwin') {
+ $git_work_tree = Win32::GetCwd();
+ $git_work_tree =~ tr/\\/\//;
+} else {
+ require Cwd;
+ $git_work_tree = Cwd::cwd();
+}
+
+my $retry = 1;
+
+launch_watchman();
+
+sub launch_watchman {
+
+ my $pid = open2(\*CHLD_OUT, \*CHLD_IN, 'watchman -j --no-pretty')
+ or die "open2() failed: $!\n" .
+ "Falling back to scanning...\n";
+
+ # In the query expression below we're asking for names of files that
+ # changed since $time but were not transient (ie created after
+ # $time but no longer exist).
+ #
+ # To accomplish this, we're using the "since" generator to use the
+ # recency index to select candidate nodes and "fields" to limit the
+ # output to file names only.
+
+ my $query = <<" END";
+ ["query", "$git_work_tree", {
+ "since": $time,
+ "fields": ["name"]
+ }]
+ END
+
+ print CHLD_IN $query;
+ close CHLD_IN;
+ my $response = do {local $/; };
+
+ die "Watchman: command returned no output.\n" .
+ "Falling back to scanning...\n" if $response eq "";
+ die "Watchman: command returned invalid output: $response\n" .
+ "Falling back to scanning...\n" unless $response =~ /^\{/;
+
+ my $json_pkg;
+ eval {
+ require JSON::XS;
+ $json_pkg = "JSON::XS";
+ 1;
+ } or do {
+ require JSON::PP;
+ $json_pkg = "JSON::PP";
+ };
+
+ my $o = $json_pkg->new->utf8->decode($response);
+
+ if ($retry > 0 and $o->{error} and $o->{error} =~ m/unable to resolve root .* directory (.*) is not watched/) {
+ print STDERR "Adding '$git_work_tree' to watchman's watch list.\n";
+ $retry--;
+ qx/watchman watch "$git_work_tree"/;
+ die "Failed to make watchman watch '$git_work_tree'.\n" .
+ "Falling back to scanning...\n" if $? != 0;
+
+ # Watchman will always return all files on the first query so
+ # return the fast "everything is dirty" flag to git and do the
+ # Watchman query just to get it over with now so we won't pay
+ # the cost in git to look up each individual file.
+ print "/\0";
+ eval { launch_watchman() };
+ exit 0;
+ }
+
+ die "Watchman: $o->{error}.\n" .
+ "Falling back to scanning...\n" if $o->{error};
+
+ binmode STDOUT, ":utf8";
+ local $, = "\0";
+ print @{$o->{files}};
+}
diff --git a/tests/mock/.git_keep/hooks/post-update.sample b/tests/mock/.git_keep/hooks/post-update.sample
new file mode 100755
index 00000000..ec17ec19
--- /dev/null
+++ b/tests/mock/.git_keep/hooks/post-update.sample
@@ -0,0 +1,8 @@
+#!/bin/sh
+#
+# An example hook script to prepare a packed repository for use over
+# dumb transports.
+#
+# To enable this hook, rename this file to "post-update".
+
+exec git update-server-info
diff --git a/tests/mock/.git_keep/hooks/pre-applypatch.sample b/tests/mock/.git_keep/hooks/pre-applypatch.sample
new file mode 100755
index 00000000..4142082b
--- /dev/null
+++ b/tests/mock/.git_keep/hooks/pre-applypatch.sample
@@ -0,0 +1,14 @@
+#!/bin/sh
+#
+# An example hook script to verify what is about to be committed
+# by applypatch from an e-mail message.
+#
+# The hook should exit with non-zero status after issuing an
+# appropriate message if it wants to stop the commit.
+#
+# To enable this hook, rename this file to "pre-applypatch".
+
+. git-sh-setup
+precommit="$(git rev-parse --git-path hooks/pre-commit)"
+test -x "$precommit" && exec "$precommit" ${1+"$@"}
+:
diff --git a/tests/mock/.git_keep/hooks/pre-commit.sample b/tests/mock/.git_keep/hooks/pre-commit.sample
new file mode 100755
index 00000000..6a756416
--- /dev/null
+++ b/tests/mock/.git_keep/hooks/pre-commit.sample
@@ -0,0 +1,49 @@
+#!/bin/sh
+#
+# An example hook script to verify what is about to be committed.
+# Called by "git commit" with no arguments. The hook should
+# exit with non-zero status after issuing an appropriate message if
+# it wants to stop the commit.
+#
+# To enable this hook, rename this file to "pre-commit".
+
+if git rev-parse --verify HEAD >/dev/null 2>&1
+then
+ against=HEAD
+else
+ # Initial commit: diff against an empty tree object
+ against=$(git hash-object -t tree /dev/null)
+fi
+
+# If you want to allow non-ASCII filenames set this variable to true.
+allownonascii=$(git config --bool hooks.allownonascii)
+
+# Redirect output to stderr.
+exec 1>&2
+
+# Cross platform projects tend to avoid non-ASCII filenames; prevent
+# them from being added to the repository. We exploit the fact that the
+# printable range starts at the space character and ends with tilde.
+if [ "$allownonascii" != "true" ] &&
+ # Note that the use of brackets around a tr range is ok here, (it's
+ # even required, for portability to Solaris 10's /usr/bin/tr), since
+ # the square bracket bytes happen to fall in the designated range.
+ test $(git diff --cached --name-only --diff-filter=A -z $against |
+ LC_ALL=C tr -d '[ -~]\0' | wc -c) != 0
+then
+ cat <<\EOF
+Error: Attempt to add a non-ASCII file name.
+
+This can cause problems if you want to work with people on other platforms.
+
+To be portable it is advisable to rename the file.
+
+If you know what you are doing you can disable this check using:
+
+ git config hooks.allownonascii true
+EOF
+ exit 1
+fi
+
+# If there are whitespace errors, print the offending file names and fail.
+exec git diff-index --check --cached $against --
diff --git a/tests/mock/.git_keep/hooks/pre-merge-commit.sample b/tests/mock/.git_keep/hooks/pre-merge-commit.sample
new file mode 100755
index 00000000..399eab19
--- /dev/null
+++ b/tests/mock/.git_keep/hooks/pre-merge-commit.sample
@@ -0,0 +1,13 @@
+#!/bin/sh
+#
+# An example hook script to verify what is about to be committed.
+# Called by "git merge" with no arguments. The hook should
+# exit with non-zero status after issuing an appropriate message to
+# stderr if it wants to stop the merge commit.
+#
+# To enable this hook, rename this file to "pre-merge-commit".
+
+. git-sh-setup
+test -x "$GIT_DIR/hooks/pre-commit" &&
+ exec "$GIT_DIR/hooks/pre-commit"
+:
diff --git a/tests/mock/.git_keep/hooks/pre-push.sample b/tests/mock/.git_keep/hooks/pre-push.sample
new file mode 100755
index 00000000..6187dbf4
--- /dev/null
+++ b/tests/mock/.git_keep/hooks/pre-push.sample
@@ -0,0 +1,53 @@
+#!/bin/sh
+
+# An example hook script to verify what is about to be pushed. Called by "git
+# push" after it has checked the remote status, but before anything has been
+# pushed. If this script exits with a non-zero status nothing will be pushed.
+#
+# This hook is called with the following parameters:
+#
+# $1 -- Name of the remote to which the push is being done
+# $2 -- URL to which the push is being done
+#
+# If pushing without using a named remote those arguments will be equal.
+#
+# Information about the commits which are being pushed is supplied as lines to
+# the standard input in the form:
+#
+#
+#
+# This sample shows how to prevent push of commits where the log message starts
+# with "WIP" (work in progress).
+
+remote="$1"
+url="$2"
+
+z40=0000000000000000000000000000000000000000
+
+while read local_ref local_sha remote_ref remote_sha
+do
+ if [ "$local_sha" = $z40 ]
+ then
+ # Handle delete
+ :
+ else
+ if [ "$remote_sha" = $z40 ]
+ then
+ # New branch, examine all commits
+ range="$local_sha"
+ else
+ # Update to existing branch, examine new commits
+ range="$remote_sha..$local_sha"
+ fi
+
+ # Check for WIP commit
+ commit=`git rev-list -n 1 --grep '^WIP' "$range"`
+ if [ -n "$commit" ]
+ then
+ echo >&2 "Found WIP commit in $local_ref, not pushing"
+ exit 1
+ fi
+ fi
+done
+
+exit 0
diff --git a/tests/mock/.git_keep/hooks/pre-rebase.sample b/tests/mock/.git_keep/hooks/pre-rebase.sample
new file mode 100755
index 00000000..6cbef5c3
--- /dev/null
+++ b/tests/mock/.git_keep/hooks/pre-rebase.sample
@@ -0,0 +1,169 @@
+#!/bin/sh
+#
+# Copyright (c) 2006, 2008 Junio C Hamano
+#
+# The "pre-rebase" hook is run just before "git rebase" starts doing
+# its job, and can prevent the command from running by exiting with
+# non-zero status.
+#
+# The hook is called with the following parameters:
+#
+# $1 -- the upstream the series was forked from.
+# $2 -- the branch being rebased (or empty when rebasing the current branch).
+#
+# This sample shows how to prevent topic branches that are already
+# merged to 'next' branch from getting rebased, because allowing it
+# would result in rebasing already published history.
+
+publish=next
+basebranch="$1"
+if test "$#" = 2
+then
+ topic="refs/heads/$2"
+else
+ topic=`git symbolic-ref HEAD` ||
+ exit 0 ;# we do not interrupt rebasing detached HEAD
+fi
+
+case "$topic" in
+refs/heads/??/*)
+ ;;
+*)
+ exit 0 ;# we do not interrupt others.
+ ;;
+esac
+
+# Now we are dealing with a topic branch being rebased
+# on top of master. Is it OK to rebase it?
+
+# Does the topic really exist?
+git show-ref -q "$topic" || {
+ echo >&2 "No such branch $topic"
+ exit 1
+}
+
+# Is topic fully merged to master?
+not_in_master=`git rev-list --pretty=oneline ^master "$topic"`
+if test -z "$not_in_master"
+then
+ echo >&2 "$topic is fully merged to master; better remove it."
+ exit 1 ;# we could allow it, but there is no point.
+fi
+
+# Is topic ever merged to next? If so you should not be rebasing it.
+only_next_1=`git rev-list ^master "^$topic" ${publish} | sort`
+only_next_2=`git rev-list ^master ${publish} | sort`
+if test "$only_next_1" = "$only_next_2"
+then
+ not_in_topic=`git rev-list "^$topic" master`
+ if test -z "$not_in_topic"
+ then
+ echo >&2 "$topic is already up to date with master"
+ exit 1 ;# we could allow it, but there is no point.
+ else
+ exit 0
+ fi
+else
+ not_in_next=`git rev-list --pretty=oneline ^${publish} "$topic"`
+ /usr/bin/perl -e '
+ my $topic = $ARGV[0];
+ my $msg = "* $topic has commits already merged to public branch:\n";
+ my (%not_in_next) = map {
+ /^([0-9a-f]+) /;
+ ($1 => 1);
+ } split(/\n/, $ARGV[1]);
+ for my $elem (map {
+ /^([0-9a-f]+) (.*)$/;
+ [$1 => $2];
+ } split(/\n/, $ARGV[2])) {
+ if (!exists $not_in_next{$elem->[0]}) {
+ if ($msg) {
+ print STDERR $msg;
+ undef $msg;
+ }
+ print STDERR " $elem->[1]\n";
+ }
+ }
+ ' "$topic" "$not_in_next" "$not_in_master"
+ exit 1
+fi
+
+<<\DOC_END
+
+This sample hook safeguards topic branches that have been
+published from being rewound.
+
+The workflow assumed here is:
+
+ * Once a topic branch forks from "master", "master" is never
+ merged into it again (either directly or indirectly).
+
+ * Once a topic branch is fully cooked and merged into "master",
+ it is deleted. If you need to build on top of it to correct
+ earlier mistakes, a new topic branch is created by forking at
+ the tip of the "master". This is not strictly necessary, but
+ it makes it easier to keep your history simple.
+
+ * Whenever you need to test or publish your changes to topic
+ branches, merge them into "next" branch.
+
+The script, being an example, hardcodes the publish branch name
+to be "next", but it is trivial to make it configurable via
+$GIT_DIR/config mechanism.
+
+With this workflow, you would want to know:
+
+(1) ... if a topic branch has ever been merged to "next". Young
+ topic branches can have stupid mistakes you would rather
+ clean up before publishing, and things that have not been
+ merged into other branches can be easily rebased without
+ affecting other people. But once it is published, you would
+ not want to rewind it.
+
+(2) ... if a topic branch has been fully merged to "master".
+ Then you can delete it. More importantly, you should not
+ build on top of it -- other people may already want to
+ change things related to the topic as patches against your
+ "master", so if you need further changes, it is better to
+ fork the topic (perhaps with the same name) afresh from the
+ tip of "master".
+
+Let's look at this example:
+
+ o---o---o---o---o---o---o---o---o---o "next"
+ / / / /
+ / a---a---b A / /
+ / / / /
+ / / c---c---c---c B /
+ / / / \ /
+ / / / b---b C \ /
+ / / / / \ /
+ ---o---o---o---o---o---o---o---o---o---o---o "master"
+
+
+A, B and C are topic branches.
+
+ * A has one fix since it was merged up to "next".
+
+ * B has finished. It has been fully merged up to "master" and "next",
+ and is ready to be deleted.
+
+ * C has not merged to "next" at all.
+
+We would want to allow C to be rebased, refuse A, and encourage
+B to be deleted.
+
+To compute (1):
+
+ git rev-list ^master ^topic next
+ git rev-list ^master next
+
+ if these match, topic has not merged in next at all.
+
+To compute (2):
+
+ git rev-list master..topic
+
+ if this is empty, it is fully merged to "master".
+
+DOC_END
diff --git a/tests/mock/.git_keep/hooks/pre-receive.sample b/tests/mock/.git_keep/hooks/pre-receive.sample
new file mode 100755
index 00000000..a1fd29ec
--- /dev/null
+++ b/tests/mock/.git_keep/hooks/pre-receive.sample
@@ -0,0 +1,24 @@
+#!/bin/sh
+#
+# An example hook script to make use of push options.
+# The example simply echoes all push options that start with 'echoback='
+# and rejects all pushes when the "reject" push option is used.
+#
+# To enable this hook, rename this file to "pre-receive".
+
+if test -n "$GIT_PUSH_OPTION_COUNT"
+then
+ i=0
+ while test "$i" -lt "$GIT_PUSH_OPTION_COUNT"
+ do
+ eval "value=\$GIT_PUSH_OPTION_$i"
+ case "$value" in
+ echoback=*)
+ echo "echo from the pre-receive-hook: ${value#*=}" >&2
+ ;;
+ reject)
+ exit 1
+ esac
+ i=$((i + 1))
+ done
+fi
diff --git a/tests/mock/.git_keep/hooks/prepare-commit-msg.sample b/tests/mock/.git_keep/hooks/prepare-commit-msg.sample
new file mode 100755
index 00000000..10fa14c5
--- /dev/null
+++ b/tests/mock/.git_keep/hooks/prepare-commit-msg.sample
@@ -0,0 +1,42 @@
+#!/bin/sh
+#
+# An example hook script to prepare the commit log message.
+# Called by "git commit" with the name of the file that has the
+# commit message, followed by the description of the commit
+# message's source. The hook's purpose is to edit the commit
+# message file. If the hook fails with a non-zero status,
+# the commit is aborted.
+#
+# To enable this hook, rename this file to "prepare-commit-msg".
+
+# This hook includes three examples. The first one removes the
+# "# Please enter the commit message..." help message.
+#
+# The second includes the output of "git diff --name-status -r"
+# into the message, just before the "git status" output. It is
+# commented because it doesn't cope with --amend or with squashed
+# commits.
+#
+# The third example adds a Signed-off-by line to the message, that can
+# still be edited. This is rarely a good idea.
+
+COMMIT_MSG_FILE=$1
+COMMIT_SOURCE=$2
+SHA1=$3
+
+/usr/bin/perl -i.bak -ne 'print unless(m/^. Please enter the commit message/..m/^#$/)' "$COMMIT_MSG_FILE"
+
+# case "$COMMIT_SOURCE,$SHA1" in
+# ,|template,)
+# /usr/bin/perl -i.bak -pe '
+# print "\n" . `git diff --cached --name-status -r`
+# if /^#/ && $first++ == 0' "$COMMIT_MSG_FILE" ;;
+# *) ;;
+# esac
+
+# SOB=$(git var GIT_COMMITTER_IDENT | sed -n 's/^\(.*>\).*$/Signed-off-by: \1/p')
+# git interpret-trailers --in-place --trailer "$SOB" "$COMMIT_MSG_FILE"
+# if test -z "$COMMIT_SOURCE"
+# then
+# /usr/bin/perl -i.bak -pe 'print "\n" if !$first_line++' "$COMMIT_MSG_FILE"
+# fi
diff --git a/tests/mock/.git_keep/hooks/update.sample b/tests/mock/.git_keep/hooks/update.sample
new file mode 100755
index 00000000..80ba9413
--- /dev/null
+++ b/tests/mock/.git_keep/hooks/update.sample
@@ -0,0 +1,128 @@
+#!/bin/sh
+#
+# An example hook script to block unannotated tags from entering.
+# Called by "git receive-pack" with arguments: refname sha1-old sha1-new
+#
+# To enable this hook, rename this file to "update".
+#
+# Config
+# ------
+# hooks.allowunannotated
+# This boolean sets whether unannotated tags will be allowed into the
+# repository. By default they won't be.
+# hooks.allowdeletetag
+# This boolean sets whether deleting tags will be allowed in the
+# repository. By default they won't be.
+# hooks.allowmodifytag
+# This boolean sets whether a tag may be modified after creation. By default
+# it won't be.
+# hooks.allowdeletebranch
+# This boolean sets whether deleting branches will be allowed in the
+# repository. By default they won't be.
+# hooks.denycreatebranch
+# This boolean sets whether remotely creating branches will be denied
+# in the repository. By default this is allowed.
+#
+
+# --- Command line
+refname="$1"
+oldrev="$2"
+newrev="$3"
+
+# --- Safety check
+if [ -z "$GIT_DIR" ]; then
+ echo "Don't run this script from the command line." >&2
+ echo " (if you want, you could supply GIT_DIR then run" >&2
+ echo " $0 [ )" >&2
+ exit 1
+fi
+
+if [ -z "$refname" -o -z "$oldrev" -o -z "$newrev" ]; then
+ echo "usage: $0 ][ " >&2
+ exit 1
+fi
+
+# --- Config
+allowunannotated=$(git config --bool hooks.allowunannotated)
+allowdeletebranch=$(git config --bool hooks.allowdeletebranch)
+denycreatebranch=$(git config --bool hooks.denycreatebranch)
+allowdeletetag=$(git config --bool hooks.allowdeletetag)
+allowmodifytag=$(git config --bool hooks.allowmodifytag)
+
+# check for no description
+projectdesc=$(sed -e '1q' "$GIT_DIR/description")
+case "$projectdesc" in
+"Unnamed repository"* | "")
+ echo "*** Project description file hasn't been set" >&2
+ exit 1
+ ;;
+esac
+
+# --- Check types
+# if $newrev is 0000...0000, it's a commit to delete a ref.
+zero="0000000000000000000000000000000000000000"
+if [ "$newrev" = "$zero" ]; then
+ newrev_type=delete
+else
+ newrev_type=$(git cat-file -t $newrev)
+fi
+
+case "$refname","$newrev_type" in
+ refs/tags/*,commit)
+ # un-annotated tag
+ short_refname=${refname##refs/tags/}
+ if [ "$allowunannotated" != "true" ]; then
+ echo "*** The un-annotated tag, $short_refname, is not allowed in this repository" >&2
+ echo "*** Use 'git tag [ -a | -s ]' for tags you want to propagate." >&2
+ exit 1
+ fi
+ ;;
+ refs/tags/*,delete)
+ # delete tag
+ if [ "$allowdeletetag" != "true" ]; then
+ echo "*** Deleting a tag is not allowed in this repository" >&2
+ exit 1
+ fi
+ ;;
+ refs/tags/*,tag)
+ # annotated tag
+ if [ "$allowmodifytag" != "true" ] && git rev-parse $refname > /dev/null 2>&1
+ then
+ echo "*** Tag '$refname' already exists." >&2
+ echo "*** Modifying a tag is not allowed in this repository." >&2
+ exit 1
+ fi
+ ;;
+ refs/heads/*,commit)
+ # branch
+ if [ "$oldrev" = "$zero" -a "$denycreatebranch" = "true" ]; then
+ echo "*** Creating a branch is not allowed in this repository" >&2
+ exit 1
+ fi
+ ;;
+ refs/heads/*,delete)
+ # delete branch
+ if [ "$allowdeletebranch" != "true" ]; then
+ echo "*** Deleting a branch is not allowed in this repository" >&2
+ exit 1
+ fi
+ ;;
+ refs/remotes/*,commit)
+ # tracking branch
+ ;;
+ refs/remotes/*,delete)
+ # delete tracking branch
+ if [ "$allowdeletebranch" != "true" ]; then
+ echo "*** Deleting a tracking branch is not allowed in this repository" >&2
+ exit 1
+ fi
+ ;;
+ *)
+ # Anything else (is there anything else?)
+ echo "*** Update hook: unknown type of update to ref $refname of type $newrev_type" >&2
+ exit 1
+ ;;
+esac
+
+# --- Finished
+exit 0
diff --git a/tests/mock/.git_keep/index b/tests/mock/.git_keep/index
new file mode 100644
index 00000000..c3e01ee6
Binary files /dev/null and b/tests/mock/.git_keep/index differ
diff --git a/tests/mock/.git_keep/info/exclude b/tests/mock/.git_keep/info/exclude
new file mode 100644
index 00000000..a5196d1b
--- /dev/null
+++ b/tests/mock/.git_keep/info/exclude
@@ -0,0 +1,6 @@
+# git ls-files --others --exclude-from=.git/info/exclude
+# Lines that start with '#' are comments.
+# For a project mostly in C, the following would be a good set of
+# exclude patterns (uncomment them if you want to use them):
+# *.[oa]
+# *~
diff --git a/tests/mock/.git_keep/logs/HEAD b/tests/mock/.git_keep/logs/HEAD
new file mode 100644
index 00000000..87bd2c79
--- /dev/null
+++ b/tests/mock/.git_keep/logs/HEAD
@@ -0,0 +1,3 @@
+0000000000000000000000000000000000000000 58d0c77b851925df29433c53d6ba31ae8ab280b0 tanvirtin 1635826503 -0400 commit (initial): initial commit
+58d0c77b851925df29433c53d6ba31ae8ab280b0 b3689ca8bc643725d18c231f6d8799e512443024 tanvirtin 1635987032 -0400 commit: add file3
+b3689ca8bc643725d18c231f6d8799e512443024 43c55a40e207871145146dbeed9b21e50bbb7803 tanvirtin 1635987073 -0400 commit: update file3
diff --git a/tests/mock/.git_keep/logs/refs/heads/master b/tests/mock/.git_keep/logs/refs/heads/master
new file mode 100644
index 00000000..87bd2c79
--- /dev/null
+++ b/tests/mock/.git_keep/logs/refs/heads/master
@@ -0,0 +1,3 @@
+0000000000000000000000000000000000000000 58d0c77b851925df29433c53d6ba31ae8ab280b0 tanvirtin 1635826503 -0400 commit (initial): initial commit
+58d0c77b851925df29433c53d6ba31ae8ab280b0 b3689ca8bc643725d18c231f6d8799e512443024 tanvirtin 1635987032 -0400 commit: add file3
+b3689ca8bc643725d18c231f6d8799e512443024 43c55a40e207871145146dbeed9b21e50bbb7803 tanvirtin 1635987073 -0400 commit: update file3
diff --git a/tests/mock/.git_keep/objects/2b/d32e42c8d723f4a63eca0dcc63049b8bb065b3 b/tests/mock/.git_keep/objects/2b/d32e42c8d723f4a63eca0dcc63049b8bb065b3
new file mode 100644
index 00000000..1231461b
Binary files /dev/null and b/tests/mock/.git_keep/objects/2b/d32e42c8d723f4a63eca0dcc63049b8bb065b3 differ
diff --git a/tests/mock/.git_keep/objects/43/c55a40e207871145146dbeed9b21e50bbb7803 b/tests/mock/.git_keep/objects/43/c55a40e207871145146dbeed9b21e50bbb7803
new file mode 100644
index 00000000..a3a761aa
Binary files /dev/null and b/tests/mock/.git_keep/objects/43/c55a40e207871145146dbeed9b21e50bbb7803 differ
diff --git a/tests/mock/.git_keep/objects/53/5e5eb1065781d8b56a971092de3bae41e5b3ff b/tests/mock/.git_keep/objects/53/5e5eb1065781d8b56a971092de3bae41e5b3ff
new file mode 100644
index 00000000..e35fc46b
Binary files /dev/null and b/tests/mock/.git_keep/objects/53/5e5eb1065781d8b56a971092de3bae41e5b3ff differ
diff --git a/tests/mock/.git_keep/objects/58/d0c77b851925df29433c53d6ba31ae8ab280b0 b/tests/mock/.git_keep/objects/58/d0c77b851925df29433c53d6ba31ae8ab280b0
new file mode 100644
index 00000000..000b79a6
--- /dev/null
+++ b/tests/mock/.git_keep/objects/58/d0c77b851925df29433c53d6ba31ae8ab280b0
@@ -0,0 +1,2 @@
+xA
+0E]sK)x@@]xzLJZsx䈉ZOs-$q..E)}7@cHS)p;q}', '_rerender_history()')
- assert.stub(api.nvim_buf_set_keymap).was_called_with(
- buf,
- 'n',
- '',
- ':lua require("vgit")._rerender_history()',
- {
- silent = true,
- noremap = true,
- }
- )
- end)
-end)
diff --git a/tests/unit/cli/Git_spec.lua b/tests/unit/cli/Git_spec.lua
new file mode 100644
index 00000000..f0413ef7
--- /dev/null
+++ b/tests/unit/cli/Git_spec.lua
@@ -0,0 +1,248 @@
+local fs = require('vgit.core.fs')
+local Object = require('vgit.core.Object')
+local assertion = require('vgit.core.assertion')
+local Git = require('vgit.cli.Git')
+local a = require('plenary.async.tests')
+
+local before_each = before_each
+local after_each = after_each
+local eq = assert.are.same
+
+local GitOps = Object:extend()
+
+function GitOps:new(filepath)
+ local err, lines = fs.read_file(filepath)
+ assert(not err)
+ if lines[#lines] == '' then
+ table.remove(lines, #lines)
+ end
+ return setmetatable({
+ filepath = filepath,
+ original_lines = lines,
+ current_lines = nil,
+ }, GitOps)
+end
+
+function GitOps:insert_into(line, index)
+ local lines = vim.tbl_deep_extend('force', {}, self.original_lines)
+ table.insert(lines, index, line)
+ fs.write_file(self.filepath, lines)
+ self.current_lines = lines
+end
+
+function GitOps:revert()
+ fs.write_file(self.filepath, self.original_lines)
+ self.current_lines = nil
+end
+
+a.describe('Git:', function()
+ local git
+
+ local function use_invalid_directory()
+ git:set_cwd('..')
+ end
+
+ -- If path is tests/mock then git will start looking from tests/mock directory for fixtures/ignoreme
+ local function use_mock_repository()
+ git:set_cwd('tests/mock')
+ end
+
+ before_each(function()
+ git = Git:new()
+ os.execute('mv tests/mock/.git_keep tests/mock/.git')
+ end)
+
+ after_each(function()
+ os.execute('mv tests/mock/.git tests/mock/.git_keep')
+ end)
+
+ a.describe('config', function()
+ a.it('should return the user defined config git object', function()
+ local err, config = git:config()
+ assert(not err)
+ assertion.assert_table(config)
+ assertion.assert_string(config['user.email'])
+ assertion.assert_string(config['user.name'])
+ end)
+ end)
+
+ a.describe('is_inside_git_dir', function()
+ a.it(
+ 'should return true if we are currently inside a git repository',
+ function()
+ local result = git:is_inside_git_dir()
+ assert(result)
+ end
+ )
+
+ a.it('should return false if we are not inside a git repository', function()
+ use_invalid_directory()
+ local result = git:is_inside_git_dir()
+ assert(not result)
+ end)
+ end)
+
+ a.describe('is_ignored', function()
+ local ignored_file = 'fixtures/ignoreme'
+ local legit_file = 'fixtures/file1'
+
+ a.it('should return true if a file is ignored', function()
+ use_mock_repository()
+ local result = git:is_ignored(ignored_file)
+ eq(result, true)
+ end)
+
+ a.it('should return false if a file is not ignored', function()
+ use_mock_repository()
+ local result = git:is_ignored(legit_file)
+ eq(result, false)
+ end)
+ end)
+
+ a.describe('file_hunks', function()
+ local filepath1 = 'fixtures/file1'
+ local filepath2 = 'fixtures/file2'
+
+ a.it('should return the hunks associated with the diff', function()
+ use_mock_repository()
+ local err, hunks = git:file_hunks(filepath1, filepath2)
+ assert(not err)
+ assert(hunks)
+ eq(hunks, {
+ {
+ diff = { '-file1-3' },
+ start = 2,
+ finish = 2,
+ header = '@@ -3 +2,0 @@ file1-2',
+ type = 'remove',
+ stat = {
+ added = 0,
+ removed = 1,
+ },
+ },
+ {
+ diff = { '+file1-6' },
+ start = 5,
+ finish = 5,
+ header = '@@ -5,0 +5 @@ file1-5',
+ type = 'add',
+ stat = {
+ added = 1,
+ removed = 0,
+ },
+ },
+ })
+ end)
+
+ a.it('should return an empty table if the files are the same', function()
+ use_mock_repository()
+ local err, hunks = git:file_hunks(filepath1, filepath1)
+ assert(not err)
+ assert(hunks)
+ eq(hunks, {})
+ end)
+ end)
+
+ a.describe('untracked_hunks', function()
+ a.it('should return a hunk comprised of all the lines', function()
+ local hunks = git:untracked_hunks({
+ 'a',
+ 'b',
+ 'c',
+ 'd',
+ 'e',
+ 'f',
+ })
+ assert(hunks)
+ eq(hunks, {
+ {
+ diff = {
+ '+a',
+ '+b',
+ '+c',
+ '+d',
+ '+e',
+ '+f',
+ },
+ start = 1,
+ finish = 6,
+ type = 'add',
+ stat = {
+ added = 6,
+ removed = 0,
+ },
+ },
+ })
+ end)
+
+ a.it('should return an empty table if the lines are empty', function()
+ local hunks = git:untracked_hunks({})
+ assert(hunks)
+ eq(hunks, {
+ {
+ diff = {},
+ start = 1,
+ finish = 0,
+ header = nil,
+ type = 'add',
+ stat = {
+ added = 0,
+ removed = 0,
+ },
+ },
+ })
+ end)
+ end)
+
+ a.describe('tracked_filename', function()
+ a.it(
+ 'should return the git tracked name for a given file in disk',
+ function()
+ use_mock_repository()
+ local filename = 'fixtures/file1'
+ local tracked_filename = git:tracked_filename(filename)
+ eq(filename, tracked_filename)
+ end
+ )
+
+ a.it('should return nil for a file not in git', function()
+ local filename = 'lua/tinman.lua'
+ local tracked_filename = git:tracked_filename(filename)
+ assert(not tracked_filename)
+ end)
+ end)
+
+ a.describe('show', function()
+ a.it(
+ 'should retrieve the current lines in git for a given filename when the base is HEAD',
+ function()
+ use_mock_repository()
+ local filename = 'fixtures/file1'
+ local err, lines = git:show(filename, 'HEAD')
+ assert(not err)
+ assert(lines)
+ eq(lines, {
+ 'file1-1',
+ 'file1-2',
+ 'file1-3',
+ 'file1-4',
+ 'file1-5',
+ })
+ end
+ )
+ a.it(
+ 'should retrieve the lines in git for a given file for a particular commit',
+ function()
+ use_mock_repository()
+ local filename = 'fixtures/file3'
+ local commit_hash = 'b3689ca8bc643725d18c231f6d8799e512443024'
+ local err, lines = git:show(filename, commit_hash)
+ assert(not err)
+ assert(lines)
+ eq(lines, {
+ 'file3-1',
+ })
+ end
+ )
+ end)
+end)
diff --git a/tests/unit/cli/models/Hunk_spec.lua b/tests/unit/cli/models/Hunk_spec.lua
new file mode 100644
index 00000000..90e54158
--- /dev/null
+++ b/tests/unit/cli/models/Hunk_spec.lua
@@ -0,0 +1,76 @@
+local Hunk = require('vgit.cli.models.Hunk')
+
+local describe = describe
+local it = it
+local before_each = before_each
+local after_each = after_each
+local eq = assert.are.same
+
+describe('Hunk:', function()
+ describe('new', function()
+ it('should create a new Hunk object', function()
+ local headers = {
+ add = '@@ -17,0 +18,15 @@ foo bar',
+ remove = '@@ -9,9 +8,0 @@ @@ foo bar',
+ change = '@@ -10,7 +10,7 @@ foo bar',
+ invalid = '@@ --10,-1 +-10,-7 @@ foo bar',
+ invalid_zero = '@@ -0,0 +0,0 @@ foo bar',
+ }
+ eq(Hunk:new(headers['add']), {
+ header = '@@ -17,0 +18,15 @@ foo bar',
+ diff = {},
+ start = 18,
+ finish = 32,
+ type = 'add',
+ stat = {
+ added = 0,
+ removed = 0,
+ },
+ })
+ eq(Hunk:new(headers['remove']), {
+ header = '@@ -9,9 +8,0 @@ @@ foo bar',
+ diff = {},
+ start = 8,
+ finish = 8,
+ type = 'remove',
+ stat = {
+ added = 0,
+ removed = 0,
+ },
+ })
+ eq(Hunk:new(headers['change']), {
+ header = '@@ -10,7 +10,7 @@ foo bar',
+ diff = {},
+ start = 10,
+ finish = 16,
+ type = 'change',
+ stat = {
+ added = 0,
+ removed = 0,
+ },
+ })
+ eq(Hunk:new(headers['invalid']), {
+ header = '@@ --10,-1 +-10,-7 @@ foo bar',
+ diff = {},
+ start = -10,
+ finish = -18,
+ type = 'change',
+ stat = {
+ added = 0,
+ removed = 0,
+ },
+ })
+ eq(Hunk:new(headers['invalid_zero']), {
+ header = '@@ -0,0 +0,0 @@ foo bar',
+ diff = {},
+ start = 0,
+ finish = 0,
+ type = 'remove',
+ stat = {
+ added = 0,
+ removed = 0,
+ },
+ })
+ end)
+ end)
+end)
diff --git a/tests/unit/Interface_spec.lua b/tests/unit/core/Config_spec.lua
similarity index 73%
rename from tests/unit/Interface_spec.lua
rename to tests/unit/core/Config_spec.lua
index 934a1c99..6359b0ee 100644
--- a/tests/unit/Interface_spec.lua
+++ b/tests/unit/core/Config_spec.lua
@@ -1,11 +1,11 @@
-local Interface = require('vgit.Interface')
+local Config = require('vgit.core.Config')
local it = it
local describe = describe
local before_each = before_each
local eq = assert.are.same
-describe('Interface:', function()
+describe('Config:', function()
local initial_state = {}
before_each(function()
@@ -21,7 +21,7 @@ describe('Interface:', function()
describe('new', function()
it('should bind the object provided into into the state object', function()
- local state = Interface:new(initial_state)
+ local state = Config:new(initial_state)
eq(state, {
data = initial_state,
})
@@ -29,14 +29,34 @@ describe('Interface:', function()
it('should throw error if invalid data type is provided', function()
assert.has_error(function()
- Interface:new(42)
+ Config:new(42)
end)
end)
end)
+ describe('size', function()
+ it('should return the size of the current config', function()
+ local state = Config:new(initial_state)
+ eq(state:size(), 3)
+ state = Config:new({})
+ eq(state:size(), 0)
+ end)
+ end)
+
+ describe('for_each', function()
+ it('should loop over all the key value pairs', function()
+ local state = Config:new(initial_state)
+ local copy_state = {}
+ state:for_each(function(key, value)
+ copy_state[key] = value
+ end)
+ eq(copy_state, initial_state)
+ end)
+ end)
+
describe('get', function()
it('should throw error on invalid argument types', function()
- local state = Interface:new({
+ local state = Config:new({
foo = 'bar',
})
assert.has_error(function()
@@ -59,7 +79,7 @@ describe('Interface:', function()
it(
'should succesfully retrieve a value given a key from the state object',
function()
- local state = Interface:new(initial_state)
+ local state = Config:new(initial_state)
eq(state:get('foo'), 'bar')
eq(state:get('bar'), 'foo')
eq(state:get('baz'), {
@@ -72,7 +92,7 @@ describe('Interface:', function()
it(
'should throw an error if a state object does not have the given key',
function()
- local state = Interface:new(initial_state)
+ local state = Config:new(initial_state)
assert.has_error(function()
eq(state:get('test'), nil)
end)
@@ -82,7 +102,7 @@ describe('Interface:', function()
describe('set', function()
it('should throw error on invalid argument types', function()
- local state = Interface:new({
+ local state = Config:new({
foo = 'bar',
})
assert.has_error(function()
@@ -103,7 +123,7 @@ describe('Interface:', function()
end)
it('should alter an existing state attribute', function()
- local state = Interface:new(initial_state)
+ local state = Config:new(initial_state)
state:set('foo', 'a')
state:set('bar', 'b')
state:set('baz', {
@@ -121,7 +141,7 @@ describe('Interface:', function()
it(
'should not change the state attribute if no values are present',
function()
- local state = Interface:new(initial_state)
+ local state = Config:new(initial_state)
for i = 10, 1, -1 do
assert.has_error(function()
state:set(i, i)
@@ -135,24 +155,9 @@ describe('Interface:', function()
end)
describe('assign', function()
- it(
- 'should not assign attributes into into state which are not in it',
- function()
- local initial = { foo = true }
- local state = Interface:new(initial)
- state:assign({
- foo = false,
- bar = true,
- })
- eq(state, {
- data = { foo = false },
- })
- end
- )
-
it('should return unmodified state when nil value is passed', function()
local initial = { foo = true }
- local state = Interface:new(initial)
+ local state = Config:new(initial)
state:assign(nil)
eq(state, {
data = initial,
@@ -164,7 +169,7 @@ describe('Interface:', function()
is_list = { 1, 2, 3, 4, 5 },
isnt_list = { a = 1, b = 2 },
}
- local state = Interface:new(initial)
+ local state = Config:new(initial)
state:assign({
is_list = { 'a', 'b' },
isnt_list = { a = 1, b = 2 },
@@ -177,41 +182,6 @@ describe('Interface:', function()
})
end)
- it('should throw error when there is a type mismatch', function()
- local state = Interface:new({
- foo = true,
- bar = {
- baz = {
- a = {
- b = {},
- },
- foo = {
- bar = {
- baz = true,
- },
- a = {
- c = 4,
- },
- },
- },
- },
- })
- assert.has_error(function()
- state:assign({
- foo = 'what',
- bar = {
- baz = {
- foo = {
- bar = {
- baz = false,
- },
- },
- },
- },
- })
- end)
- end)
-
it('should successfully assign nested objects', function()
local initial = {
foo = true,
@@ -231,7 +201,7 @@ describe('Interface:', function()
},
},
}
- local state = Interface:new(initial)
+ local state = Config:new(initial)
state:assign({
foo = false,
bar = {
diff --git a/tests/unit/core/GitObject_spec.lua b/tests/unit/core/GitObject_spec.lua
new file mode 100644
index 00000000..cdec825f
--- /dev/null
+++ b/tests/unit/core/GitObject_spec.lua
@@ -0,0 +1,188 @@
+local Git = require('vgit.cli.Git')
+local fs = require('vgit.core.fs')
+local GitObject = require('vgit.core.GitObject')
+local mock = require('luassert.mock')
+local spy = require('luassert.spy')
+local a = require('plenary.async.tests')
+
+local it = it
+local describe = describe
+local before_each = before_each
+local after_each = after_each
+local eq = assert.are.same
+
+a.describe('GitObject:', function()
+ local filename = 'foo/bar/baz'
+ local dirname = 'foo/bar'
+ local untracked_hunks = {
+ {
+ type = 'untracked',
+ },
+ }
+ local file_hunks = {
+ {
+ type = 'file',
+ },
+ }
+ local git = {
+ tracked_filename = spy.new(function()
+ return filename
+ end),
+ is_inside_git_dir = spy.new(function()
+ return true
+ end),
+ show = spy.new(function()
+ return nil, {
+ 'a',
+ 'b',
+ 'c',
+ }
+ end),
+ is_ignored = spy.new(function()
+ return false
+ end),
+ untracked_hunks = spy.new(function()
+ return untracked_hunks
+ end),
+ file_hunks = spy.new(function()
+ return nil, file_hunks
+ end),
+ }
+ local git_object
+
+ before_each(function()
+ Git.new = mock(Git.new, true)
+ fs.write_file = spy.new(function() end)
+ fs.remove_file = spy.new(function() end)
+ fs.tmpname = spy.new(function()
+ return 'temp'
+ end)
+ fs.dirname = mock(fs.dirname, true)
+ Git.new.returns(git)
+ fs.dirname.returns(dirname)
+ end)
+ after_each(function()
+ mock.revert(fs.dirname)
+ mock.revert(Git.new)
+ end)
+
+ describe('new', function()
+ it('should create a new git object', function()
+ git_object = GitObject:new(filename)
+ eq(git_object, {
+ dirname = dirname,
+ filename = {
+ native = filename,
+ tracked = nil,
+ },
+ git = git,
+ hunks = nil,
+ })
+ end)
+ end)
+
+ describe('tracked_filename', function()
+ before_each(function()
+ git_object = GitObject:new(filename)
+ git.tracked_filename = spy.new(function()
+ return filename
+ end)
+ end)
+
+ it('returns the tracked filename from git', function()
+ eq(git_object:tracked_filename(), filename)
+ end)
+ it('should cache the result', function()
+ git_object:tracked_filename()
+ git_object:tracked_filename()
+ assert.spy(git.tracked_filename).was.called(1)
+ end)
+ end)
+
+ describe('is_inside_git_dir', function()
+ before_each(function()
+ git_object = GitObject:new(filename)
+ end)
+
+ it(
+ 'should check if the current git object is inside a git repository',
+ function()
+ eq(git_object:is_inside_git_dir(), true)
+ end
+ )
+ end)
+
+ describe('lines', function()
+ before_each(function()
+ git_object = GitObject:new(filename)
+ end)
+
+ it('should show the current lines in git related to the object', function()
+ local err, lines = git_object:lines()
+ assert(not err)
+ eq(lines, { 'a', 'b', 'c' })
+ end)
+ end)
+
+ describe('is_ignored', function()
+ before_each(function()
+ git_object = GitObject:new(filename)
+ end)
+
+ it('should check with git if the git object is ignored', function()
+ eq(git_object:is_ignored(), false)
+ end)
+ end)
+
+ a.describe('live_hunks', function()
+ before_each(function()
+ git_object = GitObject:new(filename)
+ end)
+
+ a.it(
+ 'should retrieve untracked hunks if there are no tracked filename',
+ function()
+ git.tracked_filename = spy.new(function()
+ return ''
+ end)
+ local current_lines = {}
+ local err, hunks = git_object:live_hunks(current_lines)
+ assert(not err)
+ assert.stub(git.untracked_hunks).was_called_with(git, current_lines)
+ assert.stub(git.untracked_hunks).was_called(1)
+ eq(hunks, untracked_hunks)
+ end
+ )
+ a.it('should retrieve tracked hunks if there is a filename', function()
+ git.tracked_filename = spy.new(function()
+ return filename
+ end)
+ local current_lines = {}
+ local err, hunks = git_object:live_hunks(current_lines)
+ assert(not err)
+ assert.stub(fs.tmpname).was_called(2)
+ assert.stub(fs.write_file).was_called(2)
+ assert.stub(fs.remove_file).was_called(2)
+ assert.stub(git.file_hunks).was_called(1)
+ assert.stub(git.file_hunks).was_called_with(git, 'temp', 'temp')
+ assert.stub(fs.remove_file).was_called_with('temp')
+ eq(hunks, file_hunks)
+ end)
+
+ a.it(
+ 'should not return tracked hunks if an error is encountered',
+ function()
+ git.tracked_filename = spy.new(function()
+ return filename
+ end)
+ git.show = spy.new(function()
+ return { 'error has occured' }, nil
+ end)
+ local current_lines = {}
+ local err, hunks = git_object:live_hunks(current_lines)
+ assert(err)
+ assert(not hunks)
+ end
+ )
+ end)
+end)
diff --git a/tests/unit/core/Window_spec.lua b/tests/unit/core/Window_spec.lua
new file mode 100644
index 00000000..2604f028
--- /dev/null
+++ b/tests/unit/core/Window_spec.lua
@@ -0,0 +1,108 @@
+local a = require('plenary.async.tests')
+local Window = require('vgit.core.Window')
+local mock = require('luassert.mock')
+
+local describe = describe
+local it = it
+local before_each = before_each
+local after_each = after_each
+local eq = assert.are.same
+
+a.describe('Window:', function()
+ local cursor = { 10, 2 }
+ before_each(function()
+ vim.api.nvim_win_set_cursor = mock(vim.api.nvim_win_set_cursor, true)
+ vim.api.nvim_win_get_cursor = mock(vim.api.nvim_win_get_cursor, true)
+ vim.api.nvim_win_get_cursor.returns(cursor)
+ end)
+
+ after_each(function()
+ mock.revert(vim.api.nvim_win_set_cursor)
+ mock.revert(vim.api.nvim_win_get_cursor)
+ end)
+
+ a.describe('new', function()
+ a.it(
+ 'should throw an error no win_id is provided to construct the window',
+ function()
+ assert.has_error(function()
+ Window:new()
+ end)
+ end
+ )
+ a.it(
+ 'should create an instance of the window object binding the win_id',
+ function()
+ local win_id = vim.api.nvim_open_win(
+ vim.api.nvim_create_buf(false, false),
+ false,
+ { relative = 'win', row = 3, col = 3, width = 12, height = 3 }
+ )
+ local window = Window:new(win_id)
+ eq(window:is(Window), true)
+ end
+ )
+ a.it(
+ 'should create a window object binding the current win_id if the win_id is 0',
+ function()
+ local win_id = vim.api.nvim_get_current_win()
+ local window = Window:new(0)
+ eq(window:is(Window), true)
+ eq(window.win_id, win_id)
+ end
+ )
+ end)
+
+ describe('set_cursor', function()
+ local window
+ local win_id
+ local cursor
+
+ before_each(function()
+ cursor = { 1, 1 }
+ win_id = vim.api.nvim_open_win(
+ vim.api.nvim_create_buf(false, false),
+ false,
+ { relative = 'win', row = 3, col = 3, width = 12, height = 3 }
+ )
+ window = Window:new(win_id)
+ end)
+
+ a.it('should call nvim_win_set_cursor using the bounded win_id', function()
+ window:set_cursor(cursor)
+ assert.stub(vim.api.nvim_win_set_cursor).was_called_with(win_id, cursor)
+ end)
+ end)
+
+ describe('get_cursor', function()
+ local window
+ local win_id
+
+ before_each(function()
+ win_id = vim.api.nvim_open_win(
+ vim.api.nvim_create_buf(false, false),
+ false,
+ { relative = 'win', row = 3, col = 3, width = 12, height = 3 }
+ )
+ window = Window:new(win_id)
+ end)
+
+ a.it('should call nvim_win_set_cursor using the bounded win_id', function()
+ local returned_cursor = window:get_cursor()
+ eq(cursor, returned_cursor)
+ end)
+
+ a.it('should return the lnum from the current cursor', function()
+ local lnum = window:get_lnum()
+ eq(lnum, 10)
+ end)
+
+ a.it('should set the current lnum', function()
+ window:set_lnum(111)
+ assert.stub(vim.api.nvim_win_set_cursor).was_called_with(
+ win_id,
+ { 111, cursor[2] }
+ )
+ end)
+ end)
+end)
diff --git a/tests/unit/core/assertion_spec.lua b/tests/unit/core/assertion_spec.lua
new file mode 100644
index 00000000..7455d3be
--- /dev/null
+++ b/tests/unit/core/assertion_spec.lua
@@ -0,0 +1,108 @@
+local assertion = require('vgit.core.assertion')
+
+local it = it
+local describe = describe
+
+describe('assertion:', function()
+ describe('assert', function()
+ it('should not throw error if conditions are met', function()
+ assertion.assert(true)
+ end)
+ it('should throw error if conditions are not met', function()
+ assert.has_error(function()
+ assertion.assert(false)
+ end)
+ end)
+ end)
+
+ describe('assert_type', function()
+ it('should throw an error if value is not of a certain type', function()
+ assert.has_error(function()
+ assertion.assert_type(3, 'string')
+ assertion.assert_type(3, 'function')
+ assertion.assert_type('foo', 'number')
+ end)
+ end)
+ it('should not throw an error if value is of a certain type', function()
+ assertion.assert_type(3, 'number')
+ assertion.assert_type(function() end, 'function')
+ end)
+ end)
+
+ describe('assert_number', function()
+ it('should throw an error if value is not a number', function()
+ assert.has_error(function()
+ assertion.assert_number('foo')
+ end)
+ end)
+ it('should not throw an error if value is not a number', function()
+ assertion.assert_number(3)
+ end)
+ end)
+
+ describe('assert_string', function()
+ it('should throw an error if value is not a string', function()
+ assert.has_error(function()
+ assertion.assert_string(3)
+ end)
+ end)
+ it('should not throw an error if value is not a string', function()
+ assertion.assert_string('foo')
+ end)
+ end)
+
+ describe('assert_function', function()
+ it('should throw an error if value is not a function', function()
+ assert.has_error(function()
+ assertion.assert_function(3)
+ end)
+ end)
+ it('should not throw an error if value is not a function', function()
+ assertion.assert_function(function() end)
+ end)
+ end)
+
+ describe('assert_boolean', function()
+ it('should throw an error if value is not a booean', function()
+ assert.has_error(function()
+ assertion.assert_boolean(3)
+ end)
+ end)
+ it('should not throw an error if value is not a bolean', function()
+ assertion.assert_boolean(true)
+ end)
+ end)
+
+ describe('assert_nil', function()
+ it('should throw an error if value is not a nil', function()
+ assert.has_error(function()
+ assertion.assert_nil(3)
+ end)
+ end)
+ it('should not throw an error if value is not a nil', function()
+ assertion.assert_nil(nil)
+ end)
+ end)
+
+ describe('assert_table', function()
+ it('should throw an error if value is not a table', function()
+ assert.has_error(function()
+ assertion.assert_table(3)
+ end)
+ end)
+ it('should not throw an error if value is not a table', function()
+ assertion.assert_table({})
+ end)
+ end)
+
+ describe('assert_list', function()
+ it('should throw an error if value is not a table', function()
+ assert.has_error(function()
+ assertion.assert_list({ hello = 'world' })
+ end)
+ end)
+ it('should not throw an error if value is not a table', function()
+ assertion.assert_list({ 1, 2, 3, 4 })
+ end)
+ end)
+end)
diff --git a/tests/unit/core/autocmd_spec.lua b/tests/unit/core/autocmd_spec.lua
new file mode 100644
index 00000000..6860bd29
--- /dev/null
+++ b/tests/unit/core/autocmd_spec.lua
@@ -0,0 +1,67 @@
+local Buffer = require('vgit.core.Buffer')
+local autocmd = require('vgit.core.autocmd')
+local spy = require('luassert.spy')
+local mock = require('luassert.mock')
+
+local describe = describe
+local it = it
+local before_each = before_each
+local after_each = after_each
+
+describe('autocmd:', function()
+ before_each(function()
+ vim.api = mock(vim.api, true)
+ end)
+
+ after_each(function()
+ mock.revert(vim.api)
+ end)
+
+ describe('register_module', function()
+ it('should define the necessary autocmd group', function()
+ autocmd.register_module()
+ assert.stub(vim.api.nvim_exec).was.called_with(
+ 'aug VGit | autocmd! | aug END',
+ false
+ )
+ end)
+
+ it('should invoke dependencies if passed in', function()
+ local s = spy.new(function() end)
+ autocmd.register_module(s)
+ assert.spy(s).was.called(1)
+ end)
+ end)
+
+ describe('off', function()
+ it('should redefine the autocmd group', function()
+ autocmd.off()
+ assert.stub(vim.api.nvim_exec).was.called_with(
+ 'aug VGit | autocmd! | aug END',
+ false
+ )
+ end)
+ end)
+
+ describe('on', function()
+ it('should define an autocmd', function()
+ autocmd.on('BufWinEnter', 'buf_win_enter()')
+ assert.stub(vim.api.nvim_exec).was.called_with(
+ 'au! VGit BufWinEnter * :lua _G.package.loaded.vgit.buf_win_enter()',
+ false
+ )
+ end)
+
+ it('should define an autocmd with custom options', function()
+ autocmd.on('BufWinEnter', 'buf_win_enter()', {
+ once = true,
+ override = false,
+ nested = true,
+ })
+ assert.stub(vim.api.nvim_exec).was.called_with(
+ 'au VGit BufWinEnter * ++nested ++once :lua _G.package.loaded.vgit.buf_win_enter()',
+ false
+ )
+ end)
+ end)
+end)
diff --git a/tests/unit/core/env_spec.lua b/tests/unit/core/env_spec.lua
new file mode 100644
index 00000000..8f609a78
--- /dev/null
+++ b/tests/unit/core/env_spec.lua
@@ -0,0 +1,59 @@
+local env = require('vgit.core.env')
+
+local describe = describe
+local it = it
+local eq = assert.are.same
+
+describe('env:', function()
+ describe('set', function()
+ it('should throw an error if type is not a string', function()
+ assert.has_error(function()
+ env.set('string', function() end)
+ end)
+ assert.has_error(function()
+ env.set(3, {})
+ end)
+ assert.has_error(function()
+ env.set(3, { 'hello' })
+ end)
+ end)
+
+ it('should set a value', function()
+ env.set('foo', 'bar')
+ env.set('bar', 3)
+ env.set('baz', true)
+ assert(env.get('foo'))
+ assert(env.get('bar'))
+ assert(env.get('baz'))
+ end)
+ end)
+
+ describe('get', function()
+ it('should throw an error if type is not a string', function()
+ assert.has_error(function()
+ env.get(3)
+ end)
+ end)
+ it('should retrieve a value that has been set', function()
+ eq(env.set('foo', 'bar').get('foo'), 'bar')
+ end)
+ end)
+
+ describe('unset', function()
+ it('should throw an error if type is not a string', function()
+ assert.has_error(function()
+ env.unset(3)
+ end)
+ end)
+ it('should throw an error if value is not set', function()
+ assert.has_error(function()
+ env.unset('hello')
+ end)
+ end)
+ it('should unset a key that has been set', function()
+ eq(env.set('foo', 'bar').get('foo'), 'bar')
+ env.unset('foo')
+ eq(env.get('foo'), nil)
+ end)
+ end)
+end)
diff --git a/tests/unit/fs_spec.lua b/tests/unit/core/fs_spec.lua
similarity index 57%
rename from tests/unit/fs_spec.lua
rename to tests/unit/core/fs_spec.lua
index 4e4a1a65..4fc5cc0e 100644
--- a/tests/unit/fs_spec.lua
+++ b/tests/unit/core/fs_spec.lua
@@ -1,4 +1,6 @@
-local fs = require('vgit.fs')
+local a = require('plenary.async.tests')
+local Buffer = require('vgit.core.Buffer')
+local fs = require('vgit.core.fs')
local it = it
local describe = describe
@@ -13,24 +15,6 @@ describe('fs:', function()
end)
describe('filename', function()
- it('should throw error on invalid argument types', function()
- assert.has_error(function()
- fs.filename(true)
- end)
- assert.has_error(function()
- fs.filename({})
- end)
- assert.has_error(function()
- fs.filename('foo')
- end)
- assert.has_error(function()
- fs.filename(nil)
- end)
- assert.has_error(function()
- fs.filename(function() end)
- end)
- end)
-
it('should return the relative path associated with the buffer', function()
local name = 'lua/vgit/init.lua'
local current = vim.loop.cwd()
@@ -47,24 +31,6 @@ describe('fs:', function()
end)
describe('relative_filename', function()
- it('should throw error on invalid argument types', function()
- assert.has_error(function()
- fs.relative_filename(true)
- end)
- assert.has_error(function()
- fs.relative_filename({})
- end)
- assert.has_error(function()
- fs.relative_filename(1)
- end)
- assert.has_error(function()
- fs.relative_filename(nil)
- end)
- assert.has_error(function()
- fs.relative_filename(function() end)
- end)
- end)
-
it('should convert an absolute path to a relative path', function()
local current = vim.loop.cwd()
local path = current .. '/lua/vgit/init.lua'
@@ -80,24 +46,6 @@ describe('fs:', function()
end)
describe('short_filename', function()
- it('should throw error on invalid argument types', function()
- assert.has_error(function()
- fs.short_filename(true)
- end)
- assert.has_error(function()
- fs.short_filename({})
- end)
- assert.has_error(function()
- fs.short_filename(1)
- end)
- assert.has_error(function()
- fs.short_filename(nil)
- end)
- assert.has_error(function()
- fs.short_filename(function() end)
- end)
- end)
-
it('should take a long path and give you the filename', function()
eq(fs.short_filename('lua/vgit/init.lua'), 'init.lua')
eq(fs.short_filename('/init.lua'), 'init.lua')
@@ -108,56 +56,25 @@ describe('fs:', function()
end)
end)
- describe('filetype', function()
- it('should throw error on invalid argument types', function()
- assert.has_error(function()
- fs.filetype(true)
- end)
- assert.has_error(function()
- fs.filetype({})
- end)
- assert.has_error(function()
- fs.filetype('foo')
- end)
- assert.has_error(function()
- fs.filetype(nil)
- end)
- assert.has_error(function()
- fs.filetype(function() end)
- end)
- end)
-
- it('should retrieve the correct filetype for a given buffer', function()
- local buf = vim.api.nvim_create_buf(true, true)
- vim.api.nvim_buf_set_option(buf, 'filetype', 'bar')
- eq(fs.filetype(buf), 'bar')
+ a.describe('filetype', function()
+ a.it('should retrieve the correct filetype for a given buffer', function()
+ local bufnr = vim.api.nvim_create_buf(true, true)
+ local buffer = Buffer:new(bufnr)
+ vim.api.nvim_buf_set_option(bufnr, 'filetype', 'bar')
+ eq(fs.filetype(buffer), 'bar')
end)
- it('should retrieve empty string for a buffer with no filetype', function()
- local buf = vim.api.nvim_create_buf(true, true)
- eq(fs.filetype(buf), '')
- end)
+ a.it(
+ 'should retrieve empty string for a buffer with no filetype',
+ function()
+ local bufnr = vim.api.nvim_create_buf(true, true)
+ local buffer = Buffer:new(bufnr)
+ eq(fs.filetype(buffer), '')
+ end
+ )
end)
describe('read_file', function()
- it('should throw error on invalid argument types', function()
- assert.has_error(function()
- fs.read_file(true)
- end)
- assert.has_error(function()
- fs.read_file({})
- end)
- assert.has_error(function()
- fs.read_file(1)
- end)
- assert.has_error(function()
- fs.read_file(nil)
- end)
- assert.has_error(function()
- fs.read_file(function() end)
- end)
- end)
-
it(
'should retrieve an err_result for a given file path that does not exist',
function()
@@ -188,24 +105,6 @@ describe('fs:', function()
end)
describe('detect', function()
- it('should throw error on invalid argument types', function()
- assert.has_error(function()
- fs.detect_filetype(true)
- end)
- assert.has_error(function()
- fs.detect_filetype({})
- end)
- assert.has_error(function()
- fs.detect_filetype(1)
- end)
- assert.has_error(function()
- fs.detect_filetype(nil)
- end)
- assert.has_error(function()
- fs.detect_filetype(function() end)
- end)
- end)
-
it('should work for md', function()
eq('markdown', fs.detect_filetype('Readme.md'))
end)
@@ -275,48 +174,12 @@ describe('fs:', function()
end)
describe('write_file', function()
- it('should throw error on invalid argument types', function()
- assert.has_error(function()
- fs.write_file(true, {})
- end)
- assert.has_error(function()
- fs.write_file({}, {})
- end)
- assert.has_error(function()
- fs.write_file(1, {})
- end)
- assert.has_error(function()
- fs.write_file(nil, {})
- end)
- assert.has_error(function()
- fs.write_file(function() end, {})
- end)
- assert.has_error(function()
- fs.write_file('', true)
- end)
- assert.has_error(function()
- fs.write_file('', '')
- end)
- assert.has_error(function()
- fs.write_file('', 1)
- end)
- assert.has_error(function()
- fs.write_file('', nil)
- end)
- assert.has_error(function()
- fs.write_file('', { foo = 'bar' })
- end)
- assert.has_error(function()
- fs.write_file('', function() end)
- end)
- end)
-
it('should create a new file and append the contents inside it', function()
local lines = { 'foo', 'bar' }
fs.write_file(filename, lines)
local err, data = fs.read_file(filename)
eq(err, nil)
- eq(data, { 'foo', 'bar', '' })
+ eq(data, { 'foo', 'bar' })
end)
it(
@@ -329,30 +192,12 @@ describe('fs:', function()
fs.write_file(filename, lines)
local err, data = fs.read_file(filename)
eq(err, nil)
- eq(data, { 'foo', 'baz', '' })
+ eq(data, { 'foo', 'baz' })
end
)
end)
describe('remove_file', function()
- it('should throw error on invalid argument types', function()
- assert.has_error(function()
- fs.remove_file(true)
- end)
- assert.has_error(function()
- fs.remove_file({})
- end)
- assert.has_error(function()
- fs.remove_file(1)
- end)
- assert.has_error(function()
- fs.remove_file(nil)
- end)
- assert.has_error(function()
- fs.remove_file(function() end)
- end)
- end)
-
it('should remove a file succesfully', function()
local num_files = 5
local file_exists = function(name)
@@ -386,27 +231,8 @@ describe('fs:', function()
end)
describe('exists', function()
- it('should throw error on invalid argument types', function()
- assert.has_error(function()
- fs.exists(true)
- end)
- assert.has_error(function()
- fs.exists({})
- end)
- assert.has_error(function()
- fs.exists(1)
- end)
- assert.has_error(function()
- fs.exists(nil)
- end)
- assert.has_error(function()
- fs.exists(function() end)
- end)
- end)
-
it('should return true if file exists', function()
- eq(fs.exists('lua/vgit/Hunk.lua'), true)
- eq(fs.exists('lua/vgit/buffer.lua'), true)
+ eq(fs.exists('lua/vgit.lua'), true)
end)
it('should return false if file does not exists', function()
@@ -419,4 +245,12 @@ describe('fs:', function()
eq(fs.exists('lua'), true)
end)
end)
+
+ describe('dirname', function()
+ it('should return the directory name for a given filename', function()
+ eq(fs.dirname('a/b/c/d/e'), 'a/b/c/d/')
+ eq(fs.dirname('a'), '')
+ eq(fs.dirname(''), '')
+ end)
+ end)
end)
diff --git a/tests/unit/core/highlight_spec.lua b/tests/unit/core/highlight_spec.lua
new file mode 100644
index 00000000..febdbb0f
--- /dev/null
+++ b/tests/unit/core/highlight_spec.lua
@@ -0,0 +1,70 @@
+local hls_setting = require('vgit.settings.hls')
+local highlighter = require('vgit.core.highlighter')
+local mock = require('luassert.mock')
+local spy = require('luassert.spy')
+
+local it = it
+local describe = describe
+local before_each = before_each
+local after_each = after_each
+local eq = assert.are.same
+
+describe('highlighter:', function()
+ before_each(function()
+ vim.api = mock(vim.api, true)
+ hls_setting.for_each = mock(hls_setting.for_each, true)
+ end)
+
+ after_each(function()
+ mock.revert(vim.api)
+ mock.revert(hls_setting.for_each)
+ end)
+
+ describe('create', function()
+ it('should successfully create a highlight', function()
+ highlighter.create('VGitTestCreate', {
+ fg = '#bb9af7',
+ bg = '#3b4261',
+ })
+ assert.stub(vim.api.nvim_exec).was.called_with(
+ 'highlight VGitTestCreate gui = NONE guifg = #bb9af7 guibg = #3b4261 ',
+ false
+ )
+ end)
+ end)
+
+ describe('create_theme', function()
+ it('should call the underline vim highlighter functions', function()
+ highlighter.create_theme({
+ VGitTestCreateTheme1 = {
+ fg = '#bb9af7',
+ bg = '#3b4261',
+ },
+ VGitTestCreateTheme2 = {
+ fg = '#bb9af7',
+ bg = '#3b4261',
+ },
+ })
+ assert.stub(vim.api.nvim_exec).was.called_with(
+ 'highlight VGitTestCreateTheme1 gui = NONE guifg = #bb9af7 guibg = #3b4261 ',
+ false
+ )
+ assert.stub(vim.api.nvim_exec).was.called_with(
+ 'highlight VGitTestCreateTheme2 gui = NONE guifg = #bb9af7 guibg = #3b4261 ',
+ false
+ )
+ end)
+ end)
+
+ describe('register_module', function()
+ it('should create highlighters accordingly', function()
+ highlighter.register_module()
+ assert.stub(hls_setting.for_each).was.called()
+ end)
+ it('should invoke dependencies if passed in', function()
+ local s = spy.new(function() end)
+ highlighter.register_module(s)
+ assert.spy(s).was.called(1)
+ end)
+ end)
+end)
diff --git a/tests/unit/core/keymap_spec.lua b/tests/unit/core/keymap_spec.lua
new file mode 100644
index 00000000..17afd72f
--- /dev/null
+++ b/tests/unit/core/keymap_spec.lua
@@ -0,0 +1,61 @@
+local keymap = require('vgit.core.keymap')
+local mock = require('luassert.mock')
+
+local describe = describe
+local it = it
+local before_each = before_each
+local after_each = after_each
+
+describe('keymap:', function()
+ before_each(function()
+ vim.api = mock(vim.api, true)
+ end)
+
+ after_each(function()
+ mock.revert(vim.api)
+ end)
+
+ describe('define', function()
+ it('should call vim api internally to define the given keys', function()
+ local expected = {
+ { 'n', '', ':VGit hunk_up' },
+ { 'n', '', ':VGit hunk_down' },
+ { 'n', 'gs', ':VGit buffer_hunk_stage' },
+ { 'n', 'gr', ':VGit buffer_hunk_reset' },
+ { 'n', 'gp', ':VGit buffer_hunk_preview' },
+ { 'n', 'gb', ':VGit buffer_blame_preview' },
+ { 'n', 'gf', ':VGit buffer_diff_preview' },
+ { 'n', 'gh', ':VGit buffer_history_preview' },
+ { 'n', 'gu', ':VGit buffer_reset' },
+ { 'n', 'gg', ':VGit buffer_gutter_blame_preview' },
+ { 'n', 'gd', ':VGit project_diff_preview' },
+ { 'n', 'gx', ':VGit toggle_diff_preference' },
+ }
+ keymap.define({
+ ['n '] = 'hunk_up',
+ ['n '] = 'hunk_down',
+ ['n gs'] = 'buffer_hunk_stage',
+ ['n gr'] = 'buffer_hunk_reset',
+ ['n gp'] = 'buffer_hunk_preview',
+ ['n gb'] = 'buffer_blame_preview',
+ ['n gf'] = 'buffer_diff_preview',
+ ['n gh'] = 'buffer_history_preview',
+ ['n gu'] = 'buffer_reset',
+ ['n gg'] = 'buffer_gutter_blame_preview',
+ ['n gd'] = 'project_diff_preview',
+ ['n gx'] = 'toggle_diff_preference',
+ })
+ for index in ipairs(expected) do
+ assert.stub(vim.api.nvim_set_keymap).was.called_with(
+ expected[index][1],
+ expected[index][2],
+ expected[index][3],
+ {
+ noremap = true,
+ silent = true,
+ }
+ )
+ end
+ end)
+ end)
+end)
diff --git a/tests/unit/defer_spec.lua b/tests/unit/core/loop_spec.lua
similarity index 51%
rename from tests/unit/defer_spec.lua
rename to tests/unit/core/loop_spec.lua
index 70cc1bfd..fec895c1 100644
--- a/tests/unit/defer_spec.lua
+++ b/tests/unit/core/loop_spec.lua
@@ -1,11 +1,14 @@
-local defer = require('vgit.defer')
+local loop = require('vgit.core.loop')
+local mock = require('luassert.mock')
-local it = it
local describe = describe
+local it = it
+local before_each = before_each
+local after_each = after_each
local eq = assert.are.same
-describe('defer:', function()
- describe('throttle_leading', function()
+describe('loop:', function()
+ describe('throttle', function()
local closure_creator = function(initial_value)
local counter = initial_value
return function()
@@ -19,7 +22,7 @@ describe('defer:', function()
function()
local result = nil
local closure = closure_creator(1)
- local throttled_fn = defer.throttle_leading(function()
+ local throttled_fn = loop.throttle(function()
result = closure()
eq(result, 2)
end, 100)
@@ -32,7 +35,7 @@ describe('defer:', function()
it(
'should throw errors if an error occurs within the wrapped function',
function()
- local throttled_fn = defer.throttle_leading(function()
+ local throttled_fn = loop.throttle(function()
assert(false, 'an error has occured')
end, 100)
assert.has_error(function()
@@ -58,7 +61,7 @@ describe('defer:', function()
function()
local result = nil
local closure = closure_creator(1)
- local debounced_fn = defer.debounce_trailing(function()
+ local debounced_fn = loop.debounce(function()
result = closure()
eq(result, 1)
end, 100)
@@ -68,4 +71,42 @@ describe('defer:', function()
end
)
end)
+
+ describe('watch', function()
+ before_each(function()
+ vim.loop = mock(vim.loop, true)
+ vim.loop.new_fs_event.returns({ foo = 'bar' })
+ end)
+ after_each(function()
+ mock.revert(vim.loop)
+ end)
+ it('should unwatch an event by calling vim loop', function()
+ local callback = function() end
+ loop.watch('/foo/bar/baz', callback)
+ assert.stub(vim.loop.fs_event_start).was_called_with(
+ { foo = 'bar' },
+ '/foo/bar/baz',
+ {
+
+ watch_entry = false,
+ stat = false,
+ recursive = false,
+ },
+ callback
+ )
+ end)
+ end)
+
+ describe('unwatch', function()
+ before_each(function()
+ vim.loop = mock(vim.loop, true)
+ end)
+ after_each(function()
+ mock.revert(vim.loop)
+ end)
+ it('should create an fs event that watches a file', function()
+ loop.unwatch({ foo = 'bar' })
+ assert.stub(vim.loop.fs_event_stop).was_called_with({ foo = 'bar' })
+ end)
+ end)
end)
diff --git a/tests/unit/core/sign_spec.lua b/tests/unit/core/sign_spec.lua
new file mode 100644
index 00000000..c44eae72
--- /dev/null
+++ b/tests/unit/core/sign_spec.lua
@@ -0,0 +1,101 @@
+local a = require('plenary.async.tests')
+local Buffer = require('vgit.core.Buffer')
+local sign = require('vgit.core.sign')
+local mock = require('luassert.mock')
+local spy = require('luassert.spy')
+
+local it = it
+local describe = describe
+local before_each = before_each
+local after_each = after_each
+local eq = assert.are.same
+
+a.describe('sign:', function()
+ before_each(function()
+ vim.fn.sign_define = mock(vim.fn.sign_define, true)
+ vim.fn.sign_place = mock(vim.fn.sign_place, true)
+ vim.fn.sign_unplace = mock(vim.fn.sign_unplace, true)
+ vim.fn.sign_getplaced = mock(vim.fn.sign_getplaced, true)
+ vim.fn.sign_getplaced.returns({
+ { signs = { { name = 'a' }, { name = 'b' }, { name = 'c' } } },
+ })
+ end)
+
+ after_each(function()
+ mock.revert(vim.fn.sign_define)
+ mock.revert(vim.fn.sign_place)
+ mock.revert(vim.fn.sign_unplace)
+ mock.revert(vim.fn.sign_getplaced)
+ end)
+
+ a.describe('define', function()
+ a.it('defines a sign by calling vim api', function()
+ sign.define('GitChange', {
+ texthl = 'GitChange',
+ text = '┃',
+ })
+ assert.stub(vim.fn.sign_define).was_called_with('GitChange', {
+ text = '┃',
+ texthl = 'GitChange',
+ })
+ end)
+ end)
+
+ a.describe('place', function()
+ a.it('should place a sign', function()
+ local bufnr = vim.api.nvim_create_buf(false, false)
+ sign.place(Buffer:new(bufnr), 10, 'GitAdd', 7)
+ assert.stub(vim.fn.sign_place).was_called_with(
+ 10,
+ string.format('tanvirtin/vgit.nvim/hunk/signs/%s', bufnr),
+ 'GitAdd',
+ bufnr,
+ {
+ id = 10,
+ lnum = 10,
+ priority = 7,
+ }
+ )
+ end)
+ end)
+
+ a.describe('unplace', function()
+ a.it('removes a placed sign', function()
+ local bufnr = vim.api.nvim_create_buf(false, false)
+ sign.unplace(Buffer:new(bufnr))
+ assert.stub(vim.fn.sign_unplace).was_called_with(
+ string.format('tanvirtin/vgit.nvim/hunk/signs/%s', bufnr)
+ )
+ end)
+ end)
+
+ a.describe('get', function()
+ a.it('should call sign_getplaced', function()
+ local bufnr = vim.api.nvim_create_buf(false, false)
+ local result = sign.get(Buffer:new(bufnr), 10)
+ assert.stub(vim.fn.sign_getplaced).was_called_with(bufnr, {
+ group = string.format('tanvirtin/vgit.nvim/hunk/signs/%s', bufnr),
+ id = 10,
+ })
+ eq(result, {
+ 'a',
+ 'b',
+ 'c',
+ })
+ end)
+ end)
+
+ a.describe('register_module', function()
+ a.it('should define the necessary autocmd group', function()
+ sign.define = spy.new(function() end)
+ sign.register_module()
+ assert.spy(sign.define).was.called()
+ end)
+
+ a.it('should invoke dependencies if passed in', function()
+ local s = spy.new(function() end)
+ sign.register_module(s)
+ assert.spy(s).was.called(1)
+ end)
+ end)
+end)
diff --git a/tests/unit/core/utils_spec.lua b/tests/unit/core/utils_spec.lua
new file mode 100644
index 00000000..263a15ad
--- /dev/null
+++ b/tests/unit/core/utils_spec.lua
@@ -0,0 +1,104 @@
+local utils = require('vgit.core.utils')
+
+local it = it
+local describe = describe
+local eq = assert.are.same
+local not_eq = assert.are_not.same
+
+describe('utils:', function()
+ describe('retrieve', function()
+ it('should invoke a function if passed in', function()
+ local test_fn = function(value)
+ return value
+ end
+ eq(utils.retrieve(test_fn, 42), 42)
+ not_eq(utils.retrieve(test_fn, 42), 22)
+ end)
+ end)
+
+ describe('round', function()
+ it('should round pi to 3', function()
+ eq(utils.round(3.14159265359), 3)
+ end)
+ end)
+
+ describe('strip_substring', function()
+ it('should remove "/bar" from "foo/bar/baz"', function()
+ eq(utils.strip_substring('foo/bar/baz', '/bar'), 'foo/baz')
+ end)
+ it('should remove "foo/baz" from "foo/bar/baz"', function()
+ eq(utils.strip_substring('foo/bar/baz', '/bar'), 'foo/baz')
+ end)
+ it(
+ 'should remove "helix-core/src" from "helix-core/src/comment.rs"',
+ function()
+ eq(
+ utils.strip_substring('helix-core/src/comment.rs', 'helix-core/src'),
+ '/comment.rs'
+ )
+ end
+ )
+ it(
+ 'should remove "helix-core/src/" from "helix-core/src/comment.rs"',
+ function()
+ eq(
+ utils.strip_substring('helix-core/src/comment.rs', 'helix-core/src/'),
+ 'comment.rs'
+ )
+ end
+ )
+ it('should remove "x-c" from "helix-core"', function()
+ eq(utils.strip_substring('helix-core', 'x-c'), 'heliore')
+ end)
+ it('should remove "ab" from "ababababa"', function()
+ eq(utils.strip_substring('ababababa', 'ab'), 'abababa')
+ end)
+ it('should remove "ababababa" from "abababab"', function()
+ eq(utils.strip_substring('ababababa', 'abababab'), 'a')
+ end)
+ it('should remove "ababababa" from "ababababa"', function()
+ eq(utils.strip_substring('ababababa', 'ababababa'), '')
+ end)
+ it('should not remove "lua/" from "vgit.lua"', function()
+ eq(utils.strip_substring('vgit.lua', 'lua/'), 'vgit.lua')
+ end)
+ end)
+
+ describe('object_assign', function()
+ it(
+ 'should assign attributes in b into a regardless of if a has any of the attributes',
+ function()
+ local a = {}
+ local b = {
+ config = {
+ line_number = {
+ enabled = false,
+ width = 10,
+ },
+ },
+ }
+ local c = utils.object_assign(a, b)
+ eq(c, b)
+ end
+ )
+ it('should handle nested object assignment', function()
+ local a = {
+ config = {
+ line_number = {
+ width = 20,
+ },
+ },
+ }
+ local b = {
+ config = {
+ line_number = {
+ enabled = false,
+ width = 10,
+ },
+ },
+ }
+ local c = utils.object_assign(a, b)
+ eq(c, b)
+ end)
+ end)
+end)
diff --git a/tests/unit/core/virtual_text_spec.lua b/tests/unit/core/virtual_text_spec.lua
new file mode 100644
index 00000000..76c350c2
--- /dev/null
+++ b/tests/unit/core/virtual_text_spec.lua
@@ -0,0 +1,179 @@
+local a = require('plenary.async.tests')
+local Buffer = require('vgit.core.Buffer')
+local virtual_text = require('vgit.core.virtual_text')
+local mock = require('luassert.mock')
+
+local it = it
+local describe = describe
+local before_each = before_each
+local after_each = after_each
+local eq = assert.are.same
+
+a.describe('virtual_text:', function()
+ local buffer
+ local ns_id
+
+ before_each(function()
+ buffer = Buffer:new():create()
+ buffer:set_lines({ 'a', 'b', 'c' })
+ ns_id = vim.api.nvim_create_namespace('')
+ vim.api.nvim_buf_set_extmark = mock(vim.api.nvim_buf_set_extmark, true)
+ vim.api.nvim_buf_del_extmark = mock(vim.api.nvim_buf_del_extmark, true)
+ vim.api.nvim_buf_clear_namespace = mock(
+ vim.api.nvim_buf_clear_namespace,
+ true
+ )
+ end)
+
+ after_each(function()
+ mock.revert(vim.api.nvim_buf_set_extmark)
+ mock.revert(vim.api.nvim_buf_del_extmark)
+ mock.revert(vim.api.nvim_buf_clear_namespace)
+ end)
+
+ a.describe('add', function()
+ a.it('should call underlying vim api to place the virtual text', function()
+ local cursor = { 1, 1 }
+ local opts = {
+ id = 1,
+ virt_text = { { 'hello', 'GitSignsAdd' } },
+ virt_text_pos = 'overlay',
+ hl_mode = 'combine',
+ }
+ virtual_text.add(buffer, ns_id, cursor[1], cursor[2], opts)
+ assert.stub(vim.api.nvim_buf_set_extmark).was_called_with(
+ buffer.bufnr,
+ ns_id,
+ cursor[1],
+ cursor[2],
+ opts
+ )
+ end)
+ end)
+
+ a.describe('delete', function()
+ a.it(
+ 'should call underlying vim api to unplace the virtual text',
+ function()
+ virtual_text.delete(buffer, ns_id, 1)
+ assert.stub(vim.api.nvim_buf_del_extmark).was_called_with(
+ buffer.bufnr,
+ ns_id,
+ 1
+ )
+ end
+ )
+ end)
+
+ a.describe('transpose_text', function()
+ a.it('should transpose a virtual text over existing text', function()
+ local cursor = { 1, 1 }
+ virtual_text.transpose_text(
+ buffer,
+ 'hello',
+ ns_id,
+ 'GitSignsAdd',
+ cursor[1],
+ cursor[2]
+ )
+ assert.stub(vim.api.nvim_buf_set_extmark).was_called_with(
+ buffer.bufnr,
+ ns_id,
+ cursor[1],
+ cursor[2],
+ {
+ id = cursor[1] + 1 + cursor[2],
+ virt_text = { { 'hello', 'GitSignsAdd' } },
+ virt_text_pos = 'overlay',
+ hl_mode = 'combine',
+ }
+ )
+ end)
+ a.it(
+ 'should transpose a virtual text over existing text using different pos',
+ function()
+ local cursor = { 1, 1 }
+ virtual_text.transpose_text(
+ buffer,
+ 'hello',
+ ns_id,
+ 'GitSignsAdd',
+ cursor[1],
+ cursor[2],
+ 'eol'
+ )
+ assert.stub(vim.api.nvim_buf_set_extmark).was_called_with(
+ buffer.bufnr,
+ ns_id,
+ cursor[1],
+ cursor[2],
+ {
+ id = cursor[1] + 1 + cursor[2],
+ virt_text = { { 'hello', 'GitSignsAdd' } },
+ virt_text_pos = 'eol',
+ hl_mode = 'combine',
+ }
+ )
+ end
+ )
+ end)
+
+ a.describe('transpose_line', function()
+ a.it('should transpose a virtual line over existing text', function()
+ virtual_text.transpose_line(
+ buffer,
+ { { 'hello', 'GitSignsAdd' } },
+ ns_id,
+ 1
+ )
+ assert.stub(vim.api.nvim_buf_set_extmark).was_called_with(
+ buffer.bufnr,
+ ns_id,
+ 1,
+ 0,
+ {
+ id = 2,
+ virt_text = { { 'hello', 'GitSignsAdd' } },
+ virt_text_pos = 'overlay',
+ hl_mode = 'combine',
+ }
+ )
+ end)
+ a.it(
+ 'should transpose a virtual line over existing text using different pos',
+ function()
+ virtual_text.transpose_line(
+ buffer,
+ { { 'hello', 'GitSignsAdd' } },
+ ns_id,
+ 1,
+ 'eol'
+ )
+ assert.stub(vim.api.nvim_buf_set_extmark).was_called_with(
+ buffer.bufnr,
+ ns_id,
+ 1,
+ 0,
+ {
+ id = 2,
+ virt_text = { { 'hello', 'GitSignsAdd' } },
+ virt_text_pos = 'eol',
+ hl_mode = 'combine',
+ }
+ )
+ end
+ )
+ end)
+
+ a.describe('clear', function()
+ a.it('should call underlying vim api to clear the virtual text', function()
+ virtual_text.clear(buffer, ns_id)
+ assert.stub(vim.api.nvim_buf_clear_namespace).was_called_with(
+ buffer.bufnr,
+ ns_id,
+ 0,
+ -1
+ )
+ end)
+ end)
+end)
diff --git a/tests/unit/highlighter_spec.lua b/tests/unit/highlighter_spec.lua
deleted file mode 100644
index 86473f3f..00000000
--- a/tests/unit/highlighter_spec.lua
+++ /dev/null
@@ -1,36 +0,0 @@
-local highlight = require('vgit.highlight')
-
-local it = it
-local describe = describe
-local eq = assert.are.same
-
-describe('highlight:', function()
- describe('setup', function()
- it(
- 'should override state highlights with highlights specified through the config',
- function()
- highlight.setup({
- VGitSignAdd = {
- fg = 'red',
- bg = nil,
- },
- })
- eq(highlight.state.data.VGitSignAdd, {
- fg = 'red',
- bg = nil,
- })
- end
- )
- end)
-
- describe('create', function()
- it('should successfully create a highlight', function()
- local hl = 'VGitTestHighlight'
- highlight.create(hl, {
- bg = nil,
- fg = '#464b59',
- })
- vim.cmd(string.format('hi %s', hl))
- end)
- end)
-end)
diff --git a/tests/unit/logger_spec.lua b/tests/unit/logger_spec.lua
deleted file mode 100644
index 01615ae4..00000000
--- a/tests/unit/logger_spec.lua
+++ /dev/null
@@ -1,68 +0,0 @@
-local mock = require('luassert.mock')
-local logger = require('vgit.logger')
-
-local it = it
-local describe = describe
-local before_each = before_each
-local after_each = after_each
-local eq = assert.are.same
-
-describe('setup', function()
- it(
- 'should override state highlights with highlights specified through the config',
- function()
- logger.setup({
- debug = true,
- })
- eq(logger.state:get('debug'), true)
- end
- )
-end)
-
-describe('info', function()
- before_each(function()
- vim.notify = mock(vim.notify, true)
- vim.notify.returns(5)
- end)
-
- after_each(function()
- mock.revert(vim.notify)
- end)
-
- it('should call notify passing in the appropriate arguments', function()
- logger.info('hello')
- assert.stub(vim.notify).was_called_with('hello', 'info')
- end)
-end)
-
-describe('warn', function()
- before_each(function()
- vim.notify = mock(vim.notify, true)
- vim.notify.returns(5)
- end)
-
- after_each(function()
- mock.revert(vim.notify)
- end)
-
- it('should call notify passing in the appropriate arguments', function()
- logger.warn('hello')
- assert.stub(vim.notify).was_called_with('hello', 'warn')
- end)
-end)
-
-describe('error', function()
- before_each(function()
- vim.notify = mock(vim.notify, true)
- vim.notify.returns(5)
- end)
-
- after_each(function()
- mock.revert(vim.notify)
- end)
-
- it('should call notify passing in the appropriate arguments', function()
- logger.error('hello')
- assert.stub(vim.notify).was_called_with('hello', 'error')
- end)
-end)
]