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
-
- CI @@ -18,40 +17,27 @@

-preview - -## 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. -
-
-commands +Hunk Preview +Diff Preview -## 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@@]xzLJZs x 䈉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)