1

Update generated nvim config

This commit is contained in:
2024-06-05 22:05:42 +02:00
parent 859ee3a2ba
commit 075fe5f587
1292 changed files with 152601 additions and 0 deletions

View File

@ -0,0 +1,12 @@
post.pl
vim_passfile
.*.un~
.*.sw*
# ignore vimballs
*.vba
*.vmb
# ignore *.orig files
*.orig
doc/tags
# ignore test files
*.mod

View File

@ -0,0 +1,48 @@
SCRIPT=$(wildcard plugin/*.vim)
AUTOL =$(wildcard autoload/*.vim)
DOC=$(wildcard doc/*.txt)
PLUGIN=$(shell basename "$$PWD")
VERSION=$(shell sed -n '/Version:/{s/^.*\(\S\.\S\+\)$$/\1/;p}' $(SCRIPT))
.PHONY: $(PLUGIN).vmb README test
all: uninstall vimball install
vimball: $(PLUGIN).vmb
clean:
find . -type f \( -name "*.vba" -o -name "*.orig" -o -name "*.~*" \
-o -name ".VimballRecord" -o -name ".*.un~" -o -name "*.sw*" -o \
-name tags -o -name "*.vmb" \) -delete
dist-clean: clean
install:
vim -N -i NONE -u NONE -c 'ru! plugin/vimballPlugin.vim' -c':so %' -c':q!' $(PLUGIN)-$(VERSION).vmb
uninstall:
vim -N -i NONE -u NONE -c 'ru! plugin/vimballPlugin.vim' -c':RmVimball' -c':q!' $(PLUGIN)-$(VERSION).vmb
undo:
for i in */*.orig; do mv -f "$$i" "$${i%.*}"; done
README:
cp -f $(DOC) README
$(PLUGIN).vmb:
rm -f $(PLUGIN)-$(VERSION).vmb
vim -N -i NONE -u NONE -c 'ru! plugin/vimballPlugin.vim' -c ':call append("0", [ "$(SCRIPT)", "$(AUTOL)", "$(DOC)"])' -c '$$d' -c ":%MkVimball $(PLUGIN)-$(VERSION) ." -c':q!'
ln -f $(PLUGIN)-$(VERSION).vmb $(PLUGIN).vmb
release: version all README
test:
cd test && ./runtest.sh
version:
perl -i.orig -pne 'if (/Version:/) {s/\.(\d*)/sprintf(".%d", 1+$$1)/e}' ${SCRIPT} ${AUTOL}
perl -i -pne 'if (/GetLatestVimScripts:/) {s/(\d+)\s+:AutoInstall:/sprintf("%d :AutoInstall:", 1+$$1)/e}' ${SCRIPT} ${AUTOL}
#perl -i -pne 'if (/Last Change:/) {s/\d+\.\d+\.\d\+$$/sprintf("%s", `date -R`)/e}' ${SCRIPT}
perl -i -pne 'if (/Last Change:/) {s/(:\s+).*\n/sprintf(": %s", `date -R`)/e}' ${SCRIPT} ${AUTOL}
perl -i.orig -pne 'if (/Version:/) {s/\.(\d+).*\n/sprintf(".%d %s", 1+$$1, `date -R`)/e}' ${DOC}
VERSION=$(shell sed -n '/Version:/{s/^.*\(\S\.\S\+\)$$/\1/;p}' $(SCRIPT))

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,60 @@
# NrrwRgn plugin [![Say Thanks!](https://img.shields.io/badge/Say%20Thanks-!-1EAEDB.svg)](https://saythanks.io/to/cb%40256bit.org)
> A Vim plugin for focussing on a selected region
This plugin is inspired by the [Narrowing feature of Emacs](http://www.emacswiki.org/emacs/Narrowing) and means to focus on a selected region while making the rest inaccessible. You simply select the region, call `:NR` and the selected part will open in a new split window while the rest of the buffer will be protected. Once you are finished, simply write the narrowed window (`:w`) and all the changes will be moved back to the original buffer.
See also the following screencast, that shows several features available:
![screencast of the plugin](screencast.gif "Screencast")
## Installation
Use the plugin manager of your choice. Or download the [stable][] version of the plugin, edit it with Vim (`vim NrrwRgn-XXX.vmb`) and simply source it (`:so %`). Restart and take a look at the help (`:h NrrwRgn.txt`)
[stable]: http://www.vim.org/scripts/script.php?script_id=3075
## Usage
Once installed, take a look at the help at `:h NarrowRegion`.
Here is a short overview of the functionality provided by the plugin:
### Ex commands:
:NR - Open the selected region in a new narrowed window
:NW - Open the current visual window in a new narrowed window
:WR - (In the narrowed window) write the changes back to the original buffer.
:NRV - Open the narrowed window for the region that was last visually selected.
:NUD - (In a unified diff) open the selected diff in 2 Narrowed windows
:NRP - Mark a region for a Multi narrowed window
:NRM - Create a new Multi narrowed window (after :NRP) - experimental!
:NRS - Enable Syncing the buffer content back (default on)
:NRN - Disable Syncing the buffer content back
:NRL - Reselect the last selected region and open it again in a narrowed window
You can append `!` to most commands to open the narrowed part in the current window instead of a new window. In the case of `:WR`, appending `!` closes the narrowed window in addition to writing to the original buffer.
### Visual mode commands:
<Leader>nr - Open the current visual selection in a new narrowed window
### Scripting Functions:
nrrwrgn#NrrwRgnStatus() - Return a dict with all the status information for the current window
### Attention
:NRM is currently experimental
## Similar Work
Andreas Politz' [narrow_region](http://www.vim.org/scripts/script.php?script_id=2038)<br/>
Kana Natsunos [narrow](http://www.vim.org/scripts/script.php?script_id=2097)<br/>
Jonas Kramers [narrow](http://www.vim.org/scripts/script.php?script_id=2446)<br/>
Marcin Szamotulskis [ViewPort](http://www.vim.org/scripts/script.php?script_id=4296)<br/>
## License & Copyright
© 2009-2014 by Christian Brabandt. The Vim License applies. See `:h license`
__NO WARRANTY, EXPRESS OR IMPLIED. USE AT-YOUR-OWN-RISK__

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,818 @@
*NrrwRgn.txt* A Narrow Region Plugin (similar to Emacs)
Author: Christian Brabandt <cb@256bit.org>
Version: 0.33 Thu, 15 Jan 2015 20:52:29 +0100
Copyright: (c) 2009-2015 by Christian Brabandt
The VIM LICENSE applies to NrrwRgnPlugin.vim and NrrwRgnPlugin.txt
(see |copyright|) except use NrrwRgnPlugin instead of "Vim".
NO WARRANTY, EXPRESS OR IMPLIED. USE AT-YOUR-OWN-RISK.
==============================================================================
1. Contents *NarrowRegion* *NrrwRgnPlugin*
1. Contents.....................................: |NrrwRgnPlugin|
2. NrrwRgn Manual...............................: |NrrwRgn-manual|
2.1 NrrwRgn Howto..............................: |NR-HowTo|
2.2 NrrwRgn Multi..............................: |NR-multi-example|
2.3 NrrwRgn Configuration......................: |NrrwRgn-config|
2.4 NrrwRgn public functions...................: |NrrwRgn-func|
3. NrrwRgn Tips.................................: |NrrwRgn-tips|
4. NrrwRgn Feedback.............................: |NrrwRgn-feedback|
5. NrrwRgn History..............................: |NrrwRgn-history|
==============================================================================
2. NrrwRgn Manual *NrrwRgn-manual*
Functionality
This plugin is based on a discussion in comp.editors (see the thread at
http://groups.google.com/group/comp.editors/browse_frm/thread/0f562d97f80dde13)
Narrowing means focussing on a region and making the rest inaccessible. You
simply select the region, call |:NarrowRegion| and the selected part will open
in a new scratch buffer. The rest of the file will be protected, so you won't
accidentally modify that buffer. In the new buffer, you can do a global
replace, search or anything else to modify that part. When you are finished,
simply write that buffer (e.g. by |:w|) and your modifications will be put in
the original buffer making it accessible again. Use |:q!| or |:bw!| to abort
your changes and return back to the original window.
NrrwRgn allows you to either select a line based selection using an Ex-command
or you can simply use any visual selected region and press your preferred key
combination to open that selection in a new buffer.
This plugin defines the following commands:
*:NarrowRegion* *:NR*
:[range]NR[!]
:[range]NarrowRegion[!] When [range] is omitted, select only the current
line, else use the lines in the range given and
open it in a new Scratch Window.
If the current line is selected and is on a folded
region, select the whole folded text.
Whenever you are finished modifying that region
simply write the buffer.
If ! is given, open the narrowed buffer not in a
split buffer but in the current window. However
when 'hidden' is not set, will open a new split
window.
*:NarrowWindow* *:NW*
:NW[!]
:NarrowWindow[!] Select only the range that is visible the current
window and open it in a new Scratch Window.
Whenever you are finished modifying that region
simply write the buffer.
If ! is given, open the narrowed buffer not in a
split buffer but in the current window (works best
with 'hidden' set).
*:WidenRegion* *:WR*
:WR[!]
:WidenRegion[!] This command is only available in the narrowed
scratch window. If the buffer has been modified,
the contents will be put back on the original
buffer. If ! is specified, the window will be
closed, otherwise it will remain open.
*:NRV*
:NRV[!] Opened the narrowed window for the region that was
last selected in visual mode
If ! is given, open the narrowed buffer not in a
split buffer but in the current window (works best
with 'hidden' set).
*:NUD*
:NUD When viewing unified diffs, this command opens
the current chunk in 2 Narrowed Windows in
|diff-mode| The current chunk is determined as the
one, that the cursor is at.
For filetypes other than diffs, it will try to
create a diff mode for a merge conflict.
*:NRPrepare*
:[range]NRPrepare[!]
:[range]NRP[!] You can use this command, to mark several lines
that will later be put into a Narrowed Window
using |:NRM|.
Using '!' clears the selection and does not add
any lines.
Use |:NRUnprepare| to remove a line again.
*:NRUnprepare*
:[range]NRUnprepare[!]
This command will remove the current line and
remove it from the lines marked for use with |:NRM|.
If the current line was not previously selected
for a multi narrowed window, it is ignored.
*:NRMulti*
:NRMulti
:NRM[!] This command takes all lines, that have been
marked by |:NRP| and puts them together in a new
narrowed buffer.
When you write your changes back, all separate
lines will be put back at their origin.
This command also clears the list of marked lines,
that was created with |NRP|.
See also |NR-multi-example|.
If ! is given, open the narrowed buffer not in a
split buffer but in the current window (works best
with 'hidden' set).
*:NRSyncOnWrite* *:NRS*
:NRSyncOnWrite
:NRS Enable synching the buffer content back to the
original buffer when writing.
(this is the default).
*:NRNoSyncOnWrite* *:NRN*
:NRNoSyncOnWrite
:NRN Disable synching the buffer content back to the
original buffer when writing. When set, the
narrowed buffer behaves like an ordinary buffer
that you can write in the filesystem.
Note: You can still use |:WidenRegion| to write
the changes back to the original buffer.
*:NRL*
:NRL[!] Reselect the last selected region again and open
in a narrowed window.
If ! is given, open the narrowed buffer not in a
split buffer but in the current window (works best
with 'hidden' set).
2.1 NrrwRgn HowTo *NR-HowTo*
-----------------
Use the commands provided above to select a certain region to narrow. You can
also start visual mode and have the selected region being narrowed. In this
mode, NarrowRegion allows you to block select |CTRL-V| , character select |v|
or linewise select |V| a region. Then press <Leader>nr where <Leader> by
default is set to '\', unless you have set it to something different (see
|<Leader>| for information how to change this) and the selected range will
open in a new scratch buffer. This key combination only works in |Visual-mode|
If instead of <Leader>nr you use <Leader>Nr in visual mode, the selection will
be opened in the current window, replacing the original buffer.
(Alternatively, you can use the normal mode mapping <Leader>nr and the region
over which you move will be opened in a new Narrowed window).
When finished, simply write that Narrowed Region window, from which you want
to take the modifications in your original file.
It is possible, to recursively open a Narrowed Window on top of an already
narrowed window. This sounds a little bit silly, but this makes it possible,
to have several narrowed windows, which you can use for several different
things, e.g. If you have 2 different buffers opened and you want to diff a
certain region of each of those 2 buffers, simply open a Narrowed Window for
each buffer, and execute |:diffthis| in each narrowed window.
You can then interactively merge those 2 windows. And when you are finished,
simply write the narrowed window and the changes will be taken back into the
original buffer.
When viewing unified diffs, you can use the provided |:NUD| command to open 2
Narrowed Windows side by side viewing the current chunk in |diff-mode|. Those
2 Narrowed windows will be marked 'modified', since there was some post
processing involved when opening the narrowed windows. Be careful, when
quitting the windows, not to write unwanted changes into your patch file! In
the window that contains the unified buffer, you can move to a different
chunk, run |:NUD| and the 2 Narrowed Windows in diff mode will update.
2.2 NrrwRgn Multi *NR-multi-example*
-----------------
Using the commands |:NRP| and |:NRM| allows to select a range of lines, that
will be put into a narrowed buffer together. This might sound confusing, but
this allows to apply a filter before making changes. For example before
editing your config file, you decide to strip all comments for making big
changes but when you write your changes back, these comments will stay in your
file. You would do it like this: >
:v/^#/NRP
:NRMulti
<
Now a Narrowed Window will open, that contains only the configuration lines.
Each block of independent region will be separated by a string like
# Start NarrowRegion1
.....
# End NarrowRegion1
This is needed, so the plugin later knows, which region belongs where in the
original place. Blocks you don't want to change, you can safely delete, they
won't be written back into your original file. But other than that, you
shouldn't change those separating lines.
When you are finished, simply write your changes back.
==============================================================================
2.3 NrrwRgn Configuration *NrrwRgn-config*
-------------------------
NarrowRegion can be customized by setting some global variables. If you'd
like to open the narrowed window as a vertical split buffer, simply set the
variable *g:nrrw_rgn_vert* to 1 in your |.vimrc| >
let g:nrrw_rgn_vert = 1
<
(default: 0)
------------------------------------------------------------------------------
If you'd like to specify a certain width/height for you scratch buffer, then
set the variable *g:nrrw_rgn_wdth* in your |.vimrc| . This variable defines the
height or the nr of columns, if you have also set g:nrrw_rgn_vert. >
let g:nrrw_rgn_wdth = 30
<
(default: 20)
Note: if the newly created narrowed window is smaller than this, it will be
resized to fit (plus an additional padding that can be specified using the
g:nrrw_rgn_pad variable (default: 0), to not leave unwanted space around (not
for single narrowed windows, e.g. when the '!' attribute was used).
------------------------------------------------------------------------------
Resizing the narrowed window can happen either by some absolute values or by a
relative percentage. The variable *g:nrrw_rgn_resize_window* determines what
kind of resize will occur. If it is set to "absolute", the resizing will be
done by absolute lines or columns (depending on whether a horizontal or
vertical split has been done). If it is set to "relative" the window will be
resized by a percentage. Set it like this in your |.vimrc| >
let g:nrrw_rgn_resize_window = 'absolute'
<
(default: absolute)
The percentages for increasing the window size can further be specified by
seting the following variables:
default:
g:nrrw_rgn_rel_min: 10 (50 for vertical splits)
g:nrrw_rgn_rel_max: 80
------------------------------------------------------------------------------
It is possible to specify an increment value, by which the narrowed window can
be increased. This is allows to easily toggle between the normal narrowed
window size and an even increased size (think of zooming).
You can either specify a relative or absolute zooming value. An absolute
resize will happen, if the variable |g:nrrw_rgn_resize_window| is set to
"absolute" or it is unset (see above).
If absolute resizing should happen you have to either specify columns, if the
Narrowed window is a vertical split window or lines, if a horizontal split has
been done.
Example, to increase the narrowed window by 30 lines or columns if
(g:nrrw_rgn_vert is also set [see above]), set in your |.vimrc| >
let g:nrrw_rgn_incr = 30
<
(default: 10, if |g:nrrw_rgn_resize_window| is "absolute")
Note: When using the '!' attribute for narrowing (e.g. the selection will be
opened in a new window that takes the complete screen size), no resizeing will
happen
------------------------------------------------------------------------------
If you'd like to change the key combination that toggles incrementing the
Narrowed Window size, map *<Plug>NrrwrgnWinIncr* by putting this in your |.vimrc| >
nmap <F3> <Plug>NrrwrgnWinIncr
<
(default: <Leader><Space>)
This will let you use the <F3> key to toggle the window size of the Narrowed
Window. Note: This mapping is only in the narrowed window active.
The amount of how much to increase can be further refined by setting the
*g:nrrw_rgn_incr* for an absolute increase of by setting the variables
*g:nrrw_rgn_rel_min* and *g:nrrw_rgn_rel_max*
Whether an absolute or relative increase will be performed, is determined by
the |g:nrrw_rgn_resize_window| variable (see above).
------------------------------------------------------------------------------
By default, NarrowRegion highlights the region that has been selected
using the WildMenu highlighting (see |hl-WildMenu|). If you'd like to use a
different highlighting, set the variable g:nrrw_rgn_hl to your preferred
highlighting Group. For example to have the region highlighted like a search
result, you could set *g:nrrw_rgn_hl* in your |.vimrc| >
let g:nrrw_rgn_hl = 'Search'
<
(default: WildMenu)
If you want to turn off the highlighting (because this can be distracting), you
can set the global variable *g:nrrw_rgn_nohl* to 1 in your |.vimrc| >
let g:nrrw_rgn_nohl = 1
<
(default: 0)
------------------------------------------------------------------------------
If you'd like to change the key combination that starts the Narrowed Window
for your selected range, you could map *<Plug>NrrwrgnDo* in your |.vimrc| >
xmap <F3> <Plug>NrrwrgnDo
<
This will let <F3> open the Narrow-Window, but only if you have pressed it in
Visual Mode. It doesn't really make sense to map this combination to any other
mode, unless you want it to Narrow your last visually selected range.
(default: <Leader>nr)
------------------------------------------------------------------------------
If you'd like to specify the options that you want to have set for the
narrowed window, you can set the *g:nrrw_custom_options* setting, in your
|.vimrc| e.g. >
let g:nrrw_custom_options={}
let g:nrrw_custom_options['filetype'] = 'python'
>
This will only apply those options to the narrowed buffer. You need to take
care that all options you need will apply.
------------------------------------------------------------------------------
If you don't like that your narrowed window opens above the current window,
define the *g:nrrw_topbot_leftright* variable to your taste, e.g. >
let g:nrrw_topbot_leftright = 'botright'
<
Now, all narrowed windows will appear below the original window. If not
specified, the narrowed window will appear above/left of the original window.
(default: topleft)
------------------------------------------------------------------------------
If you want to use several independent narrowed regions of the same buffer
that you want to write at the same time, protecting the original buffer is not
really useful. Therefore, set the *g:nrrw_rgn_protect* variable, e.g. in your
|.vimrc| >
let g:nrrw_rgn_protect = 'n'
<
This can be useful if you diff different regions of the same file, and want
to be able to put back the changes at different positions. Please note that
you should take care not to change any part that will later be influenced
when writing the narrowed region back.
Note: Don't use overlapping regions! Your changes will probably not be put
back correctly and there is no guard against losing data accidentally. NrrwRgn
tries hard to adjust the highlighting and regions as you write your changes
back into the original buffer, but it can't guarantee that this will work and
might fail silently. Therefore, this feature is experimental!
(default: y)
------------------------------------------------------------------------------
If you are using the |:NRMulti| command and want to have the original window
update to the position of where the cursor is in the narrowed window, you can
set the variable *g:nrrw_rgn_update_orig_win,* e.g. in your |.vimrc| >
let g:nrrw_rgn_update_orig_win = 1
<
Now the cursor in the original window will always update when the position
changes in the narrowed window (using a |CursorMoved| autocommand).
Note: that this might slow down scrolling and cursor movement a bit.
(default: 0)
------------------------------------------------------------------------------
By default, NarrowRegion plugin defines the two mappings <Leader>nr in visual
mode and normal mode and <Leader>Nr only in visual mode. If you have your own
mappings defined, than NarrowRegion will complain about the key already being
defined. Chances are, this will be quite annoying to you, so you can disable
mappings those keys by defining the variables *g:nrrw_rgn_nomap_nr* and
*g:nrrw_rgn_nomap_Nr* in your |.vimr| >
:let g:nrrw_rgn_nomap_nr = 1
:let g:nrrw_rgn_nomap_Nr = 1
(default: 0)
----------------------------------------------------------------------------
*NrrwRgn-hook* *NR-hooks*
NarrowRegion can execute certain commands, when creating the narrowed window
and when closing the narrowed window. For this, you can set 2 buffer-local
variables that specify what commands to execute, which will hook into the
execution of the Narrow Region plugin.
For example, suppose you have a file, containing columns separated data (CSV
format) which you want to modify and you also have the CSV filetype plugin
(http://www.vim.org/scripts/script.php?script_id=2830) installed and you want
to modify the CSV data which should be visually arranged like a table in the
narrowed window.
Therefore you want the command |:ArrangeColumn| to be executed in the new
narrowed window upon entering it, and when writing the changes back, you want
the command |:UnArrangeColumn| to be executed just before putting the
changes back. So you set the two variables *b:nrrw_aucmd_create* and
*b:nrrw_aucmd_close* in your original buffer: >
let b:nrrw_aucmd_create = "set ft=csv|%ArrangeCol"
let b:nrrw_aucmd_close = "%UnArrangeColumn"
<
This will execute the commands in the narrowed window: >
:set ft=csv
:%ArrangeCol
and before writing the changes back, it'll execute: >
:%UnArrangeCol
Note: These hooks are executed in the narrowed window (i.e. after creating the
narrowed window and its content and before writing the changes back to the
original buffer).
Additional hooks *b:nrrw_aucmd_writepost* and *b:nrrw_aucmd_written* are
provided, when the data is written back in the original window: the first one
is executed in the narrowed window, while the second one in the original one
(the one where the narrowed region was created from). For example, consider
you want the execute the command |:make| in the narrowed window, and also write
the original buffer, whenever the narrowed window is written back to the
original window. You therefore set: >
:let b:nrrw_aucmd_writepost = ':make'
:let b:nrrw_aucmd_written = ':update'
<
This will run |:make| in the narrowed window and also |:update| the original
buffer, whenever it was modified after writing the changes from the narrowed
window back.
2.4 NrrwRgn functions *NrrwRgn-func*
---------------------
The NrrwRgn plugin defines a public function in its namespace that can be used
to query its status.
*nrrwrgn#NrrwRgnStatus()*
nrrwrgn#NrrwRgnStatus()
Returns a dict with the following keys:
'shortname': The displayed buffer name
'fullname': The complete buffer name of the original buffer
'multi': 1 if it is a multi narrowed window (|:NRMulti|),
0 otherwise.
'startl': List of start lines for a multi narrowed window
(only present, if 'multi' is 1)
'endl': List of end lines for a multi narrowed window
(only present, if 'multi' is 1)
'start': Start position (only present if 'multi' is 0)
'end': End position (only present if 'multi' is 0)
'visual': Visual Mode, if it the narrowed window was started
from a visual selected region (empty otherwise).
'enabled': Whether syncing the buffer is enabled (|:NRS|)
If not executed in a narrowed window, returns an empty dict.
=============================================================================
3. NrrwRgn Tips *NrrwRgn-tips*
To have the filetype in the narrowed window set, you can use this function: >
command! -nargs=* -bang -range -complete=filetype NN
\ :<line1>,<line2> call nrrwrgn#NrrwRgn('',<q-bang>)
\ | set filetype=<args>
<
This lets you select a region, call :NN sql and the selected region will get
the sql filetype set.
(Contributed by @fourjay, thanks!)
=============================================================================
4. NrrwRgn Feedback *NrrwRgn-feedback*
Feedback is always welcome. If you like the plugin, please rate it at the
vim-page:
http://www.vim.org/scripts/script.php?script_id=3075
You can also follow the development of the plugin at github:
http://github.com/chrisbra/NrrwRgn
Please don't hesitate to report any bugs to the maintainer, mentioned in the
third line of this document.
If you like the plugin, write me an email (look in the third line for my mail
address). And if you are really happy, vote for the plugin and consider
looking at my Amazon whishlist: http://www.amazon.de/wishlist/2BKAHE8J7Z6UW
=============================================================================
5. NrrwRgn History *NrrwRgn-history*
0.34: (unreleased) {{{1
- merge Github Pull #34 (https://github.com/chrisbra/NrrwRgn/pull/34, by
Pyrohh, thanks!)
- resize narrowed window to actual size, this won't leave the a lot of
empty lines in the narrowed window.
- don't switch erroneously to the narrowed window on writing
(https://github.com/chrisbra/NrrwRgn/issues/35, reported by Yclept Nemo
thanks!)
- Always write the narrowed scratch window back on |:w| instead of only when
it was modified (https://github.com/chrisbra/NrrwRgn/issues/37, reported by
Konfekt, thanks!)
- Do not resize window, if :NR! was used (patch by leonidborisenko from
https://github.com/chrisbra/NrrwRgn/pull/38 thanks!)
- Various improvements for Window resizing, partly by Yclept Nemo, thanks!
- Fixed error for undefined function and cursor movement in wrong window
(issue https://github.com/chrisbra/NrrwRgn/issues/42 reported by adelarsq,
thanks!)
- Don't set the original buffer to be modified in single-window mode (issue
https://github.com/chrisbra/NrrwRgn/issues/43, reported by agguser, thanks!)
- Don't clean up on BufWinLeave autocommand, so that switching buffers will
not destroy the BufWriteCmd (issue https://github.com/chrisbra/NrrwRgn/issues/44,
reported by agguser, thanks!)
- remove highlighting after closing narrowed buffer
(also issue https://github.com/chrisbra/NrrwRgn/issues/45,
reported by Serabe, thanks!)
- do not map <Leader>nr and <Leader>Nr if g:nrrw_rgn_nomap_<key> is set
(issue https://github.com/chrisbra/NrrwRgn/issues/52, reported by
digitalronin, thanks!)
- correctly highlight in block-wise visual mode, if '$' has been pressed.
- do not set bufhidden=delete for single narrowed windows
https://github.com/chrisbra/NrrwRgn/issues/62
- Make :NUD able to handle git like merge conflicts
https://github.com/chrisbra/NrrwRgn/issues/68
- Correctly jump to the original window when closing
https://github.com/chrisbra/NrrwRgn/issues/70
- Allow to Unremove lines from |:NRP| command
https://github.com/chrisbra/NrrwRgn/issues/72
- Clarify documentation about |:NRP| command
https://github.com/chrisbra/NrrwRgn/issues/71
0.33: Jan 16, 2015 {{{1
- set local options later, so that FileType autocommands don't trigger to
early
- make sure, shortening the buffer name handles multibyte characters
correctly.
- new public function |nrrwrgn#NrrwRgnStatus()|
- <Leader>nr also mapped as operator function (so the region over which you
move will be opened in the narrowed window
- highlighting wrong when char-selecting within a line
- needs Vim 7.4
- mention how to abort the narrowed window (suggested by David Fishburn,
thanks!)
- Execute hooks after the options for the narrowed window have been set
(issue #29, reported by fmorales, thanks!)
- <Leader><Space> Toggles the Narrowed Window Size (idea by David Fishburn,
thanks!)
- New hook b:nrrw_aucmd_written, to be executed, whenever the narrowed info
has been written back into the original buffer.
- g:nrrw_rgn_write_on_sync is being deprecated in favor of using the newly
"written" hook
- error on writing back multi narrowed window (issue #30, reported by
aleprovencio https://github.com/chrisbra/NrrwRgn/issues/30, thanks!)
- document autoresize function (g:nrrw_rgn_autoresize_win)
- error when calling Incr Function, Make it override over global mapping.
(issue #31, reported by zc he https://github.com/chrisbra/NrrwRgn/issues/31, thanks!)
- |:NRP| didn't work as documented (reported by David Fishburn, thanks!)
- fix small syntax error in autoload file (issue #32, reported by itchyny
(https://github.com/chrisbra/NrrwRgn/issues/32, thanks!)
- check, that dict key is available before accessing it (issue #33, reported by SirCorion
(https://github.com/chrisbra/NrrwRgn/issues/33, thanks!)
0.32: Mar 27, 2014 {{{1
- hooks could corrupt the narrowed buffer, if it wasn't closed (reported by
jszakemeister https://github.com/chrisbra/NrrwRgn/issues/19, thanks!)
- Don't parse $VIMRUNTIME/doc/options.txt for finding out buffer-local options
(reported by AguirreIF https://github.com/chrisbra/NrrwRgn/issues/21,
thanks!), instead include a fix set of option names to set when opening the
narrowed buffer.
- Switching buffers in the original narrowed buffer, may confuse NrrwRgn.
- Code cleanup (no more separate functions for visual and normal mode)
- fix issue 22 (characterwise narrowing was brocken in last commit, reported
by Matthew Boehm in https://github.com/chrisbra/NrrwRgn/issues/22, thanks!)
- in characterwise visual selection, trailing \n is not stripped when writing
(reported by Matthew Boehm in https://github.com/chrisbra/NrrwRgn/23,
thanks!)
- highlighting was wrong for characterwise visual selections
- update original window for multi narrowed regions (
https://github.com/chrisbra/NrrwRgn/24, reported by Dane Summers, thanks!),
use the g:nrrw_rgn_update_orig_win variable to enable
- error when narrowed window was moved to new tab and trying to quit
(https://github.com/chrisbra/NrrwRgn/2, reported by Mario Ricalde, thanks!)
- better default names for the narrowed window
(https://github.com/chrisbra/Nrrwrgn/28, reported by Mario Ricalde, thanks!)
- when setting g:nrrw_rgn_write_on_sync the original file will be saved,
whenever the narrowed window is written back
(https://github.com/chrisbra/26, reported by Mario Ricalde, thanks!)
- Some more error handling when using |:WidenRegion|
- Make sure highlighting is removed when using |:WidenRegion|
0.31: Feb 16, 2013 {{{1
- NRM threw some errors (reported by pydave in
https://github.com/chrisbra/NrrwRgn/issues/17, thanks!)
- don't create swapfiles (reported by ping, thanks!)
0.30: Jan 25, 2013 {{{1
- |NRL| throws erros, when used without having first narrowed a region
- |NRV!| not allowed (reported by ping, thanks!)
- when using single window narrowing, :w would jump back to the original
window. Only do this, when 'hidden' is not set (reported by ping, thanks!)
- when narrowing a region, the last visual selected region wasn't correctly
restored (reported by ping, thanks!)
- some code cleanup
- recursive narrowing was broken, fix it (reported by ping, thanks!)
0.29: Aug 20, 2012 {{{1
- Use ! to have the narrowed buffer not opened in a new window (suggested by
Greg Sexton thanks!, issue #8
https://github.com/chrisbra/NrrwRgn/issues/8)
- Fix mappings for visual mode (https://github.com/chrisbra/NrrwRgn/issues/9,
reported by Sung Pae, thanks!)
- Fix problem with setting the filetype
(https://github.com/chrisbra/NrrwRgn/issues/10, reported by Hong Xu,
thanks!)
- Fix some minor problems, when using ! mode
0.28: Jun 03, 2012 {{{1
- Plugin did not store last narrowed region when narrowed window was moved to
another tabpage (reported by Ben Fritz, thanks!)
0.27: May 17, 2012 {{{1
- When using |:NR| on a line that is folded, include the whole folded region
in the Narrowed window.
- Better filetype detection for comments
- Error handling, when doing |:NRM| without doing |:NRP| first
- Use |:NRP!| to clear the old selection
- Don't load the autoload script when sourcing the plugin script
(reported by Sergey Khorev, thanks!)
- Vim 7.3.449 introduced E855, prevent this error.
- |:NRL|
- |NRM| did not correctly parse the list of lines provided by |:NRP|
- highlighted pattern for blockwise visual narrowed regions was wrong
- Saving blockwise visual selected regions back, could corrupt the contents
0.26: Jan 02, 2012 {{{1
- Fix issue https://github.com/chrisbra/NrrwRgn/issues/7
(reported by Alessio B., thanks!)
0.25: Nov 08, 2011 {{{1
- updated documentation (patch by Jean, thanks!)
- make it possible, to not sync the narrowed buffer back by disabling
it using |:NRSyncOnWrite| |:NRNoSyncOnWrite|
0.24: Oct 24, 2011 {{{1
- error on vim.org page, reuploaded version 0.22 as 0.24
0.23: Oct 24, 2011 {{{1
- (wrongly uploaded to vim.org)
0.22: Oct 24, 2011 {{{1
- Allow customization via the use of hooks (|NR-hooks|)
0.21: July 26, 2011 {{{1
- Fix undefined variable adjust_line_numbers
https://github.com/chrisbra/NrrwRgn/issues/5 (reported by jmcantrell,
thanks!)
0.20: July 25, 2011 {{{1
- allow customization via the g:nrrw_topbot_leftright variable (Thanks Herbert
Sitz!)
- allow what options will be applied using the g:nrrw_custom_options dict
(suggested by Herbert Sitz. Thanks!)
- NRV didn't hightlight the region that was selected (reported by Herbert
Sitz, thanks!)
- use the g:nrrw_rgn_protect variable, to prevent that the original buffer
will be protected. This is useful, if you narrow several regions of the same
buffer and want to write those changes indepentently (reported by kolyuchiy
in https://github.com/chrisbra/NrrwRgn/issues/3, Thanks!)
- fix an error with not correctly deleting the highlighted region, that was
discovered when reporting issue 3 (see above). (Reported by kolyuchiy,
thanks!)
- Catch errors, when setting window local options. (Patch by Sung Pae,
Thanks!)
0.19: May 22, 2011 {{{1
- fix issue 2 from github https://github.com/chrisbra/NrrwRgn/issues/2
(Widening does not work, if the narrowed windows have been moved to a new
tabspace). Reported by vanschelven, thanks!
0.18: December 10, 2010 {{{1
- experimental feature: Allow to Narrow several different regions at once
using :g/pattern/NRP and afterwards calling :NRM
(This only works linewise. Should that be made possible for any reagion?)
- disable folds, before writing changes back, otherwise chances are, you'll
lose more data then wanted
- code cleanup
0.17: November 23, 2010 {{{1
- cache the options, that will be set (instead of parsing
$VIMRUNTIME/doc/options.txt every time) in the Narrowed Window
- getting the options didn't work, when using an autocommand like this:
autocmd BufEnter * cd %:p:h
(reported by Xu Hong, Thanks!)
- :q didn't clean up the Narrowed Buffer correctly. Fix this
- some code cleanup
0.16: November 16, 2010 {{{1
- Bugfix: copy all local options to the narrowed window (reported by Xu Hong,
Thanks!)
0.15: August 26, 2010 {{{1
- Bugfix: minor documentation update (reported by Hong Xu, Thanks!)
0.14: August 26, 2010 {{{1
- Bugfix: :only in the original buffer resulted in errors (reported by Adam
Monsen, Thanks!)
0.13: August 22, 2010 {{{1
- Unified Diff Handling (experimental feature)
0.12: July 29, 2010 {{{1
- Version 0.11, wasn't packaged correctly and the vimball file
contained some garbage. (Thanks Dennis Hostetler!)
0.11: July 28, 2010 {{{1
- Don't set 'winfixwidth' and 'winfixheight' (suggested by Charles Campbell)
0.10: May 20, 2010 {{{1
- Restore cursor position using winrestview() and winsaveview()
- fix a bug, that prevented the use of visual narrowing
- Make sure when closing the narrowed buffer, the content will be written to
the right original region
- use topleft for opening the Narrowed window
- check, that the original buffer is still available
- If you Narrow the complete buffer using :NRV and write the changes back, an
additional trailing line is inserted. Remove that line.
- When writing the changes back, update the highlighting.
0.9: May 20, 2010 {{{1
- It is now possible to Narrow a window recursively. This allows to have
several narrowed windows, and allows for example to only diff certain
regions (as was suggested in a recent thread at the vim_use mailinglist:
http://groups.google.com/group/vim_use/msg/05d7fd9bd1556f0e) therefore, the
use for the g:nrrw_rgn_sepwin variable isn't necessary anymore.
- Small documentation updates
0.8: May 18, 2010 {{{1
- the g:nrrw_rgn_sepwin variable can be used to force separate Narrowed
Windows, so you could easily diff those windows.
- make the separating of several windows a little bit safer (look at the
bufnr(), so it should work without problems for several buffers)
- switch from script local variables to buffer local variables, so narrowing
for several buffers should work.
- set 'winfixheight' for narrowed window
- Added command :NRV (suggested by Charles Campbell, thanks!)
- added error handling, in case :NRV is called, without a selected region
- take care of beeps, when calling :NRV
- output WarningMsg
0.7: May 17, 2010 {{{1
- really use the black hole register for deleting the old buffer contents in
the narrowed buffer (suggestion by esquifit in
http://groups.google.com/group/comp.editors/msg/3eb3e3a7c68597db)
- make autocommand nesting, so the highlighting will be removed when writing
the buffer contents.
- Use g:nrrw_rgn_nohl variable to disable highlighting (as this can be
disturbing).
0.6: May 04, 2010 {{{1
- the previous version had problems restoring the orig buffer, this version
fixes it (highlighting and setl ma did not work correctly)
0.5: May 04, 2010 {{{1
- The mapping that allows for narrowing a visually selected range, did not
work. (Fixed!)
- Make :WidenRegion work as expected (close the widened window) (unreleased)
0.4: Apr 28, 2010 {{{1
- Highlight narrowed region in the original buffer
- Save and Restore search-register
- Provide shortcut commands |:NR|
- Provide command |:NW| and |:NarrowWindow|
- Make plugin autoloadable
- Enable GLVS (see |:GLVS|)
- Provide Documenation (:h NarrowRegion)
- Distribute Plugin as vimball |pi_vimball.txt|
0.3: Apr 28, 2010 {{{1
- Initial upload
- development versions are available at the github repository
- put plugin on a public repository (http://github.com/chrisbra/NrrwRgn)
}}}
==============================================================================
Modeline:
vim:tw=78:ts=8:ft=help:et:fdm=marker:fdl=0:norl

View File

@ -0,0 +1,50 @@
:NR NarrowRegion.txt /*:NR*
:NRL NarrowRegion.txt /*:NRL*
:NRMulti NarrowRegion.txt /*:NRMulti*
:NRN NarrowRegion.txt /*:NRN*
:NRNoSyncOnWrite NarrowRegion.txt /*:NRNoSyncOnWrite*
:NRPrepare NarrowRegion.txt /*:NRPrepare*
:NRS NarrowRegion.txt /*:NRS*
:NRSyncOnWrite NarrowRegion.txt /*:NRSyncOnWrite*
:NRUnprepare NarrowRegion.txt /*:NRUnprepare*
:NRV NarrowRegion.txt /*:NRV*
:NUD NarrowRegion.txt /*:NUD*
:NW NarrowRegion.txt /*:NW*
:NarrowRegion NarrowRegion.txt /*:NarrowRegion*
:NarrowWindow NarrowRegion.txt /*:NarrowWindow*
:WR NarrowRegion.txt /*:WR*
:WidenRegion NarrowRegion.txt /*:WidenRegion*
<Plug>NrrwrgnDo NarrowRegion.txt /*<Plug>NrrwrgnDo*
<Plug>NrrwrgnWinIncr NarrowRegion.txt /*<Plug>NrrwrgnWinIncr*
NR-HowTo NarrowRegion.txt /*NR-HowTo*
NR-hooks NarrowRegion.txt /*NR-hooks*
NR-multi-example NarrowRegion.txt /*NR-multi-example*
NarrowRegion NarrowRegion.txt /*NarrowRegion*
NrrwRgn-config NarrowRegion.txt /*NrrwRgn-config*
NrrwRgn-feedback NarrowRegion.txt /*NrrwRgn-feedback*
NrrwRgn-func NarrowRegion.txt /*NrrwRgn-func*
NrrwRgn-history NarrowRegion.txt /*NrrwRgn-history*
NrrwRgn-hook NarrowRegion.txt /*NrrwRgn-hook*
NrrwRgn-manual NarrowRegion.txt /*NrrwRgn-manual*
NrrwRgn-tips NarrowRegion.txt /*NrrwRgn-tips*
NrrwRgn.txt NarrowRegion.txt /*NrrwRgn.txt*
NrrwRgnPlugin NarrowRegion.txt /*NrrwRgnPlugin*
b:nrrw_aucmd_close NarrowRegion.txt /*b:nrrw_aucmd_close*
b:nrrw_aucmd_create NarrowRegion.txt /*b:nrrw_aucmd_create*
b:nrrw_aucmd_writepost NarrowRegion.txt /*b:nrrw_aucmd_writepost*
b:nrrw_aucmd_written NarrowRegion.txt /*b:nrrw_aucmd_written*
g:nrrw_custom_options NarrowRegion.txt /*g:nrrw_custom_options*
g:nrrw_rgn_hl NarrowRegion.txt /*g:nrrw_rgn_hl*
g:nrrw_rgn_incr NarrowRegion.txt /*g:nrrw_rgn_incr*
g:nrrw_rgn_nohl NarrowRegion.txt /*g:nrrw_rgn_nohl*
g:nrrw_rgn_nomap_Nr NarrowRegion.txt /*g:nrrw_rgn_nomap_Nr*
g:nrrw_rgn_nomap_nr NarrowRegion.txt /*g:nrrw_rgn_nomap_nr*
g:nrrw_rgn_protect NarrowRegion.txt /*g:nrrw_rgn_protect*
g:nrrw_rgn_rel_max NarrowRegion.txt /*g:nrrw_rgn_rel_max*
g:nrrw_rgn_rel_min NarrowRegion.txt /*g:nrrw_rgn_rel_min*
g:nrrw_rgn_resize_window NarrowRegion.txt /*g:nrrw_rgn_resize_window*
g:nrrw_rgn_update_orig_win, NarrowRegion.txt /*g:nrrw_rgn_update_orig_win,*
g:nrrw_rgn_vert NarrowRegion.txt /*g:nrrw_rgn_vert*
g:nrrw_rgn_wdth NarrowRegion.txt /*g:nrrw_rgn_wdth*
g:nrrw_topbot_leftright NarrowRegion.txt /*g:nrrw_topbot_leftright*
nrrwrgn#NrrwRgnStatus() NarrowRegion.txt /*nrrwrgn#NrrwRgnStatus()*

View File

@ -0,0 +1,91 @@
" NrrwRgn.vim - Narrow Region plugin for Vim
" -------------------------------------------------------------
" Version: 0.33
" Maintainer: Christian Brabandt <cb@256bit.org>
" Last Change: Thu, 15 Jan 2015 20:52:29 +0100
" Script: http://www.vim.org/scripts/script.php?script_id=3075
" Copyright: (c) 2009-2015 by Christian Brabandt
" The VIM LICENSE applies to NrrwRgn.vim
" (see |copyright|) except use "NrrwRgn.vim"
" instead of "Vim".
" No warranty, express or implied.
" *** *** Use At-Your-Own-Risk! *** ***
" GetLatestVimScripts: 3075 33 :AutoInstall: NrrwRgn.vim
"
" Init: {{{1
let s:cpo= &cpo
if exists("g:loaded_nrrw_rgn") || &cp
finish
endif
set cpo&vim
let g:loaded_nrrw_rgn = 1
" Debug Setting
let s:debug=0
if s:debug
exe "call nrrwrgn#Debug(1)"
endif
" ----------------------------------------------------------------------------
" Public Interface: {{{1
" plugin functions "{{{2
fun! <sid>NrrwRgnOp(type, ...) " {{{3
" used for operator function mapping
let sel_save = &selection
let &selection = "inclusive"
if a:0 " Invoked from Visual mode, use '< and '> marks.
sil exe "normal! `<" . a:type . "`>y"
elseif a:type == 'line'
sil exe "normal! '[V']y"
elseif a:type == 'block'
sil exe "normal! `[\<C-V>`]y"
else
sil exe "normal! `[v`]y"
endif
call nrrwrgn#NrrwRgn(visualmode(), '')
let &selection = sel_save
endfu
" Define the Command aliases "{{{2
com! -range -bang NRPrepare :<line1>,<line2>NRP<bang>
com! -bang -range NarrowRegion :<line1>,<line2>NR
com! -bang NRMulti :NRM<bang>
com! -bang NarrowWindow :NW
com! -bang NRLast :NRL
" Define the actual Commands "{{{2
com! -range -bang NR :<line1>, <line2>call nrrwrgn#NrrwRgn('',<q-bang>)
com! -range -bang NRP :<line1>, <line2>call nrrwrgn#Prepare(<q-bang>)
com! -bang -range NRV :call nrrwrgn#NrrwRgn(visualmode(), <q-bang>)
com! -range NRUnprepare :<line1>, <line2>call nrrwrgn#Unprepare()
com! NUD :call nrrwrgn#UnifiedDiff()
com! -bang NW :exe ":" . line('w0') . ',' . line('w$') . "call nrrwrgn#NrrwRgn(0,<q-bang>)"
com! -bang NRM :call nrrwrgn#NrrwRgnDoMulti(<q-bang>)
com! -bang NRL :call nrrwrgn#LastNrrwRgn(<q-bang>)
" Define the Mapping: "{{{2
if !hasmapto('<Plug>NrrwrgnDo') && !get(g:, 'nrrw_rgn_nomap_nr', 0)
xmap <unique> <Leader>nr <Plug>NrrwrgnDo
nmap <unique> <Leader>nr <Plug>NrrwrgnDo
endif
if !hasmapto('<Plug>NrrwrgnBangDo') && !get(g:, 'nrrw_rgn_nomap_Nr', 0)
xmap <unique> <Leader>Nr <Plug>NrrwrgnBangDo
endif
if !hasmapto('VisualNrrwRgn')
xnoremap <unique> <script> <Plug>NrrwrgnDo <sid>VisualNrrwRgn
nnoremap <unique> <script> <Plug>NrrwrgnDo <sid>VisualNrrwRgn
endif
if !hasmapto('VisualNrrwRgnBang')
xnoremap <unique> <script> <Plug>NrrwrgnBangDo <sid>VisualNrrwBang
endif
xnoremap <sid>VisualNrrwRgn :<c-u>call nrrwrgn#NrrwRgn(visualmode(),'')<cr>
xnoremap <sid>VisualNrrwBang :<c-u>call nrrwrgn#NrrwRgn(visualmode(),'!')<cr>
" operator function
nnoremap <sid>VisualNrrwRgn :set opfunc=<sid>NrrwRgnOp<cr>g@
" Restore: "{{{1
let &cpo=s:cpo
unlet s:cpo
" vim: ts=4 sts=4 fdm=marker com+=l\:\"

View File

@ -0,0 +1,58 @@
#!/usr/bin/perl
use strict;
use warnings;
use WWW::Mechanize;
sub GetPassword() {
my $i=0;
my @pass;
my $passfile="./vim_passfile"; # line1: username, line2: password
open(PASS, '<',$passfile) or die "Can't open passwordfile: $passfile\n";
while(<PASS>){
chomp;
$pass[$i++] = $_;
}
close(PASS);
return @pass;
}
my $sid=3075;
my $file;
my @files=glob('*.vmb');
#my $scriptversion=shift @ARGV;
my $scriptversion = 0;
my $versioncomment=shift @ARGV;
unless ($versioncomment){
print "Please enter comment!\n";
exit;
}
$versioncomment.="\n(automatically uploaded)";
my @userpasswordpair = GetPassword();
for (@files) {
my $f = $_ if [ -f $_ ] && $_ =~ /\w+-[^.]+\.(\d+)\.vmb/;
if ($1 > $scriptversion) {
$scriptversion=$1;
$file = $f;
}
}
my $mech=WWW::Mechanize->new(autocheck => 1);
$mech->get("http://www.vim.org/login.php");
$mech->submit_form(
form_name => "login",
with_fields => {
userName => $userpasswordpair[0],
password => $userpasswordpair[1],
},
);
$mech->get("http://www.vim.org/scripts/script.php?script_id=$sid");
$mech->follow_link(text => 'upload new version');
$mech->form_name("script");
$mech->field(script_file => $file);
$mech->field(vim_version => 7.4);
$mech->field(script_version => $scriptversion);
$mech->field(version_comment => $versioncomment);
$mech->click_button(value => "upload");

Binary file not shown.

After

Width:  |  Height:  |  Size: 402 KiB

View File

@ -0,0 +1,8 @@
#!/nix/store/306znyj77fv49kwnkpxmb0j2znqpa8bj-bash-5.2p26/bin/sh
for i in */; do
cd "$i"
./cmd.sh
rm -f *.mod
cd - > /dev/null
done

View File

@ -0,0 +1 @@
foobar 1.txt

View File

@ -0,0 +1 @@
foobar 1.txt some more stuff

View File

@ -0,0 +1 @@
foobar 2.txt

View File

@ -0,0 +1 @@
foobar 2.txt some more stuff

View File

@ -0,0 +1 @@
foobar 3.txt

View File

@ -0,0 +1 @@
foobar 3.txt some more stuff

View File

@ -0,0 +1,23 @@
#!/nix/store/306znyj77fv49kwnkpxmb0j2znqpa8bj-bash-5.2p26/bin/bash
#set -x
dir="$(realpath ../..)"
LC_ALL=C vim -u NONE -N \
--cmd ':set noswapfile hidden' \
--cmd 'argadd *.txt' \
-c "sil :so $dir/plugin/NrrwRgn.vim" \
-c 'sil :bufdo :NRP' \
-c 'sil :NRM' \
-c 'sil :g/^foobar.*/s//& some more stuff/' \
-c 'sil :wq' \
-c ':bufdo if bufname("")=~"^\\d\\.txt$"|saveas! %.mod|endif' \
-c ':qa!'
rt=$(diff -uN0 <(cat *.mod) <(cat *.ok))
if [ "$?" -ne 0 ]; then
printf "Test1 failed\n"
printf "Diff:\n%s" "$rt"
exit 2;
else
printf "Test1 successful!\n"
fi

View File

@ -0,0 +1 @@
## Test 1: Multi Narrowed Window for several distinct buffers ##

View File

@ -0,0 +1,4 @@
This is a single test.
Line 2
Line 3
Line 4

View File

@ -0,0 +1,6 @@
This is a single test.
Line 2
Line 3
Line 4
Added Line
Added after Narrowing Line

View File

@ -0,0 +1,24 @@
#!/nix/store/306znyj77fv49kwnkpxmb0j2znqpa8bj-bash-5.2p26/bin/bash
#set -x
Test="Test2"
dir="$(realpath ../..)"
LC_ALL=C vim -u NONE -N \
--cmd ':set noswapfile hidden' \
-c "sil :so $dir/plugin/NrrwRgn.vim" \
-c 'sil :1,$NR' \
-c 'sil :$put =\"Added Line\"' \
-c 'sil :wq' \
-c 'sil :$put =\"Added after Narrowing Line\"' \
-c ':bufdo if bufname("")=~"^\\d\\.txt$"|saveas! %.mod|endif' \
-c ':qa!' 1.txt
rt=$(diff -uN0 <(cat *.mod) <(cat *.ok))
if [ "$?" -ne 0 ]; then
printf "$Test failed\n"
printf "Diff:\n%s" "$rt"
exit 2;
else
printf "$Test successful!\n"
fi

View File

@ -0,0 +1 @@
## Test 2: Single Narrowed Window for a single buffers ##

View File

@ -0,0 +1,4 @@
This is a single test.
Line 2
Line 3
Line 4

View File

@ -0,0 +1,6 @@
This is a single test.
Line 2
Line 3
Line 4
Added Line
Added after Narrowing Line

View File

@ -0,0 +1,26 @@
#!/nix/store/306znyj77fv49kwnkpxmb0j2znqpa8bj-bash-5.2p26/bin/bash
#set -x
Test="Test3"
dir="$(realpath ../..)"
LC_ALL=C vim -u NONE -N \
--cmd ':set noswapfile hidden' \
-c "sil :so $dir/plugin/NrrwRgn.vim" \
-c 'sp description.md | noa wincmd p | :e 1.txt' \
-c 'sil :1,$NR' \
-c 'sil :$put =\"Added Line\"' \
-c 'sil :wq' \
-c '2wincmd w' \
-c 'sil :$put =\"Added after Narrowing Line\"' \
-c ':bufdo if bufname("")=~"^\\d\\.txt$"|saveas! %.mod|endif' \
-c ':qa!'
rt=$(diff -uN0 <(cat *.mod) <(cat *.ok))
if [ "$?" -ne 0 ]; then
printf "$Test failed\n"
printf "Diff:\n%s" "$rt"
exit 2;
else
printf "$Test successful!\n"
fi

View File

@ -0,0 +1 @@
## Test 2: Single Narrowed Window for a single buffer, but has another split window ##

View File

@ -0,0 +1,4 @@
This is a single test.
Line 2
Line 3
Line 4

View File

@ -0,0 +1,6 @@
This is a single test.
Line 2
Line 3
Line 4
Added Line
Added another Line

View File

@ -0,0 +1,26 @@
#!/nix/store/306znyj77fv49kwnkpxmb0j2znqpa8bj-bash-5.2p26/bin/bash
#set -x
Test="Test4"
dir="$(realpath ../..)"
LC_ALL=C vim -u NONE -N \
--cmd ':set noswapfile hidden' \
-c "sil :so $dir/plugin/NrrwRgn.vim" \
-c ':e 1.txt' \
-c 'sil :1,$NR!' \
-c 'sil :$put =\"Added Line\"' \
-c ':w|b#' \
-c ':saveas! 1.txt.mod' \
-c '2b|w|b#|b#' \
-c 'sil :$put =\"Added another Line\"' \
-c ':w|b#|wq!'
rt=$(diff -uN0 <(cat *.mod) <(cat *.ok))
if [ "$?" -ne 0 ]; then
printf "$Test failed\n"
printf "Diff:\n%s\n" "$rt"
exit 2;
else
printf "$Test successful!\n"
fi

View File

@ -0,0 +1 @@
## Test 4: Single Narrowed Window for a single buffer, switching back and forth between buffers several times (issue #44)

View File

@ -0,0 +1,4 @@
This is a single test.
Line 2
Line 3
Line 4

View File

@ -0,0 +1,6 @@
This is a single test.
Line 2
Line 3
Line 4
Added Line
[]

View File

@ -0,0 +1,25 @@
#!/nix/store/306znyj77fv49kwnkpxmb0j2znqpa8bj-bash-5.2p26/bin/bash
#set -x
Test=`basename $PWD`
dir="$(realpath ../..)"
LC_ALL=C vim -u NONE -N \
--cmd ':set noswapfile hidden' \
-c "sil :so $dir/plugin/NrrwRgn.vim" \
-c ':e 1.txt' \
-c ':saveas! 1.txt.mod' \
-c 'sil :1,$NR!' \
-c 'sil :$put =\"Added Line\"' \
-c ':wq' \
-c ':$put =string(getmatches())' \
-c ':wq!'
rt=$(diff -uN0 <(cat *.mod) <(cat *.ok))
if [ "$?" -ne 0 ]; then
printf "$Test failed\n"
printf "Diff:\n%s\n" "$rt"
exit 2;
else
printf "$Test successful!\n"
fi

View File

@ -0,0 +1 @@
## Test 5: Make sure, highlighting is removed after closing narrowed window

View File

@ -0,0 +1,4 @@
This is a single test.
Line 2
Line 3
Line 4

View File

@ -0,0 +1,6 @@
This is a single test.
Line 2
Line 3
Line 4
Added Line
[]

View File

@ -0,0 +1,25 @@
#!/nix/store/306znyj77fv49kwnkpxmb0j2znqpa8bj-bash-5.2p26/bin/bash
#set -x
Test=`basename $PWD`
dir="$(realpath ../..)"
LC_ALL=C vim -u NONE -N \
--cmd ':set noswapfile hidden' \
-c "sil :so $dir/plugin/NrrwRgn.vim" \
-c ':e 1.txt' \
-c ':saveas! 1.txt.mod' \
-c 'exe ":1norm VG"|:norm \nr' \
-c 'sil :$put =\"Added Line\"' \
-c ':wq' \
-c ':$put =string(getmatches())' \
-c ':wq!'
rt=$(diff -uN0 <(cat *.mod) <(cat *.ok))
if [ "$?" -ne 0 ]; then
printf "$Test failed\n"
printf "Diff:\n%s\n" "$rt"
exit 2;
else
printf "$Test successful!\n"
fi

View File

@ -0,0 +1 @@
## Test 6: Make sure, highlighting is removed after closing narrowed window (for visual narrowed region)

View File

@ -0,0 +1,4 @@
This is a single test.
Line 2
Line 3
Line 4

View File

@ -0,0 +1,5 @@
This is a single test.
Line 2
Line 3
Line 4
[]

View File

@ -0,0 +1,25 @@
#!/nix/store/306znyj77fv49kwnkpxmb0j2znqpa8bj-bash-5.2p26/bin/bash
#set -x
Test=`basename $PWD`
dir="$(realpath ../..)"
LC_ALL=C vim -u NONE -N \
--cmd ':set noswapfile hidden' \
-c "sil :so $dir/plugin/NrrwRgn.vim" \
-c ':e 1.txt' \
-c ':saveas! 1.txt.mod' \
-c ':%NR' \
-c ':q!' \
-c ':set modifiable' \
-c ':$put =string(getmatches())' \
-c ':wq!'
rt=$(diff -uN0 <(cat *.mod) <(cat *.ok))
if [ "$?" -ne 0 ]; then
printf "$Test failed\n"
printf "Diff:\n%s\n" "$rt"
exit 2;
else
printf "$Test successful!\n"
fi

View File

@ -0,0 +1 @@
## Test 6: Make sure, highlighting is removed after aborting narrowed window (issue #44)

View File

@ -0,0 +1,31 @@
(issues to work on, in the order of importance):
- clean up does not work? (instn data isn't removed after :wq in narrowed window)
- When disabling Narrowing using :NRN and one quits the buffer,
should the highlighting be removed? (it doesn't yet)
- I really need some tests, like:
* Visual Narrowing
* Narrowing 1 Region
* Narrowing several independent regions with setting the g:nrrw_rgn_protect
variable
* Multi line narrowing
### Resolved
- When switching buffers using :b in the window, that was narrowed, the highlighting is keept.
(should be resolved)
- highlighting wrong for char visual mode
(e.g. select a word, press \nr and the whole line will be highlighted)
- Use nrrwrgn#NrrwRgnStatus() to make a nice statusline for e.g. vim-airline
-> latest airline supports a nice statusline for narrowed window
- Implement MultiEditing for :NRM
(see https://github.com/felixr/vim-multiedit/blob/master/plugin/multiedit.vim
and http://blog.felixriedel.com/2012/06/multi-editing-in-vim/)
-> Invalid: multiple cursors is different, not part of a narrowed feature
- New Mapping. The motion over which one moves will be opened in a narrowed window.
-> <leader>nr in normal mode

View File

@ -0,0 +1 @@
github: [sindrets]

View File

@ -0,0 +1,138 @@
name: Bug report
description: Report a problem with diffview.nvim
title: "[Bug] "
labels: [bug]
body:
- type: markdown
attributes:
value: |
Before reporting: search [existing issues](https://github.com/sindrets/diffview.nvim/issues) and make sure that diffview.nvim is updated to the latest version.
- type: textarea
attributes:
label: "Description"
description: "A description of the problem you're facing."
validations:
required: true
- type: textarea
attributes:
label: "Expected behavior"
description: "A description of the behavior you expected:"
- type: textarea
attributes:
label: "Actual behavior"
description: "Observed behavior (may optionally include images, or videos)."
validations:
required: true
- type: textarea
attributes:
label: "Steps to reproduce"
description: "Steps to reproduce the issue, preferably using the minimal config provided below."
placeholder: |
1. `nvim --clean -u mini.lua`
2. ...
validations:
required: true
- type: textarea
attributes:
label: "Health check"
value: |
<details>
<summary>Output of <code>:checkhealth diffview</code></summary>
```
#######################
### PUT OUTPUT HERE ###
#######################
```
</details>
validations:
required: true
- type: textarea
attributes:
label: "Log info"
description: "Include relevant info from `:DiffviewLog`. Look at the time stamps of the log messages to determine what is relevant. Please do not include the entire log."
value: |
<details>
<summary>Relevant info from <code>:DiffviewLog</code></summary>
```
############################
### PUT LOG CONTENT HERE ###
############################
```
</details>
validations:
required: false
- type: textarea
attributes:
label: "Neovim version"
description: "Output of `nvim --version`"
render: markdown
placeholder: |
NVIM v0.9.0
Build type: RelWithDebInfo
LuaJIT 2.1.0-beta3
validations:
required: true
- type: input
attributes:
label: "Operating system and version"
description: "On \\*nix systems you can use the output of `uname -srom`"
placeholder: "Linux 6.3.1-arch2-1 x86_64 GNU/Linux"
validations:
required: true
- type: textarea
attributes:
label: "Minimal config"
description: "If possible, please provide a **minimal** configuration necessary to reproduce the issue. Save this as `mini.lua`. If *absolutely* necessary, add plugins and config options from your `init.lua` at the indicated lines."
render: Lua
value: |
-- #######################################
-- ### USAGE: nvim --clean -u mini.lua ###
-- #######################################
local root = vim.fn.stdpath("run") .. "/nvim/diffview.nvim"
local plugin_dir = root .. "/plugins"
vim.fn.mkdir(plugin_dir, "p")
for _, name in ipairs({ "config", "data", "state", "cache" }) do
vim.env[("XDG_%s_HOME"):format(name:upper())] = root .. "/" .. name
end
local plugins = {
{ "nvim-web-devicons", url = "https://github.com/nvim-tree/nvim-web-devicons.git" },
{ "diffview.nvim", url = "https://github.com/sindrets/diffview.nvim.git" },
-- ##################################################################
-- ### ADD PLUGINS THAT ARE _NECESSARY_ FOR REPRODUCING THE ISSUE ###
-- ##################################################################
}
for _, spec in ipairs(plugins) do
local install_path = plugin_dir .. "/" .. spec[1]
if vim.fn.isdirectory(install_path) ~= 1 then
if spec.url then
print(string.format("Installing '%s'...", spec[1]))
vim.fn.system({ "git", "clone", "--depth=1", spec.url, install_path })
end
end
vim.opt.runtimepath:append(spec.path or install_path)
end
require("diffview").setup({
-- ##############################################################################
-- ### ADD DIFFVIEW.NVIM CONFIG THAT IS _NECESSARY_ FOR REPRODUCING THE ISSUE ###
-- ##############################################################################
})
vim.opt.termguicolors = true
vim.cmd("colorscheme " .. (vim.fn.has("nvim-0.8") == 1 and "habamax" or "slate"))
-- ############################################################################
-- ### ADD INIT.LUA SETTINGS THAT ARE _NECESSARY_ FOR REPRODUCING THE ISSUE ###
-- ############################################################################
print("Ready!")
validations:
required: false

View File

@ -0,0 +1 @@
blank_issues_enabled: true

View File

@ -0,0 +1,3 @@
/doc/tags
/.tests/
/.dev/

View File

@ -0,0 +1,4 @@
-- Global objects defined by C code.
read_globals = {
"vim"
}

View File

@ -0,0 +1,15 @@
{
"$schema": "https://raw.githubusercontent.com/sumneko/vscode-lua/master/setting/schema.json",
"Lua.diagnostics.disable": [
"assign-type-mismatch",
"cast-type-mismatch",
"missing-fields"
],
"runtime.version": "LuaJIT",
"workspace.checkThirdParty": false,
"workspace.library": [
"./lua",
"$VIMRUNTIME/lua",
".dev/lua/nvim"
]
}

View File

@ -0,0 +1,16 @@
Diffview.nvim is a tool for Neovim that adds an interface that enables
easy review of changes made in any Git revision.
Copyright (C) 2021 Sindre T. Strøm
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.

View File

@ -0,0 +1,33 @@
.PHONY: all
all: dev test
TEST_PATH := $(if $(TEST_PATH),$(TEST_PATH),lua/diffview/tests/)
export TEST_PATH
# Usage:
# Run all tests:
# $ make test
#
# Run tests for a specific path:
# $ TEST_PATH=tests/some/path make test
.PHONY: test
test:
nvim --headless -i NONE -n -u scripts/test_init.lua -c \
"PlenaryBustedDirectory $(TEST_PATH) { minimal_init = './scripts/test_init.lua' }"
.PHONY: dev
dev: .dev/lua/nvim
.dev/lua/nvim:
mkdir -p "$@"
git clone --filter=blob:none https://github.com/folke/neodev.nvim.git "$@/repo"
cd "$@/repo" && git -c advice.detachedHead=false checkout ce9a2e8eaba5649b553529c5498acb43a6c317cd
cp "$@/repo/types/nightly/uv.lua" \
"$@/repo/types/nightly/cmd.lua" \
"$@/repo/types/nightly/alias.lua" \
"$@/"
rm -rf "$@/repo"
.PHONY: clean
clean:
rm -rf .tests .dev

View File

@ -0,0 +1,524 @@
# Diffview.nvim
Single tabpage interface for easily cycling through diffs for all modified files
for any git rev.
![preview](https://user-images.githubusercontent.com/2786478/131269942-e34100dd-cbb9-48fe-af31-6e518ce06e9e.png)
## Introduction
Vim's diff mode is pretty good, but there is no convenient way to quickly bring
up all modified files in a diffsplit. This plugin aims to provide a simple,
unified, single tabpage interface that lets you easily review all changed files
for any git rev.
## Requirements
- Git ≥ 2.31.0 (for Git support)
- Mercurial ≥ 5.4.0 (for Mercurial support)
- Neovim ≥ 0.7.0 (with LuaJIT)
- [nvim-web-devicons](https://github.com/nvim-tree/nvim-web-devicons) (optional) For file icons
## Installation
Install the plugin with your package manager of choice.
```vim
" Plug
Plug 'sindrets/diffview.nvim'
```
```lua
-- Packer
use "sindrets/diffview.nvim"
```
## Merge Tool
![merge tool showcase](https://user-images.githubusercontent.com/2786478/188286293-13bbf0ab-3595-425d-ba4a-12f514c17eb6.png)
Opening a diff view during a merge or a rebase will list the conflicted files in
their own section. When opening a conflicted file, it will open in a 3-way diff
allowing you to resolve the merge conflicts with the context of the target
branch's version, as well as the version from the branch which is being merged.
The 3-way diff is only the default layout for merge conflicts. There are
multiple variations on this layout, a 4-way diff layout, and a single window
layout available.
In addition to the normal `:h copy-diffs` mappings, there are default mappings
provided for jumping between conflict markers, obtaining a hunk directly from
any of the diff buffers, and accepting any one, all, or none of the versions of
a file given by a conflict region.
For more information on the merge tool, mappings, layouts and how to
configure them, see:
- `:h diffview-merge-tool`
- `:h diffview-config-view.x.layout`
## File History
![file history showcase](https://user-images.githubusercontent.com/2786478/188331057-f9ec9a0d-8cda-4ff8-ac98-febcc7aa4010.png)
The file history view allows you to list all the commits that affected a given
set of paths, and view the changes made in a diff split. This is a porcelain
interface for git-log, and supports a good number of its options. Things like:
- Filtering commits by grepping commit messages and commit authors.
- Tracing the line evolution of a given set of line ranges for multiple files.
- Only listing changes for a specific commit range, branch, or tag.
- Following file changes through renames.
Get started by opening file history for:
- The current branch: `:DiffviewFileHistory`
- The current file: `:DiffviewFileHistory %`
For more info, see `:h :DiffviewFileHistory`.
## Usage
### `:DiffviewOpen [git rev] [options] [ -- {paths...}]`
Calling `:DiffviewOpen` with no args opens a new Diffview that compares against
the current index. You can also provide any valid git rev to view only changes
for that rev.
Examples:
- `:DiffviewOpen`
- `:DiffviewOpen HEAD~2`
- `:DiffviewOpen HEAD~4..HEAD~2`
- `:DiffviewOpen d4a7b0d`
- `:DiffviewOpen d4a7b0d^!`
- `:DiffviewOpen d4a7b0d..519b30e`
- `:DiffviewOpen origin/main...HEAD`
You can also provide additional paths to narrow down what files are shown:
- `:DiffviewOpen HEAD~2 -- lua/diffview plugin`
For information about additional `[options]`, visit the
[documentation](https://github.com/sindrets/diffview.nvim/blob/main/doc/diffview.txt).
Additional commands for convenience:
- `:DiffviewClose`: Close the current diffview. You can also use `:tabclose`.
- `:DiffviewToggleFiles`: Toggle the file panel.
- `:DiffviewFocusFiles`: Bring focus to the file panel.
- `:DiffviewRefresh`: Update stats and entries in the file list of the current
Diffview.
With a Diffview open and the default key bindings, you can cycle through changed
files with `<tab>` and `<s-tab>` (see configuration to change the key bindings).
#### Staging
You can stage individual hunks by editing any buffer that represents the index
(after running `:DiffviewOpen` with no `[git-rev]` the entries under "Changes"
will have the index buffer on the left side, and the entries under "Staged
changes" will have it on the right side). Once you write to an index buffer the
index will be updated.
### `:[range]DiffviewFileHistory [paths] [options]`
Opens a new file history view that lists all commits that affected the given
paths. This is a porcelain interface for git-log. Both `[paths]` and
`[options]` may be specified in any order, even interchangeably.
If no `[paths]` are given, defaults to the top-level of the working tree. The
top-level will be inferred from the current buffer when possible, otherwise the
cwd is used. Multiple `[paths]` may be provided and git pathspec is supported.
If `[range]` is given, the file history view will trace the line evolution of the
given range in the current file (for more info, see the `-L` flag in the docs).
Examples:
- `:DiffviewFileHistory`
- `:DiffviewFileHistory %`
- `:DiffviewFileHistory path/to/some/file.txt`
- `:DiffviewFileHistory path/to/some/directory`
- `:DiffviewFileHistory include/this and/this :!but/not/this`
- `:DiffviewFileHistory --range=origin..HEAD`
- `:DiffviewFileHistory --range=feat/example-branch`
- `:'<,'>DiffviewFileHistory`
> [!IMPORTANT]
> ### Familiarize Yourself With `:h diff-mode`
>
> This plugin assumes you're familiar with all the features already provided by
> nvim's builtin diff-mode. These features include:
>
> - Jumping between hunks (`:h jumpto-diffs`).
> - Applying the changes of a diff hunk from any of the diffed buffers
> (`:h copy-diffs`).
> - And more...
>
> Read the help page for more info.
---
<br>
> [!NOTE]
> Additionally check out [USAGE](USAGE.md) for examples of some more specific
> use-cases.
<br>
---
## Configuration
<p>
<details>
<summary style='cursor: pointer'><b>Example config with default values</b></summary>
```lua
-- Lua
local actions = require("diffview.actions")
require("diffview").setup({
diff_binaries = false, -- Show diffs for binaries
enhanced_diff_hl = false, -- See |diffview-config-enhanced_diff_hl|
git_cmd = { "git" }, -- The git executable followed by default args.
hg_cmd = { "hg" }, -- The hg executable followed by default args.
use_icons = true, -- Requires nvim-web-devicons
show_help_hints = true, -- Show hints for how to open the help panel
watch_index = true, -- Update views and index buffers when the git index changes.
icons = { -- Only applies when use_icons is true.
folder_closed = "",
folder_open = "",
},
signs = {
fold_closed = "",
fold_open = "",
done = "✓",
},
view = {
-- Configure the layout and behavior of different types of views.
-- Available layouts:
-- 'diff1_plain'
-- |'diff2_horizontal'
-- |'diff2_vertical'
-- |'diff3_horizontal'
-- |'diff3_vertical'
-- |'diff3_mixed'
-- |'diff4_mixed'
-- For more info, see |diffview-config-view.x.layout|.
default = {
-- Config for changed files, and staged files in diff views.
layout = "diff2_horizontal",
disable_diagnostics = false, -- Temporarily disable diagnostics for diff buffers while in the view.
winbar_info = false, -- See |diffview-config-view.x.winbar_info|
},
merge_tool = {
-- Config for conflicted files in diff views during a merge or rebase.
layout = "diff3_horizontal",
disable_diagnostics = true, -- Temporarily disable diagnostics for diff buffers while in the view.
winbar_info = true, -- See |diffview-config-view.x.winbar_info|
},
file_history = {
-- Config for changed files in file history views.
layout = "diff2_horizontal",
disable_diagnostics = false, -- Temporarily disable diagnostics for diff buffers while in the view.
winbar_info = false, -- See |diffview-config-view.x.winbar_info|
},
},
file_panel = {
listing_style = "tree", -- One of 'list' or 'tree'
tree_options = { -- Only applies when listing_style is 'tree'
flatten_dirs = true, -- Flatten dirs that only contain one single dir
folder_statuses = "only_folded", -- One of 'never', 'only_folded' or 'always'.
},
win_config = { -- See |diffview-config-win_config|
position = "left",
width = 35,
win_opts = {},
},
},
file_history_panel = {
log_options = { -- See |diffview-config-log_options|
git = {
single_file = {
diff_merges = "combined",
},
multi_file = {
diff_merges = "first-parent",
},
},
hg = {
single_file = {},
multi_file = {},
},
},
win_config = { -- See |diffview-config-win_config|
position = "bottom",
height = 16,
win_opts = {},
},
},
commit_log_panel = {
win_config = {}, -- See |diffview-config-win_config|
},
default_args = { -- Default args prepended to the arg-list for the listed commands
DiffviewOpen = {},
DiffviewFileHistory = {},
},
hooks = {}, -- See |diffview-config-hooks|
keymaps = {
disable_defaults = false, -- Disable the default keymaps
view = {
-- The `view` bindings are active in the diff buffers, only when the current
-- tabpage is a Diffview.
{ "n", "<tab>", actions.select_next_entry, { desc = "Open the diff for the next file" } },
{ "n", "<s-tab>", actions.select_prev_entry, { desc = "Open the diff for the previous file" } },
{ "n", "gf", actions.goto_file_edit, { desc = "Open the file in the previous tabpage" } },
{ "n", "<C-w><C-f>", actions.goto_file_split, { desc = "Open the file in a new split" } },
{ "n", "<C-w>gf", actions.goto_file_tab, { desc = "Open the file in a new tabpage" } },
{ "n", "<leader>e", actions.focus_files, { desc = "Bring focus to the file panel" } },
{ "n", "<leader>b", actions.toggle_files, { desc = "Toggle the file panel." } },
{ "n", "g<C-x>", actions.cycle_layout, { desc = "Cycle through available layouts." } },
{ "n", "[x", actions.prev_conflict, { desc = "In the merge-tool: jump to the previous conflict" } },
{ "n", "]x", actions.next_conflict, { desc = "In the merge-tool: jump to the next conflict" } },
{ "n", "<leader>co", actions.conflict_choose("ours"), { desc = "Choose the OURS version of a conflict" } },
{ "n", "<leader>ct", actions.conflict_choose("theirs"), { desc = "Choose the THEIRS version of a conflict" } },
{ "n", "<leader>cb", actions.conflict_choose("base"), { desc = "Choose the BASE version of a conflict" } },
{ "n", "<leader>ca", actions.conflict_choose("all"), { desc = "Choose all the versions of a conflict" } },
{ "n", "dx", actions.conflict_choose("none"), { desc = "Delete the conflict region" } },
{ "n", "<leader>cO", actions.conflict_choose_all("ours"), { desc = "Choose the OURS version of a conflict for the whole file" } },
{ "n", "<leader>cT", actions.conflict_choose_all("theirs"), { desc = "Choose the THEIRS version of a conflict for the whole file" } },
{ "n", "<leader>cB", actions.conflict_choose_all("base"), { desc = "Choose the BASE version of a conflict for the whole file" } },
{ "n", "<leader>cA", actions.conflict_choose_all("all"), { desc = "Choose all the versions of a conflict for the whole file" } },
{ "n", "dX", actions.conflict_choose_all("none"), { desc = "Delete the conflict region for the whole file" } },
},
diff1 = {
-- Mappings in single window diff layouts
{ "n", "g?", actions.help({ "view", "diff1" }), { desc = "Open the help panel" } },
},
diff2 = {
-- Mappings in 2-way diff layouts
{ "n", "g?", actions.help({ "view", "diff2" }), { desc = "Open the help panel" } },
},
diff3 = {
-- Mappings in 3-way diff layouts
{ { "n", "x" }, "2do", actions.diffget("ours"), { desc = "Obtain the diff hunk from the OURS version of the file" } },
{ { "n", "x" }, "3do", actions.diffget("theirs"), { desc = "Obtain the diff hunk from the THEIRS version of the file" } },
{ "n", "g?", actions.help({ "view", "diff3" }), { desc = "Open the help panel" } },
},
diff4 = {
-- Mappings in 4-way diff layouts
{ { "n", "x" }, "1do", actions.diffget("base"), { desc = "Obtain the diff hunk from the BASE version of the file" } },
{ { "n", "x" }, "2do", actions.diffget("ours"), { desc = "Obtain the diff hunk from the OURS version of the file" } },
{ { "n", "x" }, "3do", actions.diffget("theirs"), { desc = "Obtain the diff hunk from the THEIRS version of the file" } },
{ "n", "g?", actions.help({ "view", "diff4" }), { desc = "Open the help panel" } },
},
file_panel = {
{ "n", "j", actions.next_entry, { desc = "Bring the cursor to the next file entry" } },
{ "n", "<down>", actions.next_entry, { desc = "Bring the cursor to the next file entry" } },
{ "n", "k", actions.prev_entry, { desc = "Bring the cursor to the previous file entry" } },
{ "n", "<up>", actions.prev_entry, { desc = "Bring the cursor to the previous file entry" } },
{ "n", "<cr>", actions.select_entry, { desc = "Open the diff for the selected entry" } },
{ "n", "o", actions.select_entry, { desc = "Open the diff for the selected entry" } },
{ "n", "l", actions.select_entry, { desc = "Open the diff for the selected entry" } },
{ "n", "<2-LeftMouse>", actions.select_entry, { desc = "Open the diff for the selected entry" } },
{ "n", "-", actions.toggle_stage_entry, { desc = "Stage / unstage the selected entry" } },
{ "n", "s", actions.toggle_stage_entry, { desc = "Stage / unstage the selected entry" } },
{ "n", "S", actions.stage_all, { desc = "Stage all entries" } },
{ "n", "U", actions.unstage_all, { desc = "Unstage all entries" } },
{ "n", "X", actions.restore_entry, { desc = "Restore entry to the state on the left side" } },
{ "n", "L", actions.open_commit_log, { desc = "Open the commit log panel" } },
{ "n", "zo", actions.open_fold, { desc = "Expand fold" } },
{ "n", "h", actions.close_fold, { desc = "Collapse fold" } },
{ "n", "zc", actions.close_fold, { desc = "Collapse fold" } },
{ "n", "za", actions.toggle_fold, { desc = "Toggle fold" } },
{ "n", "zR", actions.open_all_folds, { desc = "Expand all folds" } },
{ "n", "zM", actions.close_all_folds, { desc = "Collapse all folds" } },
{ "n", "<c-b>", actions.scroll_view(-0.25), { desc = "Scroll the view up" } },
{ "n", "<c-f>", actions.scroll_view(0.25), { desc = "Scroll the view down" } },
{ "n", "<tab>", actions.select_next_entry, { desc = "Open the diff for the next file" } },
{ "n", "<s-tab>", actions.select_prev_entry, { desc = "Open the diff for the previous file" } },
{ "n", "gf", actions.goto_file_edit, { desc = "Open the file in the previous tabpage" } },
{ "n", "<C-w><C-f>", actions.goto_file_split, { desc = "Open the file in a new split" } },
{ "n", "<C-w>gf", actions.goto_file_tab, { desc = "Open the file in a new tabpage" } },
{ "n", "i", actions.listing_style, { desc = "Toggle between 'list' and 'tree' views" } },
{ "n", "f", actions.toggle_flatten_dirs, { desc = "Flatten empty subdirectories in tree listing style" } },
{ "n", "R", actions.refresh_files, { desc = "Update stats and entries in the file list" } },
{ "n", "<leader>e", actions.focus_files, { desc = "Bring focus to the file panel" } },
{ "n", "<leader>b", actions.toggle_files, { desc = "Toggle the file panel" } },
{ "n", "g<C-x>", actions.cycle_layout, { desc = "Cycle available layouts" } },
{ "n", "[x", actions.prev_conflict, { desc = "Go to the previous conflict" } },
{ "n", "]x", actions.next_conflict, { desc = "Go to the next conflict" } },
{ "n", "g?", actions.help("file_panel"), { desc = "Open the help panel" } },
{ "n", "<leader>cO", actions.conflict_choose_all("ours"), { desc = "Choose the OURS version of a conflict for the whole file" } },
{ "n", "<leader>cT", actions.conflict_choose_all("theirs"), { desc = "Choose the THEIRS version of a conflict for the whole file" } },
{ "n", "<leader>cB", actions.conflict_choose_all("base"), { desc = "Choose the BASE version of a conflict for the whole file" } },
{ "n", "<leader>cA", actions.conflict_choose_all("all"), { desc = "Choose all the versions of a conflict for the whole file" } },
{ "n", "dX", actions.conflict_choose_all("none"), { desc = "Delete the conflict region for the whole file" } },
},
file_history_panel = {
{ "n", "g!", actions.options, { desc = "Open the option panel" } },
{ "n", "<C-A-d>", actions.open_in_diffview, { desc = "Open the entry under the cursor in a diffview" } },
{ "n", "y", actions.copy_hash, { desc = "Copy the commit hash of the entry under the cursor" } },
{ "n", "L", actions.open_commit_log, { desc = "Show commit details" } },
{ "n", "X", actions.restore_entry, { desc = "Restore file to the state from the selected entry" } },
{ "n", "zr", actions.open_fold, { desc = "Expand fold" } },
{ "n", "zo", actions.open_fold, { desc = "Expand fold" } },
{ "n", "zm", actions.close_fold, { desc = "Collapse fold" } },
{ "n", "zc", actions.close_fold, { desc = "Collapse fold" } },
{ "n", "h", actions.close_fold, { desc = "Collapse fold" } },
{ "n", "za", actions.toggle_fold, { desc = "Toggle fold" } },
{ "n", "zR", actions.open_all_folds, { desc = "Expand all folds" } },
{ "n", "zM", actions.close_all_folds, { desc = "Collapse all folds" } },
{ "n", "j", actions.next_entry, { desc = "Bring the cursor to the next file entry" } },
{ "n", "<down>", actions.next_entry, { desc = "Bring the cursor to the next file entry" } },
{ "n", "k", actions.prev_entry, { desc = "Bring the cursor to the previous file entry" } },
{ "n", "<up>", actions.prev_entry, { desc = "Bring the cursor to the previous file entry" } },
{ "n", "<cr>", actions.select_entry, { desc = "Open the diff for the selected entry" } },
{ "n", "o", actions.select_entry, { desc = "Open the diff for the selected entry" } },
{ "n", "l", actions.select_entry, { desc = "Open the diff for the selected entry" } },
{ "n", "<2-LeftMouse>", actions.select_entry, { desc = "Open the diff for the selected entry" } },
{ "n", "<c-b>", actions.scroll_view(-0.25), { desc = "Scroll the view up" } },
{ "n", "<c-f>", actions.scroll_view(0.25), { desc = "Scroll the view down" } },
{ "n", "<tab>", actions.select_next_entry, { desc = "Open the diff for the next file" } },
{ "n", "<s-tab>", actions.select_prev_entry, { desc = "Open the diff for the previous file" } },
{ "n", "gf", actions.goto_file_edit, { desc = "Open the file in the previous tabpage" } },
{ "n", "<C-w><C-f>", actions.goto_file_split, { desc = "Open the file in a new split" } },
{ "n", "<C-w>gf", actions.goto_file_tab, { desc = "Open the file in a new tabpage" } },
{ "n", "<leader>e", actions.focus_files, { desc = "Bring focus to the file panel" } },
{ "n", "<leader>b", actions.toggle_files, { desc = "Toggle the file panel" } },
{ "n", "g<C-x>", actions.cycle_layout, { desc = "Cycle available layouts" } },
{ "n", "g?", actions.help("file_history_panel"), { desc = "Open the help panel" } },
},
option_panel = {
{ "n", "<tab>", actions.select_entry, { desc = "Change the current option" } },
{ "n", "q", actions.close, { desc = "Close the panel" } },
{ "n", "g?", actions.help("option_panel"), { desc = "Open the help panel" } },
},
help_panel = {
{ "n", "q", actions.close, { desc = "Close help menu" } },
{ "n", "<esc>", actions.close, { desc = "Close help menu" } },
},
},
})
```
</details>
</p>
### Hooks
The `hooks` table allows you to define callbacks for various events emitted from
Diffview. The available hooks are documented in detail in
`:h diffview-config-hooks`. The hook events are also available as User
autocommands. See `:h diffview-user-autocmds` for more details.
Examples:
```lua
hooks = {
diff_buf_read = function(bufnr)
-- Change local options in diff buffers
vim.opt_local.wrap = false
vim.opt_local.list = false
vim.opt_local.colorcolumn = { 80 }
end,
view_opened = function(view)
print(
("A new %s was opened on tab page %d!")
:format(view.class:name(), view.tabpage)
)
end,
}
```
### Keymaps
The keymaps config is structured as a table with sub-tables for various
different contexts where mappings can be declared. In these sub-tables
key-value pairs are treated as the `{lhs}` and `{rhs}` of a normal mode
mapping. These mappings all use the `:map-arguments` `silent`, `nowait`, and
`noremap`. The implementation uses `vim.keymap.set()`, so the `{rhs}` can be
either a vim command in the form of a string, or it can be a lua function:
```lua
view = {
-- Vim command:
["a"] = "<Cmd>echom 'foo'<CR>",
-- Lua function:
["b"] = function() print("bar") end,
}
```
For more control (i.e. mappings for other modes), you can also define index
values as list-like tables containing the arguments for `vim.keymap.set()`.
This way you can also change all the `:map-arguments` with the only exception
being the `buffer` field, as this will be overridden with the target buffer
number:
```lua
view = {
-- Normal and visual mode mapping to vim command:
{ { "n", "v" }, "<leader>a", "<Cmd>echom 'foo'<CR>", { silent = true } },
-- Visual mode mapping to lua function:
{ "v", "<leader>b", function() print("bar") end, { nowait = true } },
}
```
To disable any single mapping without disabling them all, set its `{rhs}` to
`false`:
```lua
view = {
-- Disable the default normal mode mapping for `<tab>`:
["<tab>"] = false,
-- Disable the default visual mode mapping for `gf`:
{ "x", "gf", false },
}
```
Most of the mapped file panel actions also work from the view if they are added
to the view maps (and vice versa). The exception is for actions that only
really make sense specifically in the file panel, such as `next_entry`,
`prev_entry`. Actions such as `toggle_stage_entry` and `restore_entry` work
just fine from the view. When invoked from the view, these will target the file
currently open in the view rather than the file under the cursor in the file
panel.
**For more details on how to set mappings for other modes, actions, and more see:**
- `:h diffview-config-keymaps`
- `:h diffview-actions`
## Restoring Files
If the right side of the diff is showing the local state of a file, you can
restore the file to the state from the left side of the diff (key binding `X`
from the file panel by default). The current state of the file is stored in the
git object database, and a command is echoed that shows how to undo the change.
## Tips and FAQ
- **Hide untracked files:**
- `DiffviewOpen -uno`
- **Exclude certain paths:**
- `DiffviewOpen -- :!exclude/this :!and/this`
- **Run as if git was started in a specific directory:**
- `DiffviewOpen -C/foo/bar/baz`
- **Diff the index against a git rev:**
- `DiffviewOpen HEAD~2 --cached`
- Defaults to `HEAD` if no rev is given.
- **Q: How do I get the diagonal lines in place of deleted lines in
diff-mode?**
- A: Change your `:h 'fillchars'`:
- (vimscript): `set fillchars+=diff:`
- (Lua): `vim.opt.fillchars:append { diff = "" }`
- Note: whether or not the diagonal lines will line up nicely will depend on
your terminal emulator. The terminal used in the screenshots is Kitty.
- **Q: How do I jump between hunks in the diff?**
- A: Use `[c` and `]c`
- `:h jumpto-diffs`
<!-- vim: set tw=80 -->

View File

@ -0,0 +1,287 @@
# Usage
This document contains a few guides for solving common problems, and describes
some more specific use-cases.
## Review a PR
### Comparing All the Changes
First, checkout the branch locally. If it's a GitHub PR, you can use
[`gh`](https://github.com/cli/cli) to do this:
```console
$ gh pr checkout {PR_ID}
```
Now, run a symmetric diff against the base branch:
```vim
:DiffviewOpen origin/HEAD...HEAD --imply-local
```
The symmetric difference rev range (triple dot) will here compare the changes on
the current branch (the PR branch) against its merge-base in `origin/HEAD`. This
is different from comparing directly against `origin/HEAD` if the branches have
diverged, and is usually what you want when comparing changes in a PR. For more
info see the section on "SPECIFYING REVISIONS" in `man git-rev-parse(1)`.
The `--imply-local` flag option will here make diffview.nvim show the working
tree versions[^1] of the changed files on the right side of the diff. This means
that if you have tools such as LSP set up, it will work for all the diff buffers
on the right side, giving you access to LSP features - such as diagnostics and
references - that can be useful while reviewing changes.
If you want the plugin to always use this option, you can add it to your default
args:
```lua
default_args = {
DiffviewOpen = { "--imply-local" },
}
```
From the file panel you can press `L` to open the commit log for all the
changes. This lets you check the full commit messages for all the commits
involved.
![diffview symdiff demo](https://user-images.githubusercontent.com/2786478/229858634-c751ebe3-cc43-48de-adda-bf0b71fa2ce7.png)
> NOTE: If Git complains `fatal: Not a valid object name origin/HEAD` then it
> likely means that your local origin remote doesn't have a default branch
> configured. You can have Git automatically query your remote and detect its
> default branch like this:
>
> ```sh
> git remote set-head -a origin
> ```
[^1]: The files as they currently exist on disk.
### Comparing Changes From the Individual PR Commits
If you're reviewing a big PR composed of many commits, you might prefer to
review the changes introduced in each of those commits individually. To do
this, you can use `:DiffviewFileHistory`:
```vim
:DiffviewFileHistory --range=origin/HEAD...HEAD --right-only --no-merges
```
Here we are again using a symmetric difference range. However, symdiff ranges
have different behavior between `git-diff` and `git-log`. Whereas in `git-diff`
it compares against the merge-base, here it will select only the commits that
are reachable from _either_ `origin/HEAD` _or_ `HEAD`, but not from both (in
other words, it's actually performing a symmetric difference here).
We then use the cherry-pick option `--right-only` to limit the commits to only
those on the right side of the symmetric difference. Finally `--no-merges`
filters out merge commits. We are left with a list of all the non-merge commits
from the PR branch.
![file history cherry pick demo](https://user-images.githubusercontent.com/2786478/229853402-f45280ee-f6e2-4325-8a39-ce25b9c5221e.png)
## Inspecting Diffs for Stashes
The latest Git stash is always stored in the reference `refs/stash`. We can
find all the stashes by traversing the reflog for this reference. This can be
achieved with the flag option `--walk-reflogs` (or it's short form `-g`). The
following command will list all stashes in the file history panel:
```vim
:DiffviewFileHistory -g --range=stash
```
## Committing
Creating commits from within nvim is a solved problem, and as such diffview.nvim
does not reinvent the wheel here. Here are a few different ways in which you can
create a new commit from within the editor:
### Use a Git Wrapper Plugin (Recommended)
Diffview.nvim _is not_, and _does not try to be_ a complete git wrapper. As
such, there are a number of features offered by such plugins that won't ever be
implemented here, because they are deemed out-of-scope. It's therefore
recommended to use some form of a Git wrapper plugin in order to get a more
complete integration of Git's features into your editor. Here are a few options:
- [`vim-fugitive`](https://github.com/tpope/vim-fugitive)
- [`neogit`](https://github.com/TimUntersberger/neogit)
- [`lazygit.nvim`](https://github.com/kdheepak/lazygit.nvim)
Example creating some `vim-fugitive` mappings for committing staged changes
from the file panel:
```lua
keymaps = {
file_panel = {
{
"n", "cc",
"<Cmd>Git commit <bar> wincmd J<CR>",
{ desc = "Commit staged changes" },
},
{
"n", "ca",
"<Cmd>Git commit --amend <bar> wincmd J<CR>",
{ desc = "Amend the last commit" },
},
{
"n", "c<space>",
":Git commit ",
{ desc = "Populate command line with \":Git commit \"" },
},
},
}
```
### Use [`neovim-remote`](https://github.com/mhinz/neovim-remote)
`neovim-remote` is a more complete version of the builtin `:h clientserver`.
Notably it implements all the `:h clientserver-missing` functionality. Hopefully
this functionality will be implemented in core at some point in the future. But
until then this separate application is needed to get the `--{...}-wait`
variants of the remote options.
With the remote installed you can simply configure your `$GIT_EDITOR`
environment variable from within the editor such that when the Git editor is
invoked, it will open in a new split inside the current editor session. This
avoids the problem of spawning nested nvim instances every time an `$EDITOR` is
invoked. Put this somewhere in your `init.lua`:
```lua
if vim.fn.executable("nvr") == 1 then
local nvr = "nvr --servername " .. vim.v.servername .. " "
vim.env.GIT_EDITOR = nvr .. "-cc split +'setl bh=delete' --remote-wait"
vim.env.EDITOR = nvr .. "-l --remote" -- (Optional)
vim.env.VISUAL = nvr .. "-l --remote" -- (Optional)
end
```
Example creating some mappings for committing staged changes from the file
panel, that will trigger `nvr`:
```lua
keymaps = {
file_panel = {
{
"n", "cc",
[[<Cmd>call jobstart(["git", "commit"]) | au BufWinEnter * ++once wincmd J<CR>]],
{ desc = "Commit staged changes" },
},
{
"n", "ca",
[[<Cmd>call jobstart(["git", "commit", "--amend"]) | au BufWinEnter * ++once wincmd J<CR>]],
{ desc = "Amend the last commit" },
},
},
}
```
### Use `:terminal`
The `:h :terminal` command allows you to run interactive terminal jobs. However,
unlike the [previously discussed](#use-neovim-remote) `neovim-remote` solution
this will spawn nested instances of nvim.
Example creating some `:terminal` mappings for committing staged changes from
the file panel:
```lua
keymaps = {
file_panel = {
{
"n", "cc",
"<Cmd>sp <bar> wincmd J <bar> term git commit<CR>",
{ desc = "Commit staged changes" },
},
{
"n", "ca",
"<Cmd>sp <bar> wincmd J <bar> term git commit -amend<CR>",
{ desc = "Amend the last commit" },
},
},
}
```
### Use `vim.ui.input()`
This example shows how to use the Neovim builtin `vim.ui.input()` to create a
simple input prompt, and create a new commit with the user's given message.
![vim.ui.input() demo](https://user-images.githubusercontent.com/2786478/274230878-661a30d5-cc1d-4a34-b196-c4adf9e9f8c5.gif)
For Neovim ≥ v0.10:
```lua
keymaps = {
file_panel = {
{
"n", "cc",
function()
vim.ui.input({ prompt = "Commit message: " }, function(msg)
if not msg then return end
local results = vim.system({ "git", "commit", "-m", msg }, { text = true }):wait()
if results.code ~= 0 then
vim.notify(
"Commit failed with the message: \n"
.. vim.trim(results.stdout .. "\n" .. results.stderr),
vim.log.levels.ERROR,
{ title = "Commit" }
)
else
vim.notify(results.stdout, vim.log.levels.INFO, { title = "Commit" })
end
end)
end,
},
},
}
```
<details>
<summary><b>For Neovim ≤ v0.10, use this function in place of <code>vim.system()</code></b></summary>
```lua
--- @class system.Results
--- @field code integer
--- @field stdout string
--- @field stderr string
--- @param cmd string|string[]
--- @return system.Results
local function system(cmd)
local results = {}
local function callback(_, data, event)
if event == "exit" then results.code = data
elseif event == "stdout" or event == "stderr" then
results[event] = table.concat(data, "\n")
end
end
vim.fn.jobwait({
vim.fn.jobstart(cmd, {
on_exit = callback,
on_stdout = callback,
on_stderr = callback,
stdout_buffered = true,
stderr_buffered = true,
})
})
return results
end
```
</details>
### Use `:!cmd`
If you only ever write simple commit messages you could make use of `:h !cmd`:
```vim
:!git commit -m 'some commit message'
```
<!-- vim: set tw=80 -->

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,217 @@
================================================================================
*diffview.changelog*
CHANGELOG
NOTE: This changelog only encompasses breaking changes.
*diffview.changelog-271*
PR: https://github.com/sindrets/diffview.nvim/pull/271
The config for log options has changed. In preparation of adding support of
other VCS, the table is now divided into sub-tables per VCS type. This allows
you to define different default log options for different VCS tools. To update
your config, just move all your current log options into the new table key
`git`:
Before: ~
>
require("diffview").setup({
-- ...
file_history_panel = {
log_options = {
single_file = {
max_count = 512,
follow = true,
},
multi_file = {
max_count = 128,
},
},
},
})
<
After: ~
>
require("diffview").setup({
-- ...
file_history_panel = {
log_options = {
git = {
single_file = {
max_count = 512,
follow = true,
},
multi_file = {
max_count = 128,
},
},
},
},
})
<
*diffview.changelog-190*
PR: https://github.com/sindrets/diffview.nvim/pull/190
This PR involves a major refactor of the layout system. The changes are made
in preparation of the planned merge-tool, which is going to involve 3-way
diffs, and possibly also 4-way diffs. Different entries in the same view may
now use completely different window layouts. Thus the action `view_windo` has
changed to reflect these changes. See |diffview-actions-view_windo| for more
details on the new usage.
*diffview.changelog-169*
PR: https://github.com/sindrets/diffview.nvim/pull/169
The file history option panel is now able to accept multiple values separated
by whitespace. This means that if you want to specify values with whitespace,
you need to quote the value, or escape the whitespace with a backslash (`\`).
*diffview.changelog-151*
PR: https://github.com/sindrets/diffview.nvim/pull/151
The config for log options has changed. The table is now divided into the
sub-tables `single_file`, and `multi_file`. This allows you to define
different default log options for history targeting singular files, and
history targeting multiple paths, and/or directories. To update your config,
just move all your log options into the new table keys `single_file` and
`multi_file`:
Before: ~
>
require("diffview").setup({
-- ...
file_history_panel = {
log_options = {
max_count = 512,
follow = true,
},
},
})
<
After: ~
>
require("diffview").setup({
-- ...
file_history_panel = {
log_options = {
single_file = {
max_count = 512,
follow = true,
},
multi_file = {
max_count = 128,
-- follow = false -- `follow` only applies to single-file history
},
},
},
})
<
You only need to define the options you want to change from the defaults. To
find all the available log options, see |diffview.git.LogOptions|.
Calling `:DiffviewFileHistory` with no args would previously target the file
in the current buffer. This has now been changed to instead target the
top-level of the working tree. This was changed because with how it worked
before, there was effectively no way to get the file history equivalent of
running `git log` with no path args. If your cwd was some subdirectory of the
working tree, and you wanted the full file history of the tree, you would have
to manually type out the path to the top-level. On the contrary, getting the
history for the current file is always as simple as just using `%`, which
expands to the current file name.
To get the file history for the current file like before, simply run: >
:DiffviewFileHistory %
<
*diffview.changelog-137*
PR: https://github.com/sindrets/diffview.nvim/pull/137
The minimum required version has been bumped to Neovim 0.7.0, as the plugin
now uses some of the API functions provided in this release.
*diffview.changelog-136*
PR: https://github.com/sindrets/diffview.nvim/pull/136
This PR refactors the internal representation of a panel (the various
interactive windows used in the plugin). The way panels are configured has
been changed and extended in a manner that is incompatible with the way it was
done before. To update your config, just move all the window related options
into a new table key `win_config`:
Before: ~
>
require("diffview").setup({
-- ...
file_panel = {
position = "left",
width = 35,
height = 16,
-- (Other options...)
},
})
<
After: ~
>
require("diffview").setup({
-- ...
file_panel = {
win_config = {
position = "left",
width = 35,
height = 16,
},
-- (Other options...)
},
})
<
This goes for both the `file_panel` and the `file_history_panel` config. To
see all the available options for `win_config`, see
|diffview-config-win_config|.
*diffview.changelog-93*
PR: https://github.com/sindrets/diffview.nvim/pull/93
The plugin will from here on out require `plenary.nvim`:
https://github.com/nvim-lua/plenary.nvim
I'm using plenary for it's async utilities as well as job management. To
update, just make sure plenary is loaded before diffview. Examples:
Packer:~
`use { 'sindrets/diffview.nvim', requires = 'nvim-lua/plenary.nvim' }`
Plug:~
`Plug 'nvim-lua/plenary.nvim'`
`Plug 'sindrets/diffview.nvim'`
*diffview.changelog-64*
PR: https://github.com/sindrets/diffview.nvim/pull/64
This PR introduces some small breaking changes in the config, and for plugins
integrating diffview.nvim.
The `use_icons` config table key has been moved out of the `file_panel` table.
This has been done because `use_icons` now applies to other contexts than just
the file panel. The correct way to configure this now is to set `use_icons`
somewhere from the top level of the config table.
For plugins integrating diffview.nvim:
Several of the git utilities have been refactored into their own namespace
(`lua/diffview/git/`). I (STS) felt this was necessary due to the growing
scope of the plugin. Most notably this means that the `Rev` class now resides
in `lua/diffview/git/rev.lua`.
vim:tw=78:ts=8:ft=help:norl:

View File

@ -0,0 +1,230 @@
DEFAULT CONFIG *diffview.defaults*
>lua
local actions = require("diffview.actions")
require("diffview").setup({
diff_binaries = false, -- Show diffs for binaries
enhanced_diff_hl = false, -- See |diffview-config-enhanced_diff_hl|
git_cmd = { "git" }, -- The git executable followed by default args.
hg_cmd = { "hg" }, -- The hg executable followed by default args.
use_icons = true, -- Requires nvim-web-devicons
show_help_hints = true, -- Show hints for how to open the help panel
watch_index = true, -- Update views and index buffers when the git index changes.
icons = { -- Only applies when use_icons is true.
folder_closed = "",
folder_open = "",
},
signs = {
fold_closed = "",
fold_open = "",
done = "✓",
},
view = {
-- Configure the layout and behavior of different types of views.
-- Available layouts:
-- 'diff1_plain'
-- |'diff2_horizontal'
-- |'diff2_vertical'
-- |'diff3_horizontal'
-- |'diff3_vertical'
-- |'diff3_mixed'
-- |'diff4_mixed'
-- For more info, see |diffview-config-view.x.layout|.
default = {
-- Config for changed files, and staged files in diff views.
layout = "diff2_horizontal",
disable_diagnostics = false, -- Temporarily disable diagnostics for diff buffers while in the view.
winbar_info = false, -- See |diffview-config-view.x.winbar_info|
},
merge_tool = {
-- Config for conflicted files in diff views during a merge or rebase.
layout = "diff3_horizontal",
disable_diagnostics = true, -- Temporarily disable diagnostics for diff buffers while in the view.
winbar_info = true, -- See |diffview-config-view.x.winbar_info|
},
file_history = {
-- Config for changed files in file history views.
layout = "diff2_horizontal",
disable_diagnostics = false, -- Temporarily disable diagnostics for diff buffers while in the view.
winbar_info = false, -- See |diffview-config-view.x.winbar_info|
},
},
file_panel = {
listing_style = "tree", -- One of 'list' or 'tree'
tree_options = { -- Only applies when listing_style is 'tree'
flatten_dirs = true, -- Flatten dirs that only contain one single dir
folder_statuses = "only_folded", -- One of 'never', 'only_folded' or 'always'.
},
win_config = { -- See |diffview-config-win_config|
position = "left",
width = 35,
win_opts = {},
},
},
file_history_panel = {
log_options = { -- See |diffview-config-log_options|
git = {
single_file = {
diff_merges = "combined",
},
multi_file = {
diff_merges = "first-parent",
},
},
hg = {
single_file = {},
multi_file = {},
},
},
win_config = { -- See |diffview-config-win_config|
position = "bottom",
height = 16,
win_opts = {},
},
},
commit_log_panel = {
win_config = {}, -- See |diffview-config-win_config|
},
default_args = { -- Default args prepended to the arg-list for the listed commands
DiffviewOpen = {},
DiffviewFileHistory = {},
},
hooks = {}, -- See |diffview-config-hooks|
keymaps = {
disable_defaults = false, -- Disable the default keymaps
view = {
-- The `view` bindings are active in the diff buffers, only when the current
-- tabpage is a Diffview.
{ "n", "<tab>", actions.select_next_entry, { desc = "Open the diff for the next file" } },
{ "n", "<s-tab>", actions.select_prev_entry, { desc = "Open the diff for the previous file" } },
{ "n", "gf", actions.goto_file_edit, { desc = "Open the file in the previous tabpage" } },
{ "n", "<C-w><C-f>", actions.goto_file_split, { desc = "Open the file in a new split" } },
{ "n", "<C-w>gf", actions.goto_file_tab, { desc = "Open the file in a new tabpage" } },
{ "n", "<leader>e", actions.focus_files, { desc = "Bring focus to the file panel" } },
{ "n", "<leader>b", actions.toggle_files, { desc = "Toggle the file panel." } },
{ "n", "g<C-x>", actions.cycle_layout, { desc = "Cycle through available layouts." } },
{ "n", "[x", actions.prev_conflict, { desc = "In the merge-tool: jump to the previous conflict" } },
{ "n", "]x", actions.next_conflict, { desc = "In the merge-tool: jump to the next conflict" } },
{ "n", "<leader>co", actions.conflict_choose("ours"), { desc = "Choose the OURS version of a conflict" } },
{ "n", "<leader>ct", actions.conflict_choose("theirs"), { desc = "Choose the THEIRS version of a conflict" } },
{ "n", "<leader>cb", actions.conflict_choose("base"), { desc = "Choose the BASE version of a conflict" } },
{ "n", "<leader>ca", actions.conflict_choose("all"), { desc = "Choose all the versions of a conflict" } },
{ "n", "dx", actions.conflict_choose("none"), { desc = "Delete the conflict region" } },
{ "n", "<leader>cO", actions.conflict_choose_all("ours"), { desc = "Choose the OURS version of a conflict for the whole file" } },
{ "n", "<leader>cT", actions.conflict_choose_all("theirs"), { desc = "Choose the THEIRS version of a conflict for the whole file" } },
{ "n", "<leader>cB", actions.conflict_choose_all("base"), { desc = "Choose the BASE version of a conflict for the whole file" } },
{ "n", "<leader>cA", actions.conflict_choose_all("all"), { desc = "Choose all the versions of a conflict for the whole file" } },
{ "n", "dX", actions.conflict_choose_all("none"), { desc = "Delete the conflict region for the whole file" } },
},
diff1 = {
-- Mappings in single window diff layouts
{ "n", "g?", actions.help({ "view", "diff1" }), { desc = "Open the help panel" } },
},
diff2 = {
-- Mappings in 2-way diff layouts
{ "n", "g?", actions.help({ "view", "diff2" }), { desc = "Open the help panel" } },
},
diff3 = {
-- Mappings in 3-way diff layouts
{ { "n", "x" }, "2do", actions.diffget("ours"), { desc = "Obtain the diff hunk from the OURS version of the file" } },
{ { "n", "x" }, "3do", actions.diffget("theirs"), { desc = "Obtain the diff hunk from the THEIRS version of the file" } },
{ "n", "g?", actions.help({ "view", "diff3" }), { desc = "Open the help panel" } },
},
diff4 = {
-- Mappings in 4-way diff layouts
{ { "n", "x" }, "1do", actions.diffget("base"), { desc = "Obtain the diff hunk from the BASE version of the file" } },
{ { "n", "x" }, "2do", actions.diffget("ours"), { desc = "Obtain the diff hunk from the OURS version of the file" } },
{ { "n", "x" }, "3do", actions.diffget("theirs"), { desc = "Obtain the diff hunk from the THEIRS version of the file" } },
{ "n", "g?", actions.help({ "view", "diff4" }), { desc = "Open the help panel" } },
},
file_panel = {
{ "n", "j", actions.next_entry, { desc = "Bring the cursor to the next file entry" } },
{ "n", "<down>", actions.next_entry, { desc = "Bring the cursor to the next file entry" } },
{ "n", "k", actions.prev_entry, { desc = "Bring the cursor to the previous file entry" } },
{ "n", "<up>", actions.prev_entry, { desc = "Bring the cursor to the previous file entry" } },
{ "n", "<cr>", actions.select_entry, { desc = "Open the diff for the selected entry" } },
{ "n", "o", actions.select_entry, { desc = "Open the diff for the selected entry" } },
{ "n", "l", actions.select_entry, { desc = "Open the diff for the selected entry" } },
{ "n", "<2-LeftMouse>", actions.select_entry, { desc = "Open the diff for the selected entry" } },
{ "n", "-", actions.toggle_stage_entry, { desc = "Stage / unstage the selected entry" } },
{ "n", "s", actions.toggle_stage_entry, { desc = "Stage / unstage the selected entry" } },
{ "n", "S", actions.stage_all, { desc = "Stage all entries" } },
{ "n", "U", actions.unstage_all, { desc = "Unstage all entries" } },
{ "n", "X", actions.restore_entry, { desc = "Restore entry to the state on the left side" } },
{ "n", "L", actions.open_commit_log, { desc = "Open the commit log panel" } },
{ "n", "zo", actions.open_fold, { desc = "Expand fold" } },
{ "n", "h", actions.close_fold, { desc = "Collapse fold" } },
{ "n", "zc", actions.close_fold, { desc = "Collapse fold" } },
{ "n", "za", actions.toggle_fold, { desc = "Toggle fold" } },
{ "n", "zR", actions.open_all_folds, { desc = "Expand all folds" } },
{ "n", "zM", actions.close_all_folds, { desc = "Collapse all folds" } },
{ "n", "<c-b>", actions.scroll_view(-0.25), { desc = "Scroll the view up" } },
{ "n", "<c-f>", actions.scroll_view(0.25), { desc = "Scroll the view down" } },
{ "n", "<tab>", actions.select_next_entry, { desc = "Open the diff for the next file" } },
{ "n", "<s-tab>", actions.select_prev_entry, { desc = "Open the diff for the previous file" } },
{ "n", "gf", actions.goto_file_edit, { desc = "Open the file in the previous tabpage" } },
{ "n", "<C-w><C-f>", actions.goto_file_split, { desc = "Open the file in a new split" } },
{ "n", "<C-w>gf", actions.goto_file_tab, { desc = "Open the file in a new tabpage" } },
{ "n", "i", actions.listing_style, { desc = "Toggle between 'list' and 'tree' views" } },
{ "n", "f", actions.toggle_flatten_dirs, { desc = "Flatten empty subdirectories in tree listing style" } },
{ "n", "R", actions.refresh_files, { desc = "Update stats and entries in the file list" } },
{ "n", "<leader>e", actions.focus_files, { desc = "Bring focus to the file panel" } },
{ "n", "<leader>b", actions.toggle_files, { desc = "Toggle the file panel" } },
{ "n", "g<C-x>", actions.cycle_layout, { desc = "Cycle available layouts" } },
{ "n", "[x", actions.prev_conflict, { desc = "Go to the previous conflict" } },
{ "n", "]x", actions.next_conflict, { desc = "Go to the next conflict" } },
{ "n", "g?", actions.help("file_panel"), { desc = "Open the help panel" } },
{ "n", "<leader>cO", actions.conflict_choose_all("ours"), { desc = "Choose the OURS version of a conflict for the whole file" } },
{ "n", "<leader>cT", actions.conflict_choose_all("theirs"), { desc = "Choose the THEIRS version of a conflict for the whole file" } },
{ "n", "<leader>cB", actions.conflict_choose_all("base"), { desc = "Choose the BASE version of a conflict for the whole file" } },
{ "n", "<leader>cA", actions.conflict_choose_all("all"), { desc = "Choose all the versions of a conflict for the whole file" } },
{ "n", "dX", actions.conflict_choose_all("none"), { desc = "Delete the conflict region for the whole file" } },
},
file_history_panel = {
{ "n", "g!", actions.options, { desc = "Open the option panel" } },
{ "n", "<C-A-d>", actions.open_in_diffview, { desc = "Open the entry under the cursor in a diffview" } },
{ "n", "y", actions.copy_hash, { desc = "Copy the commit hash of the entry under the cursor" } },
{ "n", "L", actions.open_commit_log, { desc = "Show commit details" } },
{ "n", "X", actions.restore_entry, { desc = "Restore file to the state from the selected entry" } },
{ "n", "zr", actions.open_fold, { desc = "Expand fold" } },
{ "n", "zo", actions.open_fold, { desc = "Expand fold" } },
{ "n", "zm", actions.close_fold, { desc = "Collapse fold" } },
{ "n", "zc", actions.close_fold, { desc = "Collapse fold" } },
{ "n", "h", actions.close_fold, { desc = "Collapse fold" } },
{ "n", "za", actions.toggle_fold, { desc = "Toggle fold" } },
{ "n", "zR", actions.open_all_folds, { desc = "Expand all folds" } },
{ "n", "zM", actions.close_all_folds, { desc = "Collapse all folds" } },
{ "n", "j", actions.next_entry, { desc = "Bring the cursor to the next file entry" } },
{ "n", "<down>", actions.next_entry, { desc = "Bring the cursor to the next file entry" } },
{ "n", "k", actions.prev_entry, { desc = "Bring the cursor to the previous file entry" } },
{ "n", "<up>", actions.prev_entry, { desc = "Bring the cursor to the previous file entry" } },
{ "n", "<cr>", actions.select_entry, { desc = "Open the diff for the selected entry" } },
{ "n", "o", actions.select_entry, { desc = "Open the diff for the selected entry" } },
{ "n", "l", actions.select_entry, { desc = "Open the diff for the selected entry" } },
{ "n", "<2-LeftMouse>", actions.select_entry, { desc = "Open the diff for the selected entry" } },
{ "n", "<c-b>", actions.scroll_view(-0.25), { desc = "Scroll the view up" } },
{ "n", "<c-f>", actions.scroll_view(0.25), { desc = "Scroll the view down" } },
{ "n", "<tab>", actions.select_next_entry, { desc = "Open the diff for the next file" } },
{ "n", "<s-tab>", actions.select_prev_entry, { desc = "Open the diff for the previous file" } },
{ "n", "gf", actions.goto_file_edit, { desc = "Open the file in the previous tabpage" } },
{ "n", "<C-w><C-f>", actions.goto_file_split, { desc = "Open the file in a new split" } },
{ "n", "<C-w>gf", actions.goto_file_tab, { desc = "Open the file in a new tabpage" } },
{ "n", "<leader>e", actions.focus_files, { desc = "Bring focus to the file panel" } },
{ "n", "<leader>b", actions.toggle_files, { desc = "Toggle the file panel" } },
{ "n", "g<C-x>", actions.cycle_layout, { desc = "Cycle available layouts" } },
{ "n", "g?", actions.help("file_history_panel"), { desc = "Open the help panel" } },
},
option_panel = {
{ "n", "<tab>", actions.select_entry, { desc = "Change the current option" } },
{ "n", "q", actions.close, { desc = "Close the panel" } },
{ "n", "g?", actions.help("option_panel"), { desc = "Open the help panel" } },
},
help_panel = {
{ "n", "q", actions.close, { desc = "Close help menu" } },
{ "n", "<esc>", actions.close, { desc = "Close help menu" } },
},
},
})
<
vim:ft=help:norl:

View File

@ -0,0 +1,114 @@
:DiffviewClose diffview.txt /*:DiffviewClose*
:DiffviewFileHistory diffview.txt /*:DiffviewFileHistory*
:DiffviewFocusFiles diffview.txt /*:DiffviewFocusFiles*
:DiffviewLog diffview.txt /*:DiffviewLog*
:DiffviewOpen diffview.txt /*:DiffviewOpen*
:DiffviewRefresh diffview.txt /*:DiffviewRefresh*
:DiffviewToggleFiles diffview.txt /*:DiffviewToggleFiles*
diffview diffview.txt /*diffview*
diffview-actions diffview.txt /*diffview-actions*
diffview-actions-close diffview.txt /*diffview-actions-close*
diffview-actions-close_all_folds diffview.txt /*diffview-actions-close_all_folds*
diffview-actions-close_fold diffview.txt /*diffview-actions-close_fold*
diffview-actions-conflict_choose diffview.txt /*diffview-actions-conflict_choose*
diffview-actions-conflict_choose_all diffview.txt /*diffview-actions-conflict_choose_all*
diffview-actions-copy_hash diffview.txt /*diffview-actions-copy_hash*
diffview-actions-diffget diffview.txt /*diffview-actions-diffget*
diffview-actions-focus_entry diffview.txt /*diffview-actions-focus_entry*
diffview-actions-focus_files diffview.txt /*diffview-actions-focus_files*
diffview-actions-goto_file diffview.txt /*diffview-actions-goto_file*
diffview-actions-goto_file_edit diffview.txt /*diffview-actions-goto_file_edit*
diffview-actions-goto_file_split diffview.txt /*diffview-actions-goto_file_split*
diffview-actions-goto_file_tab diffview.txt /*diffview-actions-goto_file_tab*
diffview-actions-jumpto_conflict diffview.txt /*diffview-actions-jumpto_conflict*
diffview-actions-listing_style diffview.txt /*diffview-actions-listing_style*
diffview-actions-next_conflict diffview.txt /*diffview-actions-next_conflict*
diffview-actions-next_entry diffview.txt /*diffview-actions-next_entry*
diffview-actions-open_all_folds diffview.txt /*diffview-actions-open_all_folds*
diffview-actions-open_commit_log diffview.txt /*diffview-actions-open_commit_log*
diffview-actions-open_fold diffview.txt /*diffview-actions-open_fold*
diffview-actions-open_in_diffview diffview.txt /*diffview-actions-open_in_diffview*
diffview-actions-options diffview.txt /*diffview-actions-options*
diffview-actions-prev_conflict diffview.txt /*diffview-actions-prev_conflict*
diffview-actions-prev_entry diffview.txt /*diffview-actions-prev_entry*
diffview-actions-refresh_files diffview.txt /*diffview-actions-refresh_files*
diffview-actions-restore_entry diffview.txt /*diffview-actions-restore_entry*
diffview-actions-scroll_view diffview.txt /*diffview-actions-scroll_view*
diffview-actions-select_entry diffview.txt /*diffview-actions-select_entry*
diffview-actions-select_next_entry diffview.txt /*diffview-actions-select_next_entry*
diffview-actions-select_prev_entry diffview.txt /*diffview-actions-select_prev_entry*
diffview-actions-stage_all diffview.txt /*diffview-actions-stage_all*
diffview-actions-toggle_files diffview.txt /*diffview-actions-toggle_files*
diffview-actions-toggle_flatten_dirs diffview.txt /*diffview-actions-toggle_flatten_dirs*
diffview-actions-toggle_fold diffview.txt /*diffview-actions-toggle_fold*
diffview-actions-toggle_stage_entry diffview.txt /*diffview-actions-toggle_stage_entry*
diffview-actions-unstage_all diffview.txt /*diffview-actions-unstage_all*
diffview-actions-view_windo diffview.txt /*diffview-actions-view_windo*
diffview-api diffview.txt /*diffview-api*
diffview-available-actions diffview.txt /*diffview-available-actions*
diffview-commands diffview.txt /*diffview-commands*
diffview-config diffview.txt /*diffview-config*
diffview-config-default_args diffview.txt /*diffview-config-default_args*
diffview-config-enhanced_diff_hl diffview.txt /*diffview-config-enhanced_diff_hl*
diffview-config-git_cmd diffview.txt /*diffview-config-git_cmd*
diffview-config-hg_cmd diffview.txt /*diffview-config-hg_cmd*
diffview-config-hooks diffview.txt /*diffview-config-hooks*
diffview-config-keymaps diffview.txt /*diffview-config-keymaps*
diffview-config-log_options diffview.txt /*diffview-config-log_options*
diffview-config-view.x.disable_diagnostics diffview.txt /*diffview-config-view.x.disable_diagnostics*
diffview-config-view.x.layout diffview.txt /*diffview-config-view.x.layout*
diffview-config-view.x.winbar_info diffview.txt /*diffview-config-view.x.winbar_info*
diffview-config-win_config diffview.txt /*diffview-config-win_config*
diffview-conflict-example diffview.txt /*diffview-conflict-example*
diffview-conflict-versions diffview.txt /*diffview-conflict-versions*
diffview-file-inference diffview.txt /*diffview-file-inference*
diffview-inspect-stash diffview.txt /*diffview-inspect-stash*
diffview-layouts diffview.txt /*diffview-layouts*
diffview-maps diffview.txt /*diffview-maps*
diffview-maps-close_all_folds diffview.txt /*diffview-maps-close_all_folds*
diffview-maps-copy_hash diffview.txt /*diffview-maps-copy_hash*
diffview-maps-file-history-option-panel diffview.txt /*diffview-maps-file-history-option-panel*
diffview-maps-file-history-panel diffview.txt /*diffview-maps-file-history-panel*
diffview-maps-file-panel diffview.txt /*diffview-maps-file-panel*
diffview-maps-focus_files diffview.txt /*diffview-maps-focus_files*
diffview-maps-goto_file_edit diffview.txt /*diffview-maps-goto_file_edit*
diffview-maps-goto_file_split diffview.txt /*diffview-maps-goto_file_split*
diffview-maps-goto_file_tab diffview.txt /*diffview-maps-goto_file_tab*
diffview-maps-next_entry diffview.txt /*diffview-maps-next_entry*
diffview-maps-open_all_folds diffview.txt /*diffview-maps-open_all_folds*
diffview-maps-open_in_diffview diffview.txt /*diffview-maps-open_in_diffview*
diffview-maps-options diffview.txt /*diffview-maps-options*
diffview-maps-prev_entry diffview.txt /*diffview-maps-prev_entry*
diffview-maps-refresh_files diffview.txt /*diffview-maps-refresh_files*
diffview-maps-restore_entry diffview.txt /*diffview-maps-restore_entry*
diffview-maps-select_entry diffview.txt /*diffview-maps-select_entry*
diffview-maps-select_next_entry diffview.txt /*diffview-maps-select_next_entry*
diffview-maps-select_prev_entry diffview.txt /*diffview-maps-select_prev_entry*
diffview-maps-stage_all diffview.txt /*diffview-maps-stage_all*
diffview-maps-toggle_files diffview.txt /*diffview-maps-toggle_files*
diffview-maps-toggle_stage_entry diffview.txt /*diffview-maps-toggle_stage_entry*
diffview-maps-unstage_all diffview.txt /*diffview-maps-unstage_all*
diffview-maps-view diffview.txt /*diffview-maps-view*
diffview-merge-tool diffview.txt /*diffview-merge-tool*
diffview-staging diffview.txt /*diffview-staging*
diffview-unused-actions diffview.txt /*diffview-unused-actions*
diffview-usage diffview.txt /*diffview-usage*
diffview-user-autocmds diffview.txt /*diffview-user-autocmds*
diffview.ConflictCount diffview.txt /*diffview.ConflictCount*
diffview.api.views.diff.diff_view.CDiffView diffview.txt /*diffview.api.views.diff.diff_view.CDiffView*
diffview.api.views.diff.diff_view.FileData diffview.txt /*diffview.api.views.diff.diff_view.FileData*
diffview.changelog diffview_changelog.txt /*diffview.changelog*
diffview.changelog-136 diffview_changelog.txt /*diffview.changelog-136*
diffview.changelog-137 diffview_changelog.txt /*diffview.changelog-137*
diffview.changelog-151 diffview_changelog.txt /*diffview.changelog-151*
diffview.changelog-169 diffview_changelog.txt /*diffview.changelog-169*
diffview.changelog-190 diffview_changelog.txt /*diffview.changelog-190*
diffview.changelog-271 diffview_changelog.txt /*diffview.changelog-271*
diffview.changelog-64 diffview_changelog.txt /*diffview.changelog-64*
diffview.changelog-93 diffview_changelog.txt /*diffview.changelog-93*
diffview.defaults diffview_defaults.txt /*diffview.defaults*
diffview.git.FileDict diffview.txt /*diffview.git.FileDict*
diffview.git.LogOptions diffview.txt /*diffview.git.LogOptions*
diffview.nvim diffview.txt /*diffview.nvim*
diffview.txt diffview.txt /*diffview.txt*
diffview.views.file_entry.GitStats diffview.txt /*diffview.views.file_entry.GitStats*

View File

@ -0,0 +1,652 @@
require("diffview.bootstrap")
local async = require("diffview.async")
local lazy = require("diffview.lazy")
local DiffView = lazy.access("diffview.scene.views.diff.diff_view", "DiffView") ---@type DiffView|LazyModule
local FileHistoryView = lazy.access("diffview.scene.views.file_history.file_history_view", "FileHistoryView") ---@type FileHistoryView|LazyModule
local HelpPanel = lazy.access("diffview.ui.panels.help_panel", "HelpPanel") ---@type HelpPanel|LazyModule
local StandardView = lazy.access("diffview.scene.views.standard.standard_view", "StandardView") ---@type StandardView|LazyModule
local lib = lazy.require("diffview.lib") ---@module "diffview.lib"
local utils = lazy.require("diffview.utils") ---@module "diffview.utils"
local vcs_utils = lazy.require("diffview.vcs.utils") ---@module "diffview.vcs.utils"
local Diff1 = lazy.access("diffview.scene.layouts.diff_1", "Diff1") ---@type Diff1|LazyModule
local Diff2Hor = lazy.access("diffview.scene.layouts.diff_2_hor", "Diff2Hor") ---@type Diff2Hor|LazyModule
local Diff2Ver = lazy.access("diffview.scene.layouts.diff_2_ver", "Diff2Ver") ---@type Diff2Ver|LazyModule
local Diff3 = lazy.access("diffview.scene.layouts.diff_3", "Diff3") ---@type Diff3|LazyModule
local Diff3Hor = lazy.access("diffview.scene.layouts.diff_3_hor", "Diff3Hor") ---@type Diff3Hor|LazyModule
local Diff3Ver = lazy.access("diffview.scene.layouts.diff_3_ver", "Diff3Ver") ---@type Diff3Hor|LazyModule
local Diff3Mixed = lazy.access("diffview.scene.layouts.diff_3_mixed", "Diff3Mixed") ---@type Diff3Mixed|LazyModule
local Diff4 = lazy.access("diffview.scene.layouts.diff_4", "Diff4") ---@type Diff4|LazyModule
local Diff4Mixed = lazy.access("diffview.scene.layouts.diff_4_mixed", "Diff4Mixed") ---@type Diff4Mixed|LazyModule
local api = vim.api
local await = async.await
local pl = lazy.access(utils, "path") ---@type PathLib
local M = setmetatable({}, {
__index = function(_, k)
utils.err((
"The action '%s' does not exist! "
.. "See ':h diffview-available-actions' for an overview of available actions."
):format(k))
end
})
M.compat = {}
---@return FileEntry?
---@return integer[]? cursor
local function prepare_goto_file()
local view = lib.get_current_view()
if view and not (view:instanceof(DiffView.__get()) or view:instanceof(FileHistoryView.__get())) then
return
end
---@cast view DiffView|FileHistoryView
local file = view:infer_cur_file()
if file then
---@cast file FileEntry
-- Ensure file exists
if not pl:readable(file.absolute_path) then
utils.err(
string.format(
"File does not exist on disk: '%s'",
pl:relative(file.absolute_path, ".")
)
)
return
end
local cursor
local cur_file = view.cur_entry
if file == cur_file then
local win = view.cur_layout:get_main_win()
cursor = api.nvim_win_get_cursor(win.id)
end
return file, cursor
end
end
function M.goto_file()
local file, cursor = prepare_goto_file()
if file then
local target_tab = lib.get_prev_non_view_tabpage()
if target_tab then
api.nvim_set_current_tabpage(target_tab)
file.layout:restore_winopts()
vim.cmd("sp " .. vim.fn.fnameescape(file.absolute_path))
else
vim.cmd("tabnew")
local temp_bufnr = api.nvim_get_current_buf()
file.layout:restore_winopts()
vim.cmd("keepalt edit " .. vim.fn.fnameescape(file.absolute_path))
if temp_bufnr ~= api.nvim_get_current_buf() then
api.nvim_buf_delete(temp_bufnr, { force = true })
end
end
if cursor then
utils.set_cursor(0, unpack(cursor))
end
end
end
function M.goto_file_edit()
local file, cursor = prepare_goto_file()
if file then
local target_tab = lib.get_prev_non_view_tabpage()
if target_tab then
api.nvim_set_current_tabpage(target_tab)
file.layout:restore_winopts()
vim.cmd("edit " .. vim.fn.fnameescape(file.absolute_path))
else
vim.cmd("tabnew")
local temp_bufnr = api.nvim_get_current_buf()
file.layout:restore_winopts()
vim.cmd("keepalt edit " .. vim.fn.fnameescape(file.absolute_path))
if temp_bufnr ~= api.nvim_get_current_buf() then
api.nvim_buf_delete(temp_bufnr, { force = true })
end
end
if cursor then
utils.set_cursor(0, unpack(cursor))
end
end
end
function M.goto_file_split()
local file, cursor = prepare_goto_file()
if file then
vim.cmd("new")
local temp_bufnr = api.nvim_get_current_buf()
file.layout:restore_winopts()
vim.cmd("keepalt edit " .. vim.fn.fnameescape(file.absolute_path))
if temp_bufnr ~= api.nvim_get_current_buf() then
api.nvim_buf_delete(temp_bufnr, { force = true })
end
if cursor then
utils.set_cursor(0, unpack(cursor))
end
end
end
function M.goto_file_tab()
local file, cursor = prepare_goto_file()
if file then
vim.cmd("tabnew")
local temp_bufnr = api.nvim_get_current_buf()
file.layout:restore_winopts()
vim.cmd("keepalt edit " .. vim.fn.fnameescape(file.absolute_path))
if temp_bufnr ~= api.nvim_get_current_buf() then
api.nvim_buf_delete(temp_bufnr, { force = true })
end
if cursor then
utils.set_cursor(0, unpack(cursor))
end
end
end
---@class diffview.ConflictCount
---@field total integer
---@field current integer
---@field cur_conflict? ConflictRegion
---@field conflicts ConflictRegion[]
---@param num integer
---@param use_delta? boolean
---@return diffview.ConflictCount?
function M.jumpto_conflict(num, use_delta)
local view = lib.get_current_view()
if view and view:instanceof(StandardView.__get()) then
---@cast view StandardView
local main = view.cur_layout:get_main_win()
local curfile = main.file
if main:is_valid() and curfile:is_valid() then
local next_idx
local conflicts, cur, cur_idx = vcs_utils.parse_conflicts(
api.nvim_buf_get_lines(curfile.bufnr, 0, -1, false),
main.id
)
if #conflicts > 0 then
if not use_delta then
next_idx = utils.clamp(num, 1, #conflicts)
else
local delta = num
if not cur and delta < 0 and cur_idx <= #conflicts then
delta = delta + 1
end
if (delta < 0 and cur_idx < 1) or (delta > 0 and cur_idx > #conflicts) then
cur_idx = utils.clamp(cur_idx, 1, #conflicts)
end
next_idx = (cur_idx + delta - 1) % #conflicts + 1
end
local next_conflict = conflicts[next_idx]
local curwin = api.nvim_get_current_win()
api.nvim_win_call(main.id, function()
api.nvim_win_set_cursor(main.id, { next_conflict.first, 0 })
if curwin ~= main.id then view.cur_layout:sync_scroll() end
end)
api.nvim_echo({{ ("Conflict [%d/%d]"):format(next_idx, #conflicts) }}, false, {})
return {
total = #conflicts,
current = next_idx,
cur_conflict = next_conflict,
conflicts = conflicts,
}
end
end
end
end
---Jump to the next merge conflict marker.
---@return diffview.ConflictCount?
function M.next_conflict()
return M.jumpto_conflict(1, true)
end
---Jump to the previous merge conflict marker.
---@return diffview.ConflictCount?
function M.prev_conflict()
return M.jumpto_conflict(-1, true)
end
---Execute `cmd` for each target window in the current view. If no targets
---are given, all windows are targeted.
---@param cmd string|function The vim cmd to execute, or a function.
---@return function action
function M.view_windo(cmd)
local fun
if type(cmd) == "string" then
fun = function(_, _) vim.cmd(cmd) end
else
fun = cmd
end
return function()
local view = lib.get_current_view()
if view and view:instanceof(StandardView.__get()) then
---@cast view StandardView
for _, symbol in ipairs({ "a", "b", "c", "d" }) do
local win = view.cur_layout[symbol] --[[@as Window? ]]
if win then
api.nvim_win_call(win.id, function()
fun(view.cur_layout.name, symbol)
end)
end
end
end
end
end
---@param distance number Either an exact number of lines, or a fraction of the window height.
---@return function
function M.scroll_view(distance)
local scroll_opr = distance < 0 and [[\<c-y>]] or [[\<c-e>]]
local scroll_cmd
if distance % 1 == 0 then
scroll_cmd = ([[exe "norm! %d%s"]]):format(distance, scroll_opr)
else
scroll_cmd = ([[exe "norm! " . float2nr(winheight(0) * %f) . "%s"]])
:format(math.abs(distance), scroll_opr)
end
return function()
local view = lib.get_current_view()
if view and view:instanceof(StandardView.__get()) then
---@cast view StandardView
local max = -1
local target
for _, win in ipairs(view.cur_layout.windows) do
local height = utils.win_content_height(win.id)
if height > max then
max = height
target = win.id
end
end
if target then
api.nvim_win_call(target, function()
vim.cmd(scroll_cmd)
end)
end
end
end
end
---@param kind "ours"|"theirs"|"base"|"local"
local function diff_copy_target(kind)
local view = lib.get_current_view() --[[@as DiffView|FileHistoryView ]]
local file = view.cur_entry
if file then
local layout = file.layout
local bufnr
if layout:instanceof(Diff3.__get()) then
---@cast layout Diff3
if kind == "ours" then
bufnr = layout.a.file.bufnr
elseif kind == "theirs" then
bufnr = layout.c.file.bufnr
elseif kind == "local" then
bufnr = layout.b.file.bufnr
end
elseif layout:instanceof(Diff4.__get()) then
---@cast layout Diff4
if kind == "ours" then
bufnr = layout.a.file.bufnr
elseif kind == "theirs" then
bufnr = layout.c.file.bufnr
elseif kind == "base" then
bufnr = layout.d.file.bufnr
elseif kind == "local" then
bufnr = layout.b.file.bufnr
end
end
if bufnr then return bufnr end
end
end
---@param view DiffView
---@param target "ours"|"theirs"|"base"|"all"|"none"
local function resolve_all_conflicts(view, target)
local main = view.cur_layout:get_main_win()
local curfile = main.file
if main:is_valid() and curfile:is_valid() then
local lines = api.nvim_buf_get_lines(curfile.bufnr, 0, -1, false)
local conflicts = vcs_utils.parse_conflicts(lines, main.id)
if next(conflicts) then
local content
local offset = 0
local first, last
for _, cur_conflict in ipairs(conflicts) do
-- add offset to line numbers
first = cur_conflict.first + offset
last = cur_conflict.last + offset
if target == "ours" then content = cur_conflict.ours.content
elseif target == "theirs" then content = cur_conflict.theirs.content
elseif target == "base" then content = cur_conflict.base.content
elseif target == "all" then
content = utils.vec_join(
cur_conflict.ours.content,
cur_conflict.base.content,
cur_conflict.theirs.content
)
end
content = content or {}
api.nvim_buf_set_lines(curfile.bufnr, first - 1, last, false, content)
offset = offset + (#content - (last - first) - 1)
end
utils.set_cursor(main.id, unpack({
(content and #content or 0) + first - 1,
content and content[1] and #content[#content] or 0
}))
view.cur_layout:sync_scroll()
end
end
end
---@param target "ours"|"theirs"|"base"|"all"|"none"
function M.conflict_choose_all(target)
return async.void(function()
local view = lib.get_current_view() --[[@as DiffView ]]
if (view and view:instanceof(DiffView.__get())) then
---@cast view DiffView
if view.panel:is_focused() then
local item = view:infer_cur_file(false) ---@cast item -DirData
if not item then return end
if not item.active then
-- Open the entry
await(view:set_file(item))
end
end
resolve_all_conflicts(view, target)
end
end)
end
---@param target "ours"|"theirs"|"base"|"all"|"none"
function M.conflict_choose(target)
return function()
local view = lib.get_current_view()
if view and view:instanceof(StandardView.__get()) then
---@cast view StandardView
local main = view.cur_layout:get_main_win()
local curfile = main.file
if main:is_valid() and curfile:is_valid() then
local _, cur = vcs_utils.parse_conflicts(
api.nvim_buf_get_lines(curfile.bufnr, 0, -1, false),
main.id
)
if cur then
local content
if target == "ours" then content = cur.ours.content
elseif target == "theirs" then content = cur.theirs.content
elseif target == "base" then content = cur.base.content
elseif target == "all" then
content = utils.vec_join(
cur.ours.content,
cur.base.content,
cur.theirs.content
)
end
api.nvim_buf_set_lines(curfile.bufnr, cur.first - 1, cur.last, false, content or {})
utils.set_cursor(main.id, unpack({
(content and #content or 0) + cur.first - 1,
content and content[1] and #content[#content] or 0
}))
end
end
end
end
end
---@param target "ours"|"theirs"|"base"|"local"
function M.diffget(target)
return function()
local bufnr = diff_copy_target(target)
if bufnr and api.nvim_buf_is_valid(bufnr) then
local range
if api.nvim_get_mode().mode:match("^[vV]") then
range = ("%d,%d"):format(unpack(utils.vec_sort({
vim.fn.line("."),
vim.fn.line("v")
})))
end
vim.cmd(("%sdiffget %d"):format(range or "", bufnr))
if range then
api.nvim_feedkeys(utils.t("<esc>"), "n", false)
end
end
end
end
---@param target "ours"|"theirs"|"base"|"local"
function M.diffput(target)
return function()
local bufnr = diff_copy_target(target)
if bufnr and api.nvim_buf_is_valid(bufnr) then
vim.cmd("diffput " .. bufnr)
end
end
end
function M.cycle_layout()
local layout_cycles = {
standard = {
Diff2Hor.__get(),
Diff2Ver.__get(),
},
merge_tool = {
Diff3Hor.__get(),
Diff3Ver.__get(),
Diff3Mixed.__get(),
Diff4Mixed.__get(),
Diff1.__get(),
}
}
local view = lib.get_current_view()
if not view then return end
local layouts, files, cur_file
if view:instanceof(FileHistoryView.__get()) then
---@cast view FileHistoryView
layouts = layout_cycles.standard
files = view.panel:list_files()
cur_file = view:cur_file()
elseif view:instanceof(DiffView.__get()) then
---@cast view DiffView
cur_file = view.cur_entry
if cur_file then
layouts = cur_file.kind == "conflicting"
and layout_cycles.merge_tool
or layout_cycles.standard
files = cur_file.kind == "conflicting"
and view.files.conflicting
or utils.vec_join(view.panel.files.working, view.panel.files.staged)
end
else
return
end
for _, entry in ipairs(files) do
local cur_layout = entry.layout
local next_layout = layouts[utils.vec_indexof(layouts, cur_layout.class) % #layouts + 1]
entry:convert_layout(next_layout)
end
if cur_file then
local main = view.cur_layout:get_main_win()
local pos = api.nvim_win_get_cursor(main.id)
local was_focused = view.cur_layout:is_focused()
cur_file.layout.emitter:once("files_opened", function()
utils.set_cursor(main.id, unpack(pos))
if not was_focused then view.cur_layout:sync_scroll() end
end)
view:set_file(cur_file, false)
main = view.cur_layout:get_main_win()
if was_focused then main:focus() end
end
end
---@param keymap_groups string|string[]
function M.help(keymap_groups)
keymap_groups = type(keymap_groups) == "table" and keymap_groups or { keymap_groups }
return function()
local view = lib.get_current_view()
if view then
local help_panel = HelpPanel(view, keymap_groups) --[[@as HelpPanel ]]
help_panel:focus()
end
end
end
do
M.compat.fold_cmds = {}
-- For file entries that use custom folds with `foldmethod=manual` we need to
-- replicate fold commands in all diff windows, as folds are only
-- synchronized between diff windows when `foldmethod=diff`.
local function compat_fold(fold_cmd)
return function()
if vim.wo.foldmethod ~= "manual" then
local ok, msg = pcall(vim.cmd, "norm! " .. fold_cmd)
if not ok and msg then
api.nvim_err_writeln(msg)
end
return
end
local view = lib.get_current_view()
if view and view:instanceof(StandardView.__get()) then
---@cast view StandardView
local err
for _, win in ipairs(view.cur_layout.windows) do
api.nvim_win_call(win.id, function()
local ok, msg = pcall(vim.cmd, "norm! " .. fold_cmd)
if not ok then err = msg end
end)
end
if err then api.nvim_err_writeln(err) end
end
end
end
for _, fold_cmd in ipairs({
"za", "zA", "ze", "zE", "zo", "zc", "zO", "zC", "zr", "zm", "zR", "zM",
"zv", "zx", "zX", "zn", "zN", "zi",
}) do
table.insert(M.compat.fold_cmds, {
"n",
fold_cmd,
compat_fold(fold_cmd),
{ desc = "diffview_ignore" },
})
end
end
local action_names = {
"close",
"close_all_folds",
"close_fold",
"copy_hash",
"focus_entry",
"focus_files",
"listing_style",
"next_entry",
"open_all_folds",
"open_commit_log",
"open_fold",
"open_in_diffview",
"options",
"prev_entry",
"refresh_files",
"restore_entry",
"select_entry",
"select_next_entry",
"select_prev_entry",
"stage_all",
"toggle_files",
"toggle_flatten_dirs",
"toggle_fold",
"toggle_stage_entry",
"unstage_all",
}
for _, name in ipairs(action_names) do
M[name] = function()
require("diffview").emit(name)
end
end
return M

View File

@ -0,0 +1,179 @@
local async = require("diffview.async")
local lazy = require("diffview.lazy")
local DiffView = lazy.access("diffview.scene.views.diff.diff_view", "DiffView") ---@type DiffView|LazyModule
local FileEntry = lazy.access("diffview.scene.file_entry", "FileEntry") ---@type FileEntry|LazyModule
local FilePanel = lazy.access("diffview.scene.views.diff.file_panel", "FilePanel") ---@type FilePanel|LazyModule
local Rev = lazy.access("diffview.vcs.adapters.git.rev", "GitRev") ---@type GitRev|LazyModule
local RevType = lazy.access("diffview.vcs.rev", "RevType") ---@type RevType|LazyModule
local vcs_utils = lazy.require("diffview.vcs") ---@module "diffview.vcs"
local oop = lazy.require("diffview.oop") ---@module "diffview.oop"
local utils = lazy.require("diffview.utils") ---@module "diffview.utils"
local logger = DiffviewGlobal.logger
local M = {}
---@class FileData
---@field path string Path relative to git root.
---@field oldpath string|nil If the file has been renamed, this should be the old path, oterhwise nil.
---@field status string Git status symbol.
---@field stats GitStats|nil
---@field left_null boolean Indicates that the left buffer should be represented by the null buffer.
---@field right_null boolean Indicates that the right buffer should be represented by the null buffer.
---@field selected boolean|nil Indicates that this should be the initially selected file.
---@class CDiffView : DiffView
---@field files any
---@field fetch_files function A function that should return an updated list of files.
---@field get_file_data function A function that is called with parameters `path: string` and `split: string`, and should return a list of lines that should make up the buffer.
local CDiffView = oop.create_class("CDiffView", DiffView.__get())
---CDiffView constructor.
---@param opt any
function CDiffView:init(opt)
logger:info("[api] Creating a new Custom DiffView.")
self.valid = false
local err, adapter = vcs_utils.get_adapter({ top_indicators = { opt.git_root } })
if err then
utils.err(
("Failed to create an adapter for the repository: %s")
:format(utils.str_quote(opt.git_root))
)
return
end
---@cast adapter -?
-- Fix malformed revs
for _, v in ipairs({ "left", "right" }) do
local rev = opt[v]
if not rev or not rev.type then
opt[v] = Rev(RevType.STAGE, 0)
end
end
self.fetch_files = opt.update_files
self.get_file_data = opt.get_file_data
self:super(vim.tbl_extend("force", opt, {
adapter = adapter,
panel = FilePanel(
adapter,
self.files,
self.path_args,
self.rev_arg or adapter:rev_to_pretty_string(opt.left, opt.right)
),
}))
if type(opt.files) == "table" and not vim.tbl_isempty(opt.files) then
local files = self:create_file_entries(opt.files)
for kind, entries in pairs(files) do
for _, entry in ipairs(entries) do
table.insert(self.files[kind], entry)
end
end
self.files:update_file_trees()
if self.panel.cur_file then
vim.schedule(function()
self:set_file(self.panel.cur_file, false, true)
end)
end
end
self.valid = true
end
---@override
CDiffView.get_updated_files = async.wrap(function(self, callback)
local err
repeat
local ok, new_files = pcall(self.fetch_files, self)
if not ok or type(new_files) ~= "table" then
err = { "Integrating plugin failed to provide file data!" }
break
end
---@diagnostic disable-next-line: redefined-local
local ok, entries = pcall(self.create_file_entries, self, new_files)
if not ok then
err = { "Integrating plugin provided malformed file data!" }
break
end
callback(nil, entries)
return
until true
utils.err(err, true)
logger:error(table.concat(err, "\n"))
callback(err, nil)
end)
function CDiffView:create_file_entries(files)
local entries = {}
local sections = {
{ kind = "conflicting", files = files.conflicting or {} },
{ kind = "working", files = files.working or {}, left = self.left, right = self.right },
{
kind = "staged",
files = files.staged or {},
left = self.adapter:head_rev(),
right = Rev(RevType.STAGE, 0),
},
}
for _, v in ipairs(sections) do
entries[v.kind] = {}
for _, file_data in ipairs(v.files) do
if v.kind == "conflicting" then
table.insert(entries[v.kind], FileEntry.with_layout(CDiffView.get_default_merge_layout(), {
adapter = self.adapter,
path = file_data.path,
oldpath = file_data.oldpath,
status = "U",
kind = "conflicting",
revs = {
a = Rev(RevType.STAGE, 2),
b = Rev(RevType.LOCAL),
c = Rev(RevType.STAGE, 3),
d = Rev(RevType.STAGE, 1),
},
}))
else
table.insert(entries[v.kind], FileEntry.with_layout(CDiffView.get_default_layout(), {
adapter = self.adapter,
path = file_data.path,
oldpath = file_data.oldpath,
status = file_data.status,
stats = file_data.stats,
kind = v.kind,
revs = {
a = v.left,
b = v.right,
},
get_data = self.get_file_data,
--FIXME: left_null, right_null
}))
end
if file_data.selected then
self.panel:set_cur_file(entries[v.kind][#entries[v.kind]])
end
end
end
return entries
end
M.CDiffView = CDiffView
return M

View File

@ -0,0 +1,432 @@
local lazy = require("diffview.lazy")
local oop = require("diffview.oop")
local utils = lazy.require("diffview.utils") ---@module "diffview.utils"
local M = {}
local short_flag_pat = { "^[-+](%a)=?(.*)" }
local long_flag_pat = { "^%-%-(%a[%a%d-]*)=?(.*)", "^%+%+(%a[%a%d-]*)=?(.*)" }
---@class ArgObject : diffview.Object
---@field flags table<string, string[]>
---@field args string[]
---@field post_args string[]
local ArgObject = oop.create_class("ArgObject")
---ArgObject constructor.
---@param flags table<string, string>
---@param args string[]
function ArgObject:init(flags, args, post_args)
self.flags = flags
self.args = args
self.post_args = post_args
end
---@class ArgObject.GetFlagSpec
---@field plain boolean Never cast string values to booleans.
---@field expect_list boolean Return a list of all defined values for the given flag.
---@field expect_string boolean Inferred boolean values are changed to be empty strings.
---@field no_empty boolean Return nil if the value is an empty string. Implies `expect_string`.
---@field expand boolean Expand wildcards and special keywords (`:h expand()`).
---Get a flag value.
---@param names string|string[] Flag synonyms
---@param opt? ArgObject.GetFlagSpec
---@return string[]|string|boolean
function ArgObject:get_flag(names, opt)
opt = opt or {}
if opt.no_empty then
opt.expect_string = true
end
if type(names) ~= "table" then
names = { names }
end
local values = {}
for _, name in ipairs(names) do
if self.flags[name] then
utils.vec_push(values, unpack(self.flags[name]))
end
end
values = utils.tbl_fmap(values, function(v)
if opt.expect_string and v == "true" then
-- Undo inferred boolean values
if opt.no_empty then
return nil
end
v = ""
elseif not opt.plain and (v == "true" or v == "false") then
-- Cast to boolean
v = v == "true"
end
if opt.expand then
v = vim.fn.expand(v)
end
return v
end)
-- If a list isn't expected: return the last defined value for this flag.
return opt.expect_list and values or values[#values]
end
---@class FlagValueMap : diffview.Object
---@field map table<string, string[]>
local FlagValueMap = oop.create_class("FlagValueMap")
---FlagValueMap constructor
function FlagValueMap:init()
self.map = {}
end
---@param flag_synonyms string[]
---@param producer? string[]|fun(name_lead: string, arg_lead: string): string[]
function FlagValueMap:put(flag_synonyms, producer)
for _, flag in ipairs(flag_synonyms) do
local char = flag:sub(1, 1)
if char ~= "-" and char ~= "+" then
if #flag > 1 then
flag = "--" .. flag
else
flag = "-" .. flag
end
end
self.map[flag] = producer or { "true", "false" }
self.map[#self.map + 1] = flag
end
end
---Get list of possible values for a given flag.
---@param flag_name string
---@return string[]
function FlagValueMap:get(flag_name)
local char = flag_name:sub(1, 1)
if char ~= "-" and char ~= "+" then
if #flag_name > 1 then
flag_name = "--" .. flag_name
else
flag_name = "-" .. flag_name
end
end
if type(self.map[flag_name]) == "function" then
local is_short = utils.str_match(flag_name, short_flag_pat) ~= nil
return self.map[flag_name](flag_name .. (not is_short and "=" or ""), "")
end
return self.map[flag_name]
end
---Get a list of all flag names.
---@return string[]
function FlagValueMap:get_all_names()
return utils.vec_slice(self.map)
end
---@param arg_lead string
---@return string[]?
function FlagValueMap:get_completion(arg_lead)
arg_lead = arg_lead or ""
local name
local is_short = utils.str_match(arg_lead, short_flag_pat) ~= nil
if is_short then
name = arg_lead:sub(1, 2)
arg_lead = arg_lead:match("..=?(.*)") or ""
else
name = arg_lead:gsub("=.*", "")
arg_lead = arg_lead:match("=(.*)") or ""
end
local name_lead = name .. (not is_short and "=" or "")
local values = self.map[name]
if type(values) == "function" then
values = values(name_lead, arg_lead)
end
if not values then
return nil
end
local items = {}
for _, v in ipairs(values) do
local e_lead, _ = vim.pesc(arg_lead)
if v:match(e_lead) then
items[#items + 1] = name_lead .. v
end
end
return items
end
---Parse args and create an ArgObject.
---@param args string[]
---@return ArgObject
function M.parse(args)
local flags = {}
local pre_args = {}
local post_args = {}
for i, arg in ipairs(args) do
if arg == "--" then
for j = i + 1, #args do
table.insert(post_args, args[j])
end
break
end
local flag, value
flag, value = utils.str_match(arg, short_flag_pat)
if flag then
value = (value == "") and "true" or value
if not flags[flag] then
flags[flag] = {}
end
table.insert(flags[flag], value)
goto continue
end
flag, value = utils.str_match(arg, long_flag_pat)
if flag then
value = (value == "") and "true" or value
if not flags[flag] then
flags[flag] = {}
end
table.insert(flags[flag], value)
goto continue
end
table.insert(pre_args, arg)
::continue::
end
return ArgObject(flags, pre_args, post_args)
end
---Split the line range from an EX command arg.
---@param arg string
---@return string range, string command
function M.split_ex_range(arg)
local idx = arg:match(".*()%A")
if not idx then
return "", arg
end
local slice = arg:sub(idx or 1)
idx = slice:match("[^']()%a")
if idx then
return arg:sub(1, (#arg - #slice) + idx - 1), slice:sub(idx)
end
return arg, ""
end
---@class CmdLineContext
---@field cmd_line string
---@field args string[] # The tokenized list of arguments.
---@field raw_args string[] # The unprocessed list of arguments. Contains syntax characters, such as quotes.
---@field arg_lead string # The leading part of the current argument.
---@field lead_quote string? # If present: the quote character used for the current argument.
---@field cur_pos integer # The cursor position in the command line.
---@field argidx integer # Index of the current argument.
---@field divideridx integer # The index of the end-of-options token. (default: math.huge)
---@field range string? # Ex command range.
---@field between boolean # The current position is between two arguments.
---@class arg_parser.scan.Opt
---@field cur_pos integer # The current cursor position in the command line.
---@field allow_quoted boolean # Everything between a pair of quotes should be treated as part of a single argument. (default: true)
---@field allow_ex_range boolean # The command line may contain an EX command range. (default: false)
---Tokenize a command line string.
---@param cmd_line string
---@param opt? arg_parser.scan.Opt
---@return CmdLineContext
function M.scan(cmd_line, opt)
opt = vim.tbl_extend("keep", opt or {}, {
cur_pos = #cmd_line + 1,
allow_quoted = true,
allow_ex_range = false,
}) --[[@as arg_parser.scan.Opt ]]
local args = {}
local raw_args = {}
local arg_lead
local divideridx = math.huge
local argidx
local between = false
local cur_quote, lead_quote
local arg, raw_arg = "", ""
local h, i = -1, 1
while i <= #cmd_line do
local char = cmd_line:sub(i, i)
if not argidx and i > opt.cur_pos then
argidx = #args + 1
arg_lead = arg
lead_quote = cur_quote
if h < opt.cur_pos then between = true end
end
if char == "\\" then
arg = arg .. char
raw_arg = raw_arg .. char
if i < #cmd_line then
i = i + 1
arg = arg .. cmd_line:sub(i, i)
raw_arg = raw_arg .. cmd_line:sub(i, i)
end
h = i
elseif cur_quote then
if char == cur_quote then
cur_quote = nil
else
arg = arg .. char
end
raw_arg = raw_arg .. char
h = i
elseif opt.allow_quoted and (char == [[']] or char == [["]]) then
cur_quote = char
raw_arg = raw_arg .. char
h = i
elseif char:match("%s") then
if arg ~= "" then
table.insert(args, arg)
if arg == "--" and i - 1 < #cmd_line then
divideridx = #args
end
end
if raw_arg ~= "" then
table.insert(raw_args, raw_arg)
end
arg = ""
raw_arg = ""
-- Skip whitespace
i = i + cmd_line:sub(i, -1):match("^%s+()") - 2
else
arg = arg .. char
raw_arg = raw_arg .. char
h = i
end
i = i + 1
end
if #arg > 0 then
table.insert(args, arg)
table.insert(raw_args, raw_arg)
if not arg_lead then
arg_lead = arg
lead_quote = cur_quote
end
if arg == "--" and cmd_line:sub(#cmd_line, #cmd_line) ~= "-" then
divideridx = #args
end
end
if not argidx then
argidx = #args
if cmd_line:sub(#cmd_line, #cmd_line):match("%s") then
argidx = argidx + 1
end
end
local range
if #args > 0 then
if opt.allow_ex_range then
range, args[1] = M.split_ex_range(args[1])
_, raw_args[1] = M.split_ex_range(raw_args[1])
end
if args[1] == "" then
table.remove(args, 1)
table.remove(raw_args, 1)
argidx = math.max(argidx - 1, 1)
divideridx = math.max(divideridx - 1, 1)
end
end
return {
cmd_line = cmd_line,
args = args,
raw_args = raw_args,
arg_lead = arg_lead or "",
lead_quote = lead_quote,
cur_pos = opt.cur_pos,
argidx = argidx,
divideridx = divideridx,
range = range ~= "" and range or nil,
between = between,
}
end
---Filter completion candidates.
---@param arg_lead string
---@param candidates string[]
---@return string[]
function M.filter_candidates(arg_lead, candidates)
arg_lead, _ = vim.pesc(arg_lead)
return vim.tbl_filter(function(item)
return item:match(arg_lead)
end, candidates)
end
---Process completion candidates.
---@param candidates string[]
---@param ctx CmdLineContext
---@param input_cmp boolean? Completion for |input()|.
---@return string[]
function M.process_candidates(candidates, ctx, input_cmp)
if not candidates then return {} end
local cmd_lead = ""
local ex_lead = (ctx.lead_quote or "") .. ctx.arg_lead
if ctx.arg_lead and ctx.arg_lead:find("[^\\]%s") then
ex_lead = (ctx.lead_quote or "") .. ctx.arg_lead:match(".*[^\\]%s(.*)")
end
if input_cmp then
cmd_lead = ctx.cmd_line:sub(1, ctx.cur_pos - #ex_lead)
end
local ret = vim.tbl_map(function(v)
if v:match("^" .. vim.pesc(ctx.arg_lead)) then
return cmd_lead .. ex_lead .. v:sub(#ctx.arg_lead + 1)
elseif input_cmp then
return cmd_lead .. v
end
return (ctx.lead_quote or "") .. v
end, candidates)
return M.filter_candidates(cmd_lead .. ex_lead, ret)
end
function M.ambiguous_bool(value, default, truthy, falsy)
if vim.tbl_contains(truthy, value) then
return true
end
if vim.tbl_contains(falsy, value) then
return false
end
return default
end
M.ArgObject = ArgObject
M.FlagValueMap = FlagValueMap
return M

View File

@ -0,0 +1,577 @@
local ffi = require("diffview.ffi")
local oop = require("diffview.oop")
local fmt = string.format
local uv = vim.loop
local DEFAULT_ERROR = "Unkown error."
local M = {}
---@package
---@type { [Future]: boolean }
M._watching = setmetatable({}, { __mode = "k" })
---@package
---@type { [thread]: Future }
M._handles = {}
---@alias AsyncFunc (fun(...): Future)
---@alias AsyncKind "callback"|"void"
local function dstring(object)
if not DiffviewGlobal.logger then return "" end
dstring = DiffviewGlobal.logger.dstring
return dstring(object)
end
---@param ... any
---@return table
local function tbl_pack(...)
return { n = select("#", ...), ... }
end
---@param t table
---@param i? integer
---@param j? integer
---@return any ...
local function tbl_unpack(t, i, j)
return unpack(t, i or 1, j or t.n or table.maxn(t))
end
---Returns the current thread or `nil` if it's the main thread.
---
---NOTE: coroutine.running() was changed between Lua 5.1 and 5.2:
--- • 5.1: Returns the running coroutine, or `nil` when called by the main
--- thread.
--- • 5.2: Returns the running coroutine plus a boolean, true when the running
--- coroutine is the main one.
---
---For LuaJIT, 5.2 behaviour is enabled with LUAJIT_ENABLE_LUA52COMPAT
---
---We need to handle both.
---
---Source: https://github.com/lewis6991/async.nvim/blob/bad4edbb2917324cd11662dc0209ce53f6c8bc23/lua/async.lua#L10
---@return thread?
local function current_thread()
local current, ismain = coroutine.running()
if type(ismain) == "boolean" then
return not ismain and current or nil
else
return current
end
end
---@class Waitable : diffview.Object
local Waitable = oop.create_class("Waitable")
M.Waitable = Waitable
---@abstract
---@return any ... # Any values returned by the waitable
function Waitable:await() oop.abstract_stub() end
---Schedule a callback to be invoked when this waitable has settled.
---@param callback function
function Waitable:finally(callback)
(M.void(function()
local ret = tbl_pack(M.await(self))
callback(tbl_unpack(ret))
end))()
end
---@class Future : Waitable
---@operator call : Future
---@field package thread thread
---@field package listeners Future[]
---@field package parent? Future
---@field package func? function
---@field package return_values? any[]
---@field package err? string
---@field package kind AsyncKind
---@field package started boolean
---@field package awaiting_cb boolean
---@field package done boolean
---@field package has_raised boolean # `true` if this future has raised an error.
local Future = oop.create_class("Future", Waitable)
function Future:init(opt)
opt = opt or {}
if opt.thread then
self.thread = opt.thread
elseif opt.func then
self.thread = coroutine.create(opt.func)
else
error("Either 'thread' or 'func' must be specified!")
end
M._handles[self.thread] = self
self.listeners = {}
self.kind = opt.kind
self.started = false
self.awaiting_cb = false
self.done = false
self.has_raised = false
end
---@package
---@return string
function Future:__tostring()
return dstring(self.thread)
end
---@package
function Future:destroy()
M._handles[self.thread] = nil
end
---@package
---@param value boolean
function Future:set_done(value)
self.done = value
if self:is_watching() then
self:dprint("done was set:", self.done)
end
end
---@return boolean
function Future:is_done()
return not not self.done
end
---@return any ... # If the future has completed, this returns any returned values.
function Future:get_returned()
if not self.return_values then return end
return unpack(self.return_values, 2, table.maxn(self.return_values))
end
---@package
---@param ... any
function Future:dprint(...)
if not DiffviewGlobal.logger then return end
if DiffviewGlobal.debug_level >= 10 or M._watching[self] then
local t = { self, "::", ... }
for i = 1, table.maxn(t) do t[i] = dstring(t[i]) end
DiffviewGlobal.logger:debug(table.concat(t, " "))
end
end
---@package
---@param ... any
function Future:dprintf(...)
self:dprint(fmt(...))
end
---Start logging debug info about this future.
function Future:watch()
M._watching[self] = true
end
---Stop logging debug info about this future.
function Future:unwatch()
M._watching[self] = nil
end
---@package
---@return boolean
function Future:is_watching()
return not not M._watching[self]
end
---@package
---@param force? boolean
function Future:raise(force)
if self.has_raised and not force then return end
self.has_raised = true
error(self.err)
end
---@package
function Future:step(...)
self:dprint("step")
local ret = { coroutine.resume(self.thread, ...) }
local ok = ret[1]
if not ok then
local err = ret[2] or DEFAULT_ERROR
local func_info
if self.func then
func_info = debug.getinfo(self.func, "uS")
end
local msg = fmt(
"The coroutine failed with this message: \n"
.. "\tcontext: cur_thread=%s co_thread=%s %s\n%s",
dstring(current_thread() or "main"),
dstring(self.thread),
func_info and fmt("co_func=%s:%d", func_info.short_src, func_info.linedefined) or "",
debug.traceback(self.thread, err)
)
self:set_done(true)
self:notify_all(false, msg)
self:destroy()
self:raise()
return
end
if coroutine.status(self.thread) == "dead" then
self:dprint("handle dead")
self:set_done(true)
self:notify_all(true, unpack(ret, 2, table.maxn(ret)))
self:destroy()
return
end
end
---@package
---@param ok boolean
---@param ... any
function Future:notify_all(ok, ...)
local ret_values = tbl_pack(ok, ...)
if not ok then
self.err = ret_values[2] or DEFAULT_ERROR
end
local seen = {}
while next(self.listeners) do
local handle = table.remove(self.listeners, #self.listeners) --[[@as Future ]]
-- We don't want to trigger multiple steps for a single thread
if handle and not seen[handle.thread] then
self:dprint("notifying:", handle)
seen[handle.thread] = true
handle:step(ret_values)
end
end
end
---@override
---@return any ... # Return values
function Future:await()
if self.err then
self:raise(true)
return
end
if self:is_done() then
return self:get_returned()
end
local current = current_thread()
if not current then
-- Await called from main thread
return self:toplevel_await()
end
local parent_handle = M._handles[current]
if not parent_handle then
-- We're on a thread not managed by us: create a Future wrap around the
-- thread
self:dprint("creating a wrapper around unmanaged thread")
self.parent = Future({
thread = current,
kind = "void",
})
else
self.parent = parent_handle
end
if current ~= self.thread then
-- We want the current thread to be notified when this future is done /
-- terminated
table.insert(self.listeners, self.parent)
end
self:dprintf("awaiting: yielding=%s listeners=%s", dstring(current), dstring(self.listeners))
coroutine.yield()
local ok
if not self.return_values then
ok = self.err == nil
else
ok = self.return_values[1]
if not ok then
self.err = self.return_values[2] or DEFAULT_ERROR
end
end
if not ok then
self:raise(true)
return
end
return self:get_returned()
end
---@package
---@return any ...
function Future:toplevel_await()
local ok, status
while true do
ok, status = vim.wait(1000 * 60, function()
return coroutine.status(self.thread) == "dead"
end, 1)
-- Respect interrupts
if status ~= -1 then break end
end
if not ok then
if status == -1 then
error("Async task timed out!")
elseif status == -2 then
error("Async task got interrupted!")
end
end
if self.err then
self:raise(true)
return
end
return self:get_returned()
end
---@class async._run.Opt
---@field kind AsyncKind
---@field nparams? integer
---@field args any[]
---@package
---@param func function
---@param opt async._run.Opt
function M._run(func, opt)
opt = opt or {}
local handle ---@type Future
local use_err_handler = not not current_thread()
local function wrapped_func(...)
if use_err_handler then
-- We are not on the main thread: use custom err handler
local ok = xpcall(func, function(err)
handle.err = debug.traceback(err, 2)
end, ...)
if not ok then
handle:dprint("an error was raised: terminating")
handle:set_done(true)
handle:destroy()
error(handle.err, 0)
return
end
else
func(...)
end
-- Check if we need to yield until cb. We might not need to if the cb was
-- called in a synchronous way.
if opt.kind == "callback" and not handle:is_done() then
handle.awaiting_cb = true
handle:dprintf("yielding for cb: current=%s", dstring(current_thread()))
coroutine.yield()
handle:dprintf("resuming after cb: current=%s", dstring(current_thread()))
end
handle:set_done(true)
end
if opt.kind == "callback" then
local cur_cb = opt.args[opt.nparams]
local function wrapped_cb(...)
handle:set_done(true)
handle.return_values = { true, ... }
if cur_cb then cur_cb(...) end
if handle.awaiting_cb then
-- The thread was yielding for the callback: resume
handle.awaiting_cb = false
handle:step()
end
handle:notify_all(true, ...)
end
opt.args[opt.nparams] = wrapped_cb
end
handle = Future({ func = wrapped_func, kind = opt.kind })
handle:dprint("created thread")
handle.func = func
handle.started = true
handle:step(tbl_unpack(opt.args))
return handle
end
---Create an async task for a function with no return values.
---@param func function
---@return AsyncFunc
function M.void(func)
return function(...)
return M._run(func, {
kind = "void",
args = { ... },
})
end
end
---Create an async task for a callback style function.
---@param func function
---@param nparams? integer # The number of parameters.
---The last parameter in `func` must be the callback. For Lua functions this
---can be derived through reflection. If `func` is an FFI procedure then
---`nparams` is required.
---@return AsyncFunc
function M.wrap(func, nparams)
if not nparams then
local info = debug.getinfo(func, "uS")
assert(info.what == "Lua", "Parameter count can only be derived for Lua functions!")
nparams = info.nparams
end
return function(...)
return M._run(func, {
nparams = nparams,
kind = "callback",
args = { ... },
})
end
end
---@param waitable Waitable
---@return any ... # Any values returned by the waitable
function M.await(waitable)
return waitable:await()
end
---Await the async function `x` with the given arguments in protected mode. `x`
---may also be a waitable, in which case the subsequent parameters are ignored.
---@param x AsyncFunc|Waitable # The async function or waitable.
---@param ... any # Arguments to be applied to the `x` if it's a function.
---@return boolean ok # `false` if the execution of `x` failed.
---@return any result # Either the first returned value from `x` or an error message.
---@return any ... # Any subsequent values returned from `x`.
function M.pawait(x, ...)
local args = tbl_pack(...)
return pcall(function()
if type(x) == "function" then
return M.await(x(tbl_unpack(args)))
else
return x:await()
end
end)
end
-- ###############################
-- ### VARIOUS ASYNC UTILITIES ###
-- ###############################
local await = M.await
---Create a synchronous version of an async `void` task. Calling the resulting
---function will block until the async task is done.
---@param func function
function M.sync_void(func)
local afunc = M.void(func)
return function(...)
return await(afunc(...))
end
end
---Create a synchronous version of an async `wrap` task. Calling the resulting
---function will block until the async task is done. Any values that were
---passed to the callback will be returned.
---@param func function
---@param nparams? integer
---@return (fun(...): ...)
function M.sync_wrap(func, nparams)
local afunc = M.wrap(func, nparams)
return function(...)
return await(afunc(...))
end
end
---Run the given async tasks concurrently, and then wait for them all to
---terminate.
---@param tasks (AsyncFunc|Waitable)[]
M.join = M.void(function(tasks)
---@type Waitable[]
local futures = {}
-- Ensure all async tasks are started
for _, cur in ipairs(tasks) do
if cur then
if type(cur) == "function" then
futures[#futures+1] = cur()
else
---@cast cur Waitable
futures[#futures+1] = cur
end
end
end
-- Await all futures
for _, future in ipairs(futures) do
await(future)
end
end)
---Run, and await the given async tasks in sequence.
---@param tasks (AsyncFunc|Waitable)[]
M.chain = M.void(function(tasks)
for _, task in ipairs(tasks) do
if type(task) == "function" then
---@cast task AsyncFunc
await(task())
else
---@cast task Waitable
await(task)
end
end
end)
---Async task that resolves after the given `timeout` ms passes.
---@param timeout integer # Duration of the timeout (ms)
M.timeout = M.wrap(function(timeout, callback)
local timer = assert(uv.new_timer())
timer:start(
timeout,
0,
function()
if not timer:is_closing() then timer:close() end
callback()
end
)
end)
---Yield until the Neovim API is available.
---@param fast_only? boolean # Only schedule if in an |api-fast| event.
--- When this is `true`, the scheduler will resume immediately unless the
--- editor is in an |api-fast| event. This means that the API might still be
--- limited by other mechanisms (i.e. |textlock|).
M.scheduler = M.wrap(function(fast_only, callback)
if (fast_only and not vim.in_fast_event()) or not ffi.nvim_is_locked() then
callback()
return
end
vim.schedule(callback)
end)
M.schedule_now = M.wrap(vim.schedule, 1)
return M

View File

@ -0,0 +1,56 @@
if DiffviewGlobal and DiffviewGlobal.bootstrap_done then
return DiffviewGlobal.bootstrap_ok
end
local lazy = require("diffview.lazy")
local EventEmitter = lazy.access("diffview.events", "EventEmitter") ---@type EventEmitter|LazyModule
local Logger = lazy.access("diffview.logger", "Logger") ---@type Logger|LazyModule
local config = lazy.require("diffview.config") ---@module "diffview.config"
local diffview = lazy.require("diffview") ---@module "diffview"
local utils = lazy.require("diffview.utils") ---@module "diffview.utils"
local uv = vim.loop
local function err(msg)
msg = msg:gsub("'", "''")
vim.cmd("echohl Error")
vim.cmd(string.format("echom '[diffview.nvim] %s'", msg))
vim.cmd("echohl NONE")
end
_G.DiffviewGlobal = {
bootstrap_done = true,
bootstrap_ok = false,
}
if vim.fn.has("nvim-0.7") ~= 1 then
err(
"Minimum required version is Neovim 0.7.0! Cannot continue."
.. " (See ':h diffview.changelog-137')"
)
return false
end
_G.DiffviewGlobal = {
---Debug Levels:
---0: NOTHING
---1: NORMAL
---5: LOADING
---10: RENDERING & ASYNC
---@diagnostic disable-next-line: missing-parameter
debug_level = tonumber((uv.os_getenv("DEBUG_DIFFVIEW"))) or 0,
state = {},
bootstrap_done = true,
bootstrap_ok = true,
}
DiffviewGlobal.logger = Logger()
DiffviewGlobal.emitter = EventEmitter()
DiffviewGlobal.emitter:on_any(function(e, args)
diffview.nore_emit(e.id, utils.tbl_unpack(args))
config.user_emitter:nore_emit(e.id, utils.tbl_unpack(args))
end)
return true

View File

@ -0,0 +1,660 @@
require("diffview.bootstrap")
---@diagnostic disable: deprecated
local EventEmitter = require("diffview.events").EventEmitter
local actions = require("diffview.actions")
local lazy = require("diffview.lazy")
local Diff1 = lazy.access("diffview.scene.layouts.diff_1", "Diff1") ---@type Diff1|LazyModule
local Diff2 = lazy.access("diffview.scene.layouts.diff_2", "Diff2") ---@type Diff2|LazyModule
local Diff2Hor = lazy.access("diffview.scene.layouts.diff_2_hor", "Diff2Hor") ---@type Diff2Hor|LazyModule
local Diff2Ver = lazy.access("diffview.scene.layouts.diff_2_ver", "Diff2Ver") ---@type Diff2Ver|LazyModule
local Diff3 = lazy.access("diffview.scene.layouts.diff_3", "Diff3") ---@type Diff3|LazyModule
local Diff3Hor = lazy.access("diffview.scene.layouts.diff_3_hor", "Diff3Hor") ---@type Diff3Hor|LazyModule
local Diff3Mixed = lazy.access("diffview.scene.layouts.diff_3_mixed", "Diff3Mixed") ---@type Diff3Mixed|LazyModule
local Diff3Ver = lazy.access("diffview.scene.layouts.diff_3_ver", "Diff3Ver") ---@type Diff3Hor|LazyModule
local Diff4 = lazy.access("diffview.scene.layouts.diff_4", "Diff4") ---@type Diff4|LazyModule
local Diff4Mixed = lazy.access("diffview.scene.layouts.diff_4_mixed", "Diff4Mixed") ---@type Diff4Mixed|LazyModule
local utils = lazy.require("diffview.utils") ---@module "diffview.utils"
local M = {}
local setup_done = false
---@deprecated
function M.diffview_callback(cb_name)
if cb_name == "select" then
-- Reroute deprecated action
return actions.select_entry
end
return actions[cb_name]
end
---@class ConfigLogOptions
---@field single_file LogOptions
---@field multi_file LogOptions
-- stylua: ignore start
---@class DiffviewConfig
M.defaults = {
diff_binaries = false,
enhanced_diff_hl = false,
git_cmd = { "git" },
hg_cmd = { "hg" },
use_icons = true,
show_help_hints = true,
watch_index = true,
icons = {
folder_closed = "",
folder_open = "",
},
signs = {
fold_closed = "",
fold_open = "",
done = "",
},
view = {
default = {
layout = "diff2_horizontal",
disable_diagnostics = false,
winbar_info = false,
},
merge_tool = {
layout = "diff3_horizontal",
disable_diagnostics = true,
winbar_info = true,
},
file_history = {
layout = "diff2_horizontal",
disable_diagnostics = false,
winbar_info = false,
},
},
file_panel = {
listing_style = "tree",
tree_options = {
flatten_dirs = true,
folder_statuses = "only_folded"
},
win_config = {
position = "left",
width = 35,
win_opts = {}
},
},
file_history_panel = {
log_options = {
---@type ConfigLogOptions
git = {
single_file = {
diff_merges = "first-parent",
follow = true,
},
multi_file = {
diff_merges = "first-parent",
},
},
---@type ConfigLogOptions
hg = {
single_file = {},
multi_file = {},
},
},
win_config = {
position = "bottom",
height = 16,
win_opts = {}
},
},
commit_log_panel = {
win_config = {
win_opts = {}
},
},
default_args = {
DiffviewOpen = {},
DiffviewFileHistory = {},
},
hooks = {},
-- Tabularize formatting pattern: `\v(\"[^"]{-}\",\ze(\s*)actions)|actions\.\w+(\(.{-}\))?,?|\{\ desc\ \=`
keymaps = {
disable_defaults = false, -- Disable the default keymaps
view = {
-- The `view` bindings are active in the diff buffers, only when the current
-- tabpage is a Diffview.
{ "n", "<tab>", actions.select_next_entry, { desc = "Open the diff for the next file" } },
{ "n", "<s-tab>", actions.select_prev_entry, { desc = "Open the diff for the previous file" } },
{ "n", "gf", actions.goto_file_edit, { desc = "Open the file in the previous tabpage" } },
{ "n", "<C-w><C-f>", actions.goto_file_split, { desc = "Open the file in a new split" } },
{ "n", "<C-w>gf", actions.goto_file_tab, { desc = "Open the file in a new tabpage" } },
{ "n", "<leader>e", actions.focus_files, { desc = "Bring focus to the file panel" } },
{ "n", "<leader>b", actions.toggle_files, { desc = "Toggle the file panel." } },
{ "n", "g<C-x>", actions.cycle_layout, { desc = "Cycle through available layouts." } },
{ "n", "[x", actions.prev_conflict, { desc = "In the merge-tool: jump to the previous conflict" } },
{ "n", "]x", actions.next_conflict, { desc = "In the merge-tool: jump to the next conflict" } },
{ "n", "<leader>co", actions.conflict_choose("ours"), { desc = "Choose the OURS version of a conflict" } },
{ "n", "<leader>ct", actions.conflict_choose("theirs"), { desc = "Choose the THEIRS version of a conflict" } },
{ "n", "<leader>cb", actions.conflict_choose("base"), { desc = "Choose the BASE version of a conflict" } },
{ "n", "<leader>ca", actions.conflict_choose("all"), { desc = "Choose all the versions of a conflict" } },
{ "n", "dx", actions.conflict_choose("none"), { desc = "Delete the conflict region" } },
{ "n", "<leader>cO", actions.conflict_choose_all("ours"), { desc = "Choose the OURS version of a conflict for the whole file" } },
{ "n", "<leader>cT", actions.conflict_choose_all("theirs"), { desc = "Choose the THEIRS version of a conflict for the whole file" } },
{ "n", "<leader>cB", actions.conflict_choose_all("base"), { desc = "Choose the BASE version of a conflict for the whole file" } },
{ "n", "<leader>cA", actions.conflict_choose_all("all"), { desc = "Choose all the versions of a conflict for the whole file" } },
{ "n", "dX", actions.conflict_choose_all("none"), { desc = "Delete the conflict region for the whole file" } },
unpack(actions.compat.fold_cmds),
},
diff1 = {
-- Mappings in single window diff layouts
{ "n", "g?", actions.help({ "view", "diff1" }), { desc = "Open the help panel" } },
},
diff2 = {
-- Mappings in 2-way diff layouts
{ "n", "g?", actions.help({ "view", "diff2" }), { desc = "Open the help panel" } },
},
diff3 = {
-- Mappings in 3-way diff layouts
{ { "n", "x" }, "2do", actions.diffget("ours"), { desc = "Obtain the diff hunk from the OURS version of the file" } },
{ { "n", "x" }, "3do", actions.diffget("theirs"), { desc = "Obtain the diff hunk from the THEIRS version of the file" } },
{ "n", "g?", actions.help({ "view", "diff3" }), { desc = "Open the help panel" } },
},
diff4 = {
-- Mappings in 4-way diff layouts
{ { "n", "x" }, "1do", actions.diffget("base"), { desc = "Obtain the diff hunk from the BASE version of the file" } },
{ { "n", "x" }, "2do", actions.diffget("ours"), { desc = "Obtain the diff hunk from the OURS version of the file" } },
{ { "n", "x" }, "3do", actions.diffget("theirs"), { desc = "Obtain the diff hunk from the THEIRS version of the file" } },
{ "n", "g?", actions.help({ "view", "diff4" }), { desc = "Open the help panel" } },
},
file_panel = {
{ "n", "j", actions.next_entry, { desc = "Bring the cursor to the next file entry" } },
{ "n", "<down>", actions.next_entry, { desc = "Bring the cursor to the next file entry" } },
{ "n", "k", actions.prev_entry, { desc = "Bring the cursor to the previous file entry" } },
{ "n", "<up>", actions.prev_entry, { desc = "Bring the cursor to the previous file entry" } },
{ "n", "<cr>", actions.select_entry, { desc = "Open the diff for the selected entry" } },
{ "n", "o", actions.select_entry, { desc = "Open the diff for the selected entry" } },
{ "n", "l", actions.select_entry, { desc = "Open the diff for the selected entry" } },
{ "n", "<2-LeftMouse>", actions.select_entry, { desc = "Open the diff for the selected entry" } },
{ "n", "-", actions.toggle_stage_entry, { desc = "Stage / unstage the selected entry" } },
{ "n", "s", actions.toggle_stage_entry, { desc = "Stage / unstage the selected entry" } },
{ "n", "S", actions.stage_all, { desc = "Stage all entries" } },
{ "n", "U", actions.unstage_all, { desc = "Unstage all entries" } },
{ "n", "X", actions.restore_entry, { desc = "Restore entry to the state on the left side" } },
{ "n", "L", actions.open_commit_log, { desc = "Open the commit log panel" } },
{ "n", "zo", actions.open_fold, { desc = "Expand fold" } },
{ "n", "h", actions.close_fold, { desc = "Collapse fold" } },
{ "n", "zc", actions.close_fold, { desc = "Collapse fold" } },
{ "n", "za", actions.toggle_fold, { desc = "Toggle fold" } },
{ "n", "zR", actions.open_all_folds, { desc = "Expand all folds" } },
{ "n", "zM", actions.close_all_folds, { desc = "Collapse all folds" } },
{ "n", "<c-b>", actions.scroll_view(-0.25), { desc = "Scroll the view up" } },
{ "n", "<c-f>", actions.scroll_view(0.25), { desc = "Scroll the view down" } },
{ "n", "<tab>", actions.select_next_entry, { desc = "Open the diff for the next file" } },
{ "n", "<s-tab>", actions.select_prev_entry, { desc = "Open the diff for the previous file" } },
{ "n", "gf", actions.goto_file_edit, { desc = "Open the file in the previous tabpage" } },
{ "n", "<C-w><C-f>", actions.goto_file_split, { desc = "Open the file in a new split" } },
{ "n", "<C-w>gf", actions.goto_file_tab, { desc = "Open the file in a new tabpage" } },
{ "n", "i", actions.listing_style, { desc = "Toggle between 'list' and 'tree' views" } },
{ "n", "f", actions.toggle_flatten_dirs, { desc = "Flatten empty subdirectories in tree listing style" } },
{ "n", "R", actions.refresh_files, { desc = "Update stats and entries in the file list" } },
{ "n", "<leader>e", actions.focus_files, { desc = "Bring focus to the file panel" } },
{ "n", "<leader>b", actions.toggle_files, { desc = "Toggle the file panel" } },
{ "n", "g<C-x>", actions.cycle_layout, { desc = "Cycle available layouts" } },
{ "n", "[x", actions.prev_conflict, { desc = "Go to the previous conflict" } },
{ "n", "]x", actions.next_conflict, { desc = "Go to the next conflict" } },
{ "n", "g?", actions.help("file_panel"), { desc = "Open the help panel" } },
{ "n", "<leader>cO", actions.conflict_choose_all("ours"), { desc = "Choose the OURS version of a conflict for the whole file" } },
{ "n", "<leader>cT", actions.conflict_choose_all("theirs"), { desc = "Choose the THEIRS version of a conflict for the whole file" } },
{ "n", "<leader>cB", actions.conflict_choose_all("base"), { desc = "Choose the BASE version of a conflict for the whole file" } },
{ "n", "<leader>cA", actions.conflict_choose_all("all"), { desc = "Choose all the versions of a conflict for the whole file" } },
{ "n", "dX", actions.conflict_choose_all("none"), { desc = "Delete the conflict region for the whole file" } },
},
file_history_panel = {
{ "n", "g!", actions.options, { desc = "Open the option panel" } },
{ "n", "<C-A-d>", actions.open_in_diffview, { desc = "Open the entry under the cursor in a diffview" } },
{ "n", "y", actions.copy_hash, { desc = "Copy the commit hash of the entry under the cursor" } },
{ "n", "L", actions.open_commit_log, { desc = "Show commit details" } },
{ "n", "X", actions.restore_entry, { desc = "Restore file to the state from the selected entry" } },
{ "n", "zo", actions.open_fold, { desc = "Expand fold" } },
{ "n", "zc", actions.close_fold, { desc = "Collapse fold" } },
{ "n", "h", actions.close_fold, { desc = "Collapse fold" } },
{ "n", "za", actions.toggle_fold, { desc = "Toggle fold" } },
{ "n", "zR", actions.open_all_folds, { desc = "Expand all folds" } },
{ "n", "zM", actions.close_all_folds, { desc = "Collapse all folds" } },
{ "n", "j", actions.next_entry, { desc = "Bring the cursor to the next file entry" } },
{ "n", "<down>", actions.next_entry, { desc = "Bring the cursor to the next file entry" } },
{ "n", "k", actions.prev_entry, { desc = "Bring the cursor to the previous file entry" } },
{ "n", "<up>", actions.prev_entry, { desc = "Bring the cursor to the previous file entry" } },
{ "n", "<cr>", actions.select_entry, { desc = "Open the diff for the selected entry" } },
{ "n", "o", actions.select_entry, { desc = "Open the diff for the selected entry" } },
{ "n", "l", actions.select_entry, { desc = "Open the diff for the selected entry" } },
{ "n", "<2-LeftMouse>", actions.select_entry, { desc = "Open the diff for the selected entry" } },
{ "n", "<c-b>", actions.scroll_view(-0.25), { desc = "Scroll the view up" } },
{ "n", "<c-f>", actions.scroll_view(0.25), { desc = "Scroll the view down" } },
{ "n", "<tab>", actions.select_next_entry, { desc = "Open the diff for the next file" } },
{ "n", "<s-tab>", actions.select_prev_entry, { desc = "Open the diff for the previous file" } },
{ "n", "gf", actions.goto_file_edit, { desc = "Open the file in the previous tabpage" } },
{ "n", "<C-w><C-f>", actions.goto_file_split, { desc = "Open the file in a new split" } },
{ "n", "<C-w>gf", actions.goto_file_tab, { desc = "Open the file in a new tabpage" } },
{ "n", "<leader>e", actions.focus_files, { desc = "Bring focus to the file panel" } },
{ "n", "<leader>b", actions.toggle_files, { desc = "Toggle the file panel" } },
{ "n", "g<C-x>", actions.cycle_layout, { desc = "Cycle available layouts" } },
{ "n", "g?", actions.help("file_history_panel"), { desc = "Open the help panel" } },
},
option_panel = {
{ "n", "<tab>", actions.select_entry, { desc = "Change the current option" } },
{ "n", "q", actions.close, { desc = "Close the panel" } },
{ "n", "g?", actions.help("option_panel"), { desc = "Open the help panel" } },
},
help_panel = {
{ "n", "q", actions.close, { desc = "Close help menu" } },
{ "n", "<esc>", actions.close, { desc = "Close help menu" } },
},
},
}
-- stylua: ignore end
---@type EventEmitter
M.user_emitter = EventEmitter()
M._config = M.defaults
---@class GitLogOptions
---@field follow boolean
---@field first_parent boolean
---@field show_pulls boolean
---@field reflog boolean
---@field walk_reflogs boolean
---@field all boolean
---@field merges boolean
---@field no_merges boolean
---@field reverse boolean
---@field cherry_pick boolean
---@field left_only boolean
---@field right_only boolean
---@field max_count integer
---@field L string[]
---@field author string
---@field grep string
---@field G string
---@field S string
---@field diff_merges string
---@field rev_range string
---@field base string
---@field path_args string[]
---@field after string
---@field before string
---@class HgLogOptions
---@field follow string
---@field limit integer
---@field user string
---@field no_merges boolean
---@field rev string
---@field keyword string
---@field branch string
---@field bookmark string
---@field include string
---@field exclude string
---@field path_args string[]
---@alias LogOptions GitLogOptions|HgLogOptions
M.log_option_defaults = {
---@type GitLogOptions
git = {
follow = false,
first_parent = false,
show_pulls = false,
reflog = false,
walk_reflogs = false,
all = false,
merges = false,
no_merges = false,
reverse = false,
cherry_pick = false,
left_only = false,
right_only = false,
rev_range = nil,
base = nil,
max_count = 256,
L = {},
diff_merges = nil,
author = nil,
grep = nil,
G = nil,
S = nil,
path_args = {},
},
---@type HgLogOptions
hg = {
limit = 256,
user = nil,
no_merges = false,
rev = nil,
keyword = nil,
include = nil,
exclude = nil,
},
}
---@return DiffviewConfig
function M.get_config()
if not setup_done then
M.setup()
end
return M._config
end
---@param single_file boolean
---@param t GitLogOptions|HgLogOptions
---@param vcs "git"|"hg"
---@return GitLogOptions|HgLogOptions
function M.get_log_options(single_file, t, vcs)
local log_options
if single_file then
log_options = M._config.file_history_panel.log_options[vcs].single_file
else
log_options = M._config.file_history_panel.log_options[vcs].multi_file
end
if t then
log_options = vim.tbl_extend("force", log_options, t)
for k, _ in pairs(log_options) do
if t[k] == "" then
log_options[k] = nil
end
end
end
return log_options
end
---@alias LayoutName "diff1_plain"
--- | "diff2_horizontal"
--- | "diff2_vertical"
--- | "diff3_horizontal"
--- | "diff3_vertical"
--- | "diff3_mixed"
--- | "diff4_mixed"
local layout_map = {
diff1_plain = Diff1,
diff2_horizontal = Diff2Hor,
diff2_vertical = Diff2Ver,
diff3_horizontal = Diff3Hor,
diff3_vertical = Diff3Ver,
diff3_mixed = Diff3Mixed,
diff4_mixed = Diff4Mixed,
}
---@param layout_name LayoutName
---@return Layout
function M.name_to_layout(layout_name)
assert(layout_map[layout_name], "Invalid layout name: " .. layout_name)
return layout_map[layout_name].__get()
end
---@param layout Layout
---@return table?
function M.get_layout_keymaps(layout)
if layout:instanceof(Diff1.__get()) then
return M._config.keymaps.diff1
elseif layout:instanceof(Diff2.__get()) then
return M._config.keymaps.diff2
elseif layout:instanceof(Diff3.__get()) then
return M._config.keymaps.diff3
elseif layout:instanceof(Diff4.__get()) then
return M._config.keymaps.diff4
end
end
function M.find_option_keymap(t)
for _, mapping in ipairs(t) do
if mapping[3] and mapping[3] == actions.options then
return mapping
end
end
end
function M.find_help_keymap(t)
for _, mapping in ipairs(t) do
if type(mapping[4]) == "table" and mapping[4].desc == "Open the help panel" then
return mapping
end
end
end
---@param values vector
---@param no_quote? boolean
---@return string
local function fmt_enum(values, no_quote)
return table.concat(vim.tbl_map(function(v)
return (not no_quote and type(v) == "string") and ("'" .. v .. "'") or v
end, values), "|")
end
---@param ... table
---@return table
function M.extend_keymaps(...)
local argc = select("#", ...)
local argv = { ... }
local contexts = {}
for i = 1, argc do
local cur = argv[i]
if type(cur) == "table" then
contexts[#contexts + 1] = { subject = cur, expanded = {} }
end
end
for _, ctx in ipairs(contexts) do
-- Expand the normal mode maps
for lhs, rhs in pairs(ctx.subject) do
if type(lhs) == "string" then
ctx.expanded["n " .. lhs] = {
"n",
lhs,
rhs,
{ silent = true, nowait = true },
}
end
end
for _, map in ipairs(ctx.subject) do
for _, mode in ipairs(type(map[1]) == "table" and map[1] or { map[1] }) do
ctx.expanded[mode .. " " .. map[2]] = utils.vec_join(
mode,
map[2],
utils.vec_slice(map, 3)
)
end
end
end
local merged = vim.tbl_extend("force", unpack(
vim.tbl_map(function(v)
return v.expanded
end, contexts)
))
return vim.tbl_values(merged)
end
function M.setup(user_config)
user_config = user_config or {}
M._config = vim.tbl_deep_extend(
"force",
utils.tbl_deep_clone(M.defaults),
user_config
)
---@type EventEmitter
M.user_emitter = EventEmitter()
--#region DEPRECATION NOTICES
if type(M._config.file_panel.use_icons) ~= "nil" then
utils.warn("'file_panel.use_icons' has been deprecated. See ':h diffview.changelog-64'.")
end
-- Move old panel preoperties to win_config
local old_win_config_spec = { "position", "width", "height" }
for _, panel_name in ipairs({ "file_panel", "file_history_panel" }) do
local panel_config = M._config[panel_name]
---@cast panel_config table
local notified = false
for _, option in ipairs(old_win_config_spec) do
if panel_config[option] ~= nil then
if not notified then
utils.warn(
("'%s.{%s}' has been deprecated. See ':h diffview.changelog-136'.")
:format(panel_name, fmt_enum(old_win_config_spec, true))
)
notified = true
end
panel_config.win_config[option] = panel_config[option]
panel_config[option] = nil
end
end
end
-- Move old keymaps
if user_config.key_bindings then
M._config.keymaps = vim.tbl_deep_extend("force", M._config.keymaps, user_config.key_bindings)
user_config.keymaps = user_config.key_bindings
M._config.key_bindings = nil
end
local user_log_options = utils.tbl_access(user_config, "file_history_panel.log_options")
if user_log_options then
local top_options = {
"single_file",
"multi_file",
}
for _, name in ipairs(top_options) do
if user_log_options[name] ~= nil then
utils.warn("Global config of 'file_panel.log_options' has been deprecated. See ':h diffview.changelog-271'.")
end
break
end
local option_names = {
"max_count",
"follow",
"all",
"merges",
"no_merges",
"reverse",
}
for _, name in ipairs(option_names) do
if user_log_options[name] ~= nil then
utils.warn(
("'file_history_panel.log_options.{%s}' has been deprecated. See ':h diffview.changelog-151'.")
:format(fmt_enum(option_names, true))
)
break
end
end
end
--#endregion
if #M._config.git_cmd == 0 then
M._config.git_cmd = M.defaults.git_cmd
end
do
-- Validate layouts
local view = M._config.view
local standard_layouts = { "diff2_horizontal", "diff2_vertical", -1 }
local merge_layuots = {
"diff1_plain",
"diff3_horizontal",
"diff3_vertical",
"diff3_mixed",
"diff4_mixed",
-1
}
local valid_layouts = {
default = standard_layouts,
merge_tool = merge_layuots,
file_history = standard_layouts,
}
for _, kind in ipairs(vim.tbl_keys(valid_layouts)) do
if not vim.tbl_contains(valid_layouts[kind], view[kind].layout) then
utils.err(("Invalid layout name '%s' for 'view.%s'! Must be one of (%s)."):format(
view[kind].layout,
kind,
fmt_enum(valid_layouts[kind])
))
view[kind].layout = M.defaults.view[kind].layout
end
end
end
for _, name in ipairs({ "single_file", "multi_file" }) do
for _, vcs in ipairs({ "git", "hg" }) do
local t = M._config.file_history_panel.log_options[vcs]
t[name] = vim.tbl_extend(
"force",
M.log_option_defaults[vcs],
t[name]
)
for k, _ in pairs(t[name]) do
if t[name][k] == "" then
t[name][k] = nil
end
end
end
end
for event, callback in pairs(M._config.hooks) do
if type(callback) == "function" then
M.user_emitter:on(event, function (_, ...)
callback(...)
end)
end
end
if M._config.keymaps.disable_defaults then
for name, _ in pairs(M._config.keymaps) do
if name ~= "disable_defaults" then
M._config.keymaps[name] = utils.tbl_access(user_config, { "keymaps", name }) or {}
end
end
else
M._config.keymaps = utils.tbl_clone(M.defaults.keymaps)
end
-- Merge default and user keymaps
for name, keymap in pairs(M._config.keymaps) do
if type(name) == "string" and type(keymap) == "table" then
M._config.keymaps[name] = M.extend_keymaps(
keymap,
utils.tbl_access(user_config, { "keymaps", name }) or {}
)
end
end
-- Disable keymaps set to `false`
for name, keymaps in pairs(M._config.keymaps) do
if type(name) == "string" and type(keymaps) == "table" then
for i = #keymaps, 1, -1 do
local v = keymaps[i]
if type(v) == "table" and not v[3] then
table.remove(keymaps, i)
end
end
end
end
setup_done = true
end
M.actions = actions
return M

View File

@ -0,0 +1,294 @@
local lazy = require("diffview.lazy")
local oop = require("diffview.oop")
local async = require("diffview.async")
local utils = lazy.require("diffview.utils") ---@module "diffview.utils"
local await = async.await
local M = {}
---@class Condvar : Waitable
---@operator call : Condvar
local Condvar = oop.create_class("Condvar", async.Waitable)
M.Condvar = Condvar
function Condvar:init()
self.handles = {}
end
---@override
Condvar.await = async.sync_wrap(function(self, callback)
table.insert(self.handles, callback)
end, 2)
function Condvar:notify_all()
local len = #self.handles
for i, cb in ipairs(self.handles) do
if i > len then break end
cb()
end
if #self.handles > len then
self.handles = utils.vec_slice(self.handles, len + 1)
else
self.handles = {}
end
end
---@class SignalConsumer : Waitable
---@operator call : SignalConsumer
---@field package parent Signal
local SignalConsumer = oop.create_class("SignalConsumer", async.Waitable)
function SignalConsumer:init(parent)
self.parent = parent
end
---@override
---@param self SignalConsumer
SignalConsumer.await = async.sync_void(function(self)
await(self.parent)
end)
---Check if the signal has been emitted.
---@return boolean
function SignalConsumer:check()
return self.parent:check()
end
---Listen for the signal to be emitted. If the signal has already been emitted,
---the callback is invoked immediately. The callback can potentially be called
---multiple times if the signal is reset between emissions.
---@see Signal.reset
---@param callback fun(signal: Signal)
function SignalConsumer:listen(callback)
self.parent:listen(callback)
end
function SignalConsumer:get_name()
return self.parent:get_name()
end
---@class Signal : SignalConsumer
---@operator call : Signal
---@field package name string
---@field package emitted boolean
---@field package cond Condvar
---@field package listeners (fun(signal: Signal))[]
local Signal = oop.create_class("Signal", async.Waitable)
M.Signal = Signal
function Signal:init(name)
self.name = name or "UNNAMED_SIGNAL"
self.emitted = false
self.cond = Condvar()
self.listeners = {}
end
---@override
---@param self Signal
Signal.await = async.sync_void(function(self)
if self.emitted then return end
await(self.cond)
end)
---Send the signal.
function Signal:send()
if self.emitted then return end
self.emitted = true
for _, listener in ipairs(self.listeners) do
listener(self)
end
self.cond:notify_all()
end
---Listen for the signal to be emitted. If the signal has already been emitted,
---the callback is invoked immediately. The callback can potentially be called
---multiple times if the signal is reset between emissions.
---@see Signal.reset
---@param callback fun(signal: Signal)
function Signal:listen(callback)
self.listeners[#self.listeners + 1] = callback
if self.emitted then callback(self) end
end
---@return SignalConsumer
function Signal:new_consumer()
return SignalConsumer(self)
end
---Check if the signal has been emitted.
---@return boolean
function Signal:check()
return self.emitted
end
---Reset the signal such that it can be sent again.
function Signal:reset()
self.emitted = false
end
function Signal:get_name()
return self.name
end
---@class WorkPool : Waitable
---@operator call : WorkPool
---@field package workers table<Signal, boolean>
local WorkPool = oop.create_class("WorkPool", async.Waitable)
M.WorkPool = WorkPool
function WorkPool:init()
self.workers = {}
end
---Check in a worker. Returns a "checkout" signal that must be used to resolve
---the work.
---@return Signal checkout
function WorkPool:check_in()
local signal = Signal()
self.workers[signal] = true
signal:listen(function()
self.workers[signal] = nil
end)
return signal
end
function WorkPool:size()
return #vim.tbl_keys(self.workers)
end
---Wait for all workers to resolve and check out.
---@override
---@param self WorkPool
WorkPool.await = async.sync_void(function(self)
local cur = next(self.workers)
while cur do
self.workers[cur] = nil
await(cur)
cur = next(self.workers)
end
end)
---@class Permit : diffview.Object
---@operator call : Permit
---@field parent Semaphore
local Permit = oop.create_class("Permit")
function Permit:init(opt)
self.parent = opt.parent
end
function Permit:destroy()
self.parent = nil
end
---@param self Permit
function Permit:forget()
if self.parent then
local parent = self.parent
self:destroy()
parent:forget_one()
end
end
---@class Semaphore : diffview.Object
---@operator call : Semaphore
---@field initial_count integer
---@field permit_count integer
---@field queue fun(p: Permit)[]
local Semaphore = oop.create_class("Semaphore")
M.Semaphore = Semaphore
function Semaphore:init(permit_count)
assert(permit_count)
self.initial_count = permit_count
self.permit_count = permit_count
self.queue = {}
end
function Semaphore:forget_one()
if self.permit_count == self.initial_count then return end
if next(self.queue) then
local next_contractee = table.remove(self.queue, 1)
next_contractee(Permit({ parent = self }))
else
self.permit_count = self.permit_count + 1
end
end
---@param self Semaphore
---@param callback fun(permit: Permit)
Semaphore.acquire = async.wrap(function(self, callback)
if self.permit_count <= 0 then
table.insert(self.queue, callback)
return
end
self.permit_count = self.permit_count - 1
return callback(Permit({ parent = self }))
end)
---@class CountDownLatch : Waitable
---@operator call : CountDownLatch
---@field initial_count integer
---@field counter integer
---@field sem Semaphore
---@field condvar Condvar
---@field count_down fun(self: CountDownLatch)
local CountDownLatch = oop.create_class("CountDownLatch", async.Waitable)
M.CountDownLatch = CountDownLatch
function CountDownLatch:init(count)
self.initial_count = count
self.counter = count
self.sem = Semaphore(1)
self.condvar = Condvar()
end
function CountDownLatch:count_down()
local permit = await(self.sem:acquire()) --[[@as Permit ]]
if self.counter == 0 then
-- The counter reached 0 while we were waiting for the permit
permit:forget()
return
end
self.counter = self.counter - 1
permit:forget()
if self.counter == 0 then
self.condvar:notify_all()
end
end
---@override
function CountDownLatch:await()
if self.counter == 0 then return end
await(self.condvar)
end
function CountDownLatch:reset()
local permit = await(self.sem:acquire()) --[[@as Permit ]]
self.counter = self.initial_count
permit:forget()
self.condvar:notify_all()
end
return M

View File

@ -0,0 +1,246 @@
local async = require("diffview.async")
local utils = require("diffview.utils")
local await = async.await
local uv = vim.loop
local M = {}
---@class Closeable
---@field close fun() # Perform cleanup and release the associated handle.
---@class ManagedFn : Closeable
---@operator call : unknown ...
---@param ... uv_handle_t
function M.try_close(...)
local args = { ... }
for i = 1, select("#", ...) do
local handle = args[i]
if handle and not handle:is_closing() then
handle:close()
end
end
end
---@return ManagedFn
local function wrap(timer, fn)
return setmetatable({}, {
__call = function(_, ...)
fn(...)
end,
__index = {
close = function()
timer:stop()
M.try_close(timer)
end,
},
})
end
---Debounces a function on the leading edge.
---@param ms integer Timeout in ms
---@param fn function Function to debounce
---@return ManagedFn # Debounced function.
function M.debounce_leading(ms, fn)
local timer = assert(uv.new_timer())
local lock = false
return wrap(timer, function(...)
timer:start(ms, 0, function()
timer:stop()
lock = false
end)
if not lock then
lock = true
fn(...)
end
end)
end
---Debounces a function on the trailing edge.
---@param ms integer Timeout in ms
---@param rush_first boolean If the managed fn is called and it's not recovering from a debounce: call the fn immediately.
---@param fn function Function to debounce
---@return ManagedFn # Debounced function.
function M.debounce_trailing(ms, rush_first, fn)
local timer = assert(uv.new_timer())
local lock = false
local debounced_fn, args
debounced_fn = wrap(timer, function(...)
if not lock and rush_first and args == nil then
lock = true
fn(...)
else
args = utils.tbl_pack(...)
end
timer:start(ms, 0, function()
lock = false
timer:stop()
if args then
local a = args
args = nil
fn(utils.tbl_unpack(a))
end
end)
end)
return debounced_fn
end
---Throttles a function on the leading edge.
---@param ms integer Timeout in ms
---@param fn function Function to throttle
---@return ManagedFn # throttled function.
function M.throttle_leading(ms, fn)
local timer = assert(uv.new_timer())
local lock = false
return wrap(timer, function(...)
if not lock then
timer:start(ms, 0, function()
lock = false
timer:stop()
end)
lock = true
fn(...)
end
end)
end
---Throttles a function on the trailing edge.
---@param ms integer Timeout in ms
---@param rush_first boolean If the managed fn is called and it's not recovering from a throttle: call the fn immediately.
---@param fn function Function to throttle
---@return ManagedFn # throttled function.
function M.throttle_trailing(ms, rush_first, fn)
local timer = assert(uv.new_timer())
local lock = false
local throttled_fn, args
throttled_fn = wrap(timer, function(...)
if lock or (not rush_first and args == nil) then
args = utils.tbl_pack(...)
end
if lock then return end
lock = true
if rush_first then
fn(...)
end
timer:start(ms, 0, function()
lock = false
if args then
local a = args
args = nil
if rush_first then
throttled_fn(utils.tbl_unpack(a))
else
fn(utils.tbl_unpack(a))
end
end
end)
end)
return throttled_fn
end
---Throttle a function against a target framerate. The function will always be
---called when the editor is unlocked and writing to buffers is possible.
---@param framerate integer # Target framerate. Set to <= 0 to render whenever the scheduler is ready.
---@param fn function
function M.throttle_render(framerate, fn)
local lock = false
local use_framerate = framerate > 0
local period = use_framerate and (1000 / framerate) * 1E6 or 0
local throttled_fn
local args, last
throttled_fn = async.void(function(...)
args = utils.tbl_pack(...)
if lock then return end
lock = true
await(async.schedule_now())
fn(utils.tbl_unpack(args))
args = nil
if use_framerate then
local now = uv.hrtime()
if last and now - last < period then
local wait = period - (now - last)
await(async.timeout(wait / 1E6))
last = last + period
else
last = now
end
end
lock = false
if args ~= nil then
throttled_fn(utils.tbl_unpack(args))
end
end)
return throttled_fn
end
---Repeatedly call `func` with a fixed time delay.
---@param func function
---@param delay integer # Delay between executions (ms)
---@return Closeable
function M.set_interval(func, delay)
local timer = assert(uv.new_timer())
local ret = {
close = function()
timer:stop()
M.try_close(timer)
end,
}
timer:start(delay, delay, function()
local should_close = func()
if type(should_close) == "boolean" and should_close then
ret.close()
end
end)
return ret
end
---Call `func` after a fixed time delay.
---@param func function
---@param delay integer # Delay until execution (ms)
---@return Closeable
function M.set_timeout(func, delay)
local timer = assert(uv.new_timer())
local ret = {
close = function()
timer:stop()
M.try_close(timer)
end,
}
timer:start(delay, 0, function()
func()
ret.close()
end)
return ret
end
return M

View File

@ -0,0 +1,225 @@
--[[
An implementation of Myers' diff algorithm
Derived from: https://github.com/Swatinem/diff
]]
local oop = require("diffview.oop")
local M = {}
---@enum EditToken
local EditToken = oop.enum({
NOOP = 1,
DELETE = 2,
INSERT = 3,
REPLACE = 4,
})
---@class Diff : diffview.Object
---@operator call : Diff
---@field a any[]
---@field b any[]
---@field moda boolean[]
---@field modb boolean[]
---@field up table<integer, integer>
---@field down table<integer, integer>
---@field eql_fn function
local Diff = oop.create_class("Diff")
---Diff constructor.
---@param a any[]
---@param b any[]
---@param eql_fn function|nil
function Diff:init(a, b, eql_fn)
self.a = a
self.b = b
self.moda = {}
self.modb = {}
self.up = {}
self.down = {}
self.eql_fn = eql_fn or function(aa, bb)
return aa == bb
end
for i = 1, #a do
self.moda[i] = false
end
for i = 1, #b do
self.modb[i] = false
end
self:lcs(1, #self.a + 1, 1, #self.b + 1)
end
---@return EditToken[]
function Diff:create_edit_script()
local astart = 1
local bstart = 1
local aend = #self.moda
local bend = #self.modb
local script = {}
while astart <= aend or bstart <= bend do
if astart <= aend and bstart <= bend then
if not self.moda[astart] and not self.modb[bstart] then
table.insert(script, EditToken.NOOP)
astart = astart + 1
bstart = bstart + 1
goto continue
elseif self.moda[astart] and self.modb[bstart] then
table.insert(script, EditToken.REPLACE)
astart = astart + 1
bstart = bstart + 1
goto continue
end
end
if astart <= aend and (bstart > bend or self.moda[astart]) then
table.insert(script, EditToken.DELETE)
astart = astart + 1
end
if bstart <= bend and (astart > aend or self.modb[bstart]) then
table.insert(script, EditToken.INSERT)
bstart = bstart + 1
end
::continue::
end
return script
end
---@private
---@param astart integer
---@param aend integer
---@param bstart integer
---@param bend integer
function Diff:lcs(astart, aend, bstart, bend)
-- separate common head
while astart < aend and bstart < bend and self.eql_fn(self.a[astart], self.b[bstart]) do
astart = astart + 1
bstart = bstart + 1
end
-- separate common tail
while astart < aend and bstart < bend and self.eql_fn(self.a[aend - 1], self.b[bend - 1]) do
aend = aend - 1
bend = bend - 1
end
if astart == aend then
-- only insertions
while bstart < bend do
self.modb[bstart] = true
bstart = bstart + 1
end
elseif bend == bstart then
-- only deletions
while astart < aend do
self.moda[astart] = true
astart = astart + 1
end
else
local snake = self:snake(astart, aend, bstart, bend)
self:lcs(astart, snake.x, bstart, snake.y)
self:lcs(snake.u, aend, snake.v, bend)
end
end
---@class Diff.Snake
---@field x integer
---@field y integer
---@field u integer
---@field v integer
---@private
---@param astart integer
---@param aend integer
---@param bstart integer
---@param bend integer
---@return Diff.Snake
function Diff:snake(astart, aend, bstart, bend)
local N = aend - astart
local MM = bend - bstart
local kdown = astart - bstart
local kup = aend - bend
local delta = N - MM
local deltaOdd = delta % 2 ~= 0
self.down[kdown + 1] = astart
self.up[kup - 1] = aend
local Dmax = (N + MM) / 2 + 1
for D = 0, Dmax do
local x, y
-- Forward path
for k = kdown - D, kdown + D, 2 do
if k == kdown - D then
x = self.down[k + 1] -- down
else
x = self.down[k - 1] + 1 -- right
if k < kdown + D and self.down[k + 1] >= x then
x = self.down[k + 1] -- down
end
end
y = x - k
while x < aend and y < bend and self.eql_fn(self.a[x], self.b[y]) do
x = x + 1
y = y + 1 -- diagonal
end
self.down[k] = x
if deltaOdd and kup - D < k and k < kup + D and self.up[k] <= self.down[k] then
return {
x = self.down[k],
y = self.down[k] - k,
u = self.up[k],
v = self.up[k] - k,
}
end
end
-- Reverse path
for k = kup - D, kup + D, 2 do
if k == kup + D then
x = self.up[k - 1] -- up
else
x = self.up[k + 1] - 1 -- left
if k > kup - D and self.up[k - 1] < x then
x = self.up[k - 1] -- up
end
end
y = x - k
while x > astart and y > bstart and self.eql_fn(self.a[x - 1], self.b[y - 1]) do
x = x - 1
y = y - 1 -- diagonal
end
self.up[k] = x
if not deltaOdd and kdown - D <= k and k <= kdown + D and self.up[k] <= self.down[k] then
return {
x = self.down[k],
y = self.down[k] - k,
u = self.up[k],
v = self.up[k] - k,
}
end
end
end
error("Unexpected state!")
end
M.EditToken = EditToken
M.Diff = Diff
return M

View File

@ -0,0 +1,237 @@
local lazy = require("diffview.lazy")
local oop = require("diffview.oop")
local utils = lazy.require("diffview.utils") ---@module "diffview.utils"
local M = {}
---@enum EventName
local EventName = oop.enum({
FILES_STAGED = 1,
})
---@alias ListenerType "normal"|"once"|"any"|"any_once"
---@alias ListenerCallback (fun(e: Event, ...): boolean?)
---@class Listener
---@field type ListenerType
---@field callback ListenerCallback The original callback
---@field call function
---@class Event : diffview.Object
---@operator call : Event
---@field id any
---@field propagate boolean
local Event = oop.create_class("Event")
function Event:init(opt)
self.id = opt.id
self.propagate = true
end
function Event:stop_propagation()
self.propagate = false
end
---@class EventEmitter : diffview.Object
---@operator call : EventEmitter
---@field event_map table<any, Listener[]> # Registered events mapped to subscribed listeners.
---@field any_listeners Listener[] # Listeners subscribed to all events.
---@field emit_lock table<any, boolean>
local EventEmitter = oop.create_class("EventEmitter")
---EventEmitter constructor.
function EventEmitter:init()
self.event_map = {}
self.any_listeners = {}
self.emit_lock = {}
end
---Subscribe to a given event.
---@param event_id any Event identifier.
---@param callback ListenerCallback
function EventEmitter:on(event_id, callback)
if not self.event_map[event_id] then
self.event_map[event_id] = {}
end
table.insert(self.event_map[event_id], 1, {
type = "normal",
callback = callback,
call = function(event, args)
return callback(event, utils.tbl_unpack(args))
end,
})
end
---Subscribe a one-shot listener to a given event.
---@param event_id any Event identifier.
---@param callback ListenerCallback
function EventEmitter:once(event_id, callback)
if not self.event_map[event_id] then
self.event_map[event_id] = {}
end
local emitted = false
table.insert(self.event_map[event_id], 1, {
type = "once",
callback = callback,
call = function(event, args)
if not emitted then
emitted = true
return callback(event, utils.tbl_unpack(args))
end
end,
})
end
---Add a new any-listener, subscribed to all events.
---@param callback ListenerCallback
function EventEmitter:on_any(callback)
table.insert(self.any_listeners, 1, {
type = "any",
callback = callback,
call = function(event, args)
return callback(event, args)
end,
})
end
---Add a new one-shot any-listener, subscribed to all events.
---@param callback ListenerCallback
function EventEmitter:once_any(callback)
local emitted = false
table.insert(self.any_listeners, 1, {
type = "any_once",
callback = callback,
call = function(event, args)
if not emitted then
emitted = true
return callback(event, utils.tbl_unpack(args))
end
end,
})
end
---Unsubscribe a listener. If no event is given, the listener is unsubscribed
---from all events.
---@param callback function
---@param event_id? any Only unsubscribe listeners from this event.
function EventEmitter:off(callback, event_id)
---@type Listener[][]
local all
if event_id then
all = { self.event_map[event_id] }
else
all = utils.vec_join(
vim.tbl_values(self.event_map),
{ self.any_listeners }
)
end
for _, listeners in ipairs(all) do
local remove = {}
for i, listener in ipairs(listeners) do
if listener.callback == callback then
remove[#remove + 1] = i
end
end
for i = #remove, 1, -1 do
table.remove(listeners, remove[i])
end
end
end
---Clear all listeners for a given event. If no event is given: clear all listeners.
---@param event_id any?
function EventEmitter:clear(event_id)
for e, _ in pairs(self.event_map) do
if event_id == nil or event_id == e then
self.event_map[e] = nil
end
end
end
---@param listeners Listener[]
---@param event Event
---@param args table
---@return Listener[]
local function filter_call(listeners, event, args)
listeners = utils.vec_slice(listeners) --[[@as Listener[] ]]
local result = {}
for i = 1, #listeners do
local cur = listeners[i]
local ret = cur.call(event, args)
local discard = (type(ret) == "boolean" and ret)
or cur.type == "once"
or cur.type == "any_once"
if not discard then result[#result + 1] = cur end
if not event.propagate then
for j = i + 1, #listeners do result[j] = listeners[j] end
break
end
end
return result
end
---Notify all listeners subscribed to a given event.
---@param event_id any Event identifier.
---@param ... any Event callback args.
function EventEmitter:emit(event_id, ...)
if not self.emit_lock[event_id] then
local args = utils.tbl_pack(...)
local e = Event({ id = event_id })
if type(self.event_map[event_id]) == "table" then
self.event_map[event_id] = filter_call(self.event_map[event_id], e, args)
end
if e.propagate then
self.any_listeners = filter_call(self.any_listeners, e, args)
end
end
end
---Non-recursively notify all listeners subscribed to a given event.
---@param event_id any Event identifier.
---@param ... any Event callback args.
function EventEmitter:nore_emit(event_id, ...)
if not self.emit_lock[event_id] then
self.emit_lock[event_id] = true
local args = utils.tbl_pack(...)
local e = Event({ id = event_id })
if type(self.event_map[event_id]) == "table" then
self.event_map[event_id] = filter_call(self.event_map[event_id], e, args)
end
if e.propagate then
self.any_listeners = filter_call(self.any_listeners, e, args)
end
self.emit_lock[event_id] = false
end
end
---Get all listeners subscribed to the given event.
---@param event_id any Event identifier.
---@return Listener[]?
function EventEmitter:get(event_id)
return self.event_map[event_id]
end
M.EventName = EventName
M.Event = Event
M.EventEmitter = EventEmitter
return M

View File

@ -0,0 +1,50 @@
local ffi = require("ffi")
local C = ffi.C
local M = setmetatable({}, { __index = ffi })
local HAS_NVIM_0_9 = vim.fn.has("nvim-0.9") == 1
---Check if the |textlock| is active.
---@return boolean
function M.nvim_is_textlocked()
return C.textlock > 0
end
---Check if the nvim API is locked for any reason.
---See: |api-fast|, |textlock|
---@return boolean
function M.nvim_is_locked()
if vim.in_fast_event() then return true end
if HAS_NVIM_0_9 then
return C.textlock > 0 or C.allbuf_lock > 0 or C.expr_map_lock > 0
end
return C.textlock > 0 or C.allbuf_lock > 0 or C.ex_normal_lock > 0
end
ffi.cdef([[
/// Non-zero when changing text and jumping to another window or editing another buffer is not
/// allowed.
extern int textlock;
/// Non-zero when no buffer name can be changed, no buffer can be deleted and
/// current directory can't be changed. Used for SwapExists et al.
extern int allbuf_lock;
]])
if HAS_NVIM_0_9 then
ffi.cdef([[
/// Running expr mapping, prevent use of ex_normal() and text changes
extern int expr_map_lock;
]])
else
ffi.cdef([[
/// prevent use of ex_normal()
extern int ex_normal_lock;
]])
end
return M

View File

@ -0,0 +1,102 @@
local health = vim.health or require("health")
local fmt = string.format
-- Polyfill deprecated health api
if vim.fn.has("nvim-0.10") ~= 1 then
health = {
start = health.report_start,
ok = health.report_ok,
info = health.report_info,
warn = health.report_warn,
error = health.report_error,
}
end
local M = {}
M.plugin_deps = {
{
name = "nvim-web-devicons",
optional = true,
},
}
---@param cmd string|string[]
---@return string[] stdout
---@return integer code
local function system_list(cmd)
local out = vim.fn.systemlist(cmd)
return out or {}, vim.v.shell_error
end
local function lualib_available(name)
local ok, _ = pcall(require, name)
return ok
end
function M.check()
if vim.fn.has("nvim-0.7") == 0 then
health.error("Diffview.nvim requires Neovim 0.7.0+")
end
-- LuaJIT
if not _G.jit then
health.error("Not running on LuaJIT! Non-JIT Lua runtimes are not officially supported by the plugin. Mileage may vary.")
end
health.start("Checking plugin dependencies")
local missing_essential = false
for _, plugin in ipairs(M.plugin_deps) do
if lualib_available(plugin.name) then
health.ok(plugin.name .. " installed.")
else
if plugin.optional then
health.warn(fmt("Optional dependency '%s' not found.", plugin.name))
else
missing_essential = true
health.error(fmt("Dependency '%s' not found!", plugin.name))
end
end
end
health.start("Checking VCS tools")
;(function()
if missing_essential then
health.warn("Cannot perform checks on external dependencies without all essential plugin dependencies installed!")
return
end
health.info("The plugin requires at least one of the supported VCS tools to be valid.")
local has_valid_adapter = false
local adapter_kinds = {
{ class = require("diffview.vcs.adapters.git").GitAdapter, name = "Git" },
{ class = require("diffview.vcs.adapters.hg").HgAdapter, name = "Mercurial" },
}
for _, kind in ipairs(adapter_kinds) do
local bs = kind.class.bootstrap
if not bs.done then kind.class.run_bootstrap() end
if bs.version_string then
health.ok(fmt("%s found.", kind.name))
end
if bs.ok then
health.ok(fmt("%s is up-to-date. (%s)", kind.name, bs.version_string))
has_valid_adapter = true
else
health.warn(bs.err or (kind.name .. ": Unknown error"))
end
end
if not has_valid_adapter then
health.error("No valid VCS tool was found!")
end
end)()
end
return M

View File

@ -0,0 +1,497 @@
local lazy = require("diffview.lazy")
local config = lazy.require("diffview.config") ---@module "diffview.config"
local utils = lazy.require("diffview.utils") ---@module "diffview.utils"
local api = vim.api
local web_devicons
local icon_cache = {}
local M = {}
---@alias hl.HiValue<T> T|"NONE"
---@class hl.HiSpec
---@field fg hl.HiValue<string>
---@field bg hl.HiValue<string>
---@field sp hl.HiValue<string>
---@field style hl.HiValue<string>
---@field ctermfg hl.HiValue<integer>
---@field ctermbg hl.HiValue<integer>
---@field cterm hl.HiValue<string>
---@field blend hl.HiValue<integer>
---@field default hl.HiValue<boolean> Only set values if the hl group is cleared.
---@field link string|-1
---@field explicit boolean All undefined fields will be cleared from the hl group.
---@class hl.HiLinkSpec
---@field force boolean
---@field default boolean
---@field clear boolean
---@class hl.HlData
---@field link string|integer
---@field fg integer Foreground color integer
---@field bg integer Background color integer
---@field sp integer Special color integer
---@field x_fg string Foreground color hex string
---@field x_bg string Bakground color hex string
---@field x_sp string Special color hex string
---@field bold boolean
---@field italic boolean
---@field underline boolean
---@field underlineline boolean
---@field undercurl boolean
---@field underdash boolean
---@field underdot boolean
---@field strikethrough boolean
---@field standout boolean
---@field reverse boolean
---@field blend integer
---@field default boolean
---@alias hl.HlAttrValue integer|boolean
local HAS_NVIM_0_8 = vim.fn.has("nvim-0.8") == 1
local HAS_NVIM_0_9 = vim.fn.has("nvim-0.9") == 1
---@enum HlAttribute
M.HlAttribute = {
fg = 1,
bg = 2,
sp = 3,
x_fg = 4,
x_bg = 5,
x_sp = 6,
bold = 7,
italic = 8,
underline = 9,
underlineline = 10,
undercurl = 11,
underdash = 12,
underdot = 13,
strikethrough = 14,
standout = 15,
reverse = 16,
blend = 17,
}
local style_attrs = {
"bold",
"italic",
"underline",
"underlineline",
"undercurl",
"underdash",
"underdot",
"strikethrough",
"standout",
"reverse",
}
-- NOTE: Some atrtibutes have been renamed in v0.8.0
if HAS_NVIM_0_8 then
M.HlAttribute.underdashed = M.HlAttribute.underdash
M.HlAttribute.underdash = nil
M.HlAttribute.underdotted = M.HlAttribute.underdot
M.HlAttribute.underdot = nil
M.HlAttribute.underdouble = M.HlAttribute.underlineline
M.HlAttribute.underlineline = nil
style_attrs = {
"bold",
"italic",
"underline",
"underdouble",
"undercurl",
"underdashed",
"underdotted",
"strikethrough",
"standout",
"reverse",
}
end
utils.add_reverse_lookup(M.HlAttribute)
utils.add_reverse_lookup(style_attrs)
local hlattr = M.HlAttribute
---@param name string Syntax group name.
---@param no_trans? boolean Don't translate the syntax group (follow links).
---@return hl.HlData?
function M.get_hl(name, no_trans)
local hl
if no_trans then
if HAS_NVIM_0_9 then
hl = api.nvim_get_hl(0, { name = name, link = true })
else
hl = api.nvim__get_hl_defs(0)[name]
end
else
local id = api.nvim_get_hl_id_by_name(name)
if id then
if HAS_NVIM_0_9 then
hl = api.nvim_get_hl(0, { id = id, link = false })
else
hl = api.nvim_get_hl_by_id(id, true)
end
end
end
if hl then
if not HAS_NVIM_0_9 then
-- Handle renames
if hl.foreground then hl.fg = hl.foreground; hl.foreground = nil end
if hl.background then hl.bg = hl.background; hl.background = nil end
if hl.special then hl.sp = hl.special; hl.special = nil end
end
if hl.fg then hl.x_fg = string.format("#%06x", hl.fg) end
if hl.bg then hl.x_bg = string.format("#%06x", hl.bg) end
if hl.sp then hl.x_sp = string.format("#%06x", hl.sp) end
return hl
end
end
---@param name string Syntax group name.
---@param attr HlAttribute|string Attribute kind.
---@param no_trans? boolean Don't translate the syntax group (follow links).
---@return hl.HlAttrValue?
function M.get_hl_attr(name, attr, no_trans)
local hl = M.get_hl(name, no_trans)
if type(attr) == "string" then attr = hlattr[attr] end
if not (hl and attr) then return end
return hl[hlattr[attr]]
end
---@param groups string|string[] Syntax group name, or an ordered list of
---groups where the first found value will be returned.
---@param no_trans? boolean Don't translate the syntax group (follow links).
---@return string?
function M.get_fg(groups, no_trans)
no_trans = not not no_trans
if type(groups) ~= "table" then groups = { groups } end
for _, group in ipairs(groups) do
local v = M.get_hl_attr(group, hlattr.x_fg, no_trans) --[[@as string? ]]
if v then return v end
end
end
---@param groups string|string[] Syntax group name, or an ordered list of
---groups where the first found value will be returned.
---@param no_trans? boolean Don't translate the syntax group (follow links).
---@return string?
function M.get_bg(groups, no_trans)
no_trans = not not no_trans
if type(groups) ~= "table" then groups = { groups } end
for _, group in ipairs(groups) do
local v = M.get_hl_attr(group, hlattr.x_bg, no_trans) --[[@as string? ]]
if v then return v end
end
end
---@param groups string|string[] Syntax group name, or an ordered list of
---groups where the first found value will be returned.
---@param no_trans? boolean Don't translate the syntax group (follow links).
---@return string?
function M.get_style(groups, no_trans)
no_trans = not not no_trans
if type(groups) ~= "table" then groups = { groups } end
for _, group in ipairs(groups) do
local hl = M.get_hl(group, no_trans)
if hl then
local res = {}
for _, attr in ipairs(style_attrs) do
if hl[attr] then table.insert(res, attr)
end
if #res > 0 then
return table.concat(res, ",")
end
end
end
end
end
---@param spec hl.HiSpec
---@return hl.HlData
function M.hi_spec_to_def_map(spec)
---@type hl.HlData
local res = {}
local fields = { "fg", "bg", "sp", "ctermfg", "ctermbg", "default", "link" }
for _, field in ipairs(fields) do
res[field] = spec[field]
end
if spec.style then
local spec_attrs = utils.add_reverse_lookup(vim.split(spec.style, ","))
for _, attr in ipairs(style_attrs) do
res[attr] = spec_attrs[attr] ~= nil
end
end
return res
end
---@param groups string|string[] Syntax group name or a list of group names.
---@param opt hl.HiSpec
function M.hi(groups, opt)
if type(groups) ~= "table" then groups = { groups } end
for _, group in ipairs(groups) do
local def_spec
if opt.explicit then
def_spec = M.hi_spec_to_def_map(opt)
else
def_spec = M.hi_spec_to_def_map(
vim.tbl_extend("force", M.get_hl(group, true) or {}, opt)
)
end
for k, v in pairs(def_spec) do
if v == "NONE" then
def_spec[k] = nil
end
end
if not HAS_NVIM_0_9 and def_spec.link then
-- Pre 0.9 `nvim_set_hl()` could not set other attributes in combination
-- with `link`. Furthermore, setting non-link attributes would clear the
-- link, but this does *not* happen if you set the other attributes first
-- (???). However, if the value of `link` is `-1`, the group will be
-- cleared regardless (?????).
local link = def_spec.link
def_spec.link = nil
if not def_spec.default then
api.nvim_set_hl(0, group, def_spec)
end
if link ~= -1 then
api.nvim_set_hl(0, group, { link = link, default = def_spec.default })
end
else
api.nvim_set_hl(0, group, def_spec)
end
end
end
---@param from string|string[] Syntax group name or a list of group names.
---@param to? string Syntax group name. (default: `"NONE"`)
---@param opt? hl.HiLinkSpec
function M.hi_link(from, to, opt)
if to and tostring(to):upper() == "NONE" then
---@diagnostic disable-next-line: cast-local-type
to = -1
end
opt = vim.tbl_extend("keep", opt or {}, {
force = true,
}) --[[@as hl.HiLinkSpec ]]
if type(from) ~= "table" then from = { from } end
for _, f in ipairs(from) do
if opt.clear then
if not HAS_NVIM_0_9 then
-- Pre 0.9 `nvim_set_hl()` did not clear other attributes when `link` was set.
api.nvim_set_hl(0, f, {})
end
api.nvim_set_hl(0, f, { default = opt.default, link = to })
else
-- When `clear` is not set; use our `hi()` function such that other
-- attributes are not affected.
M.hi(f, { default = opt.default, link = to })
end
end
end
---Clear highlighting for a given syntax group, or all groups if no group is
---given.
---@param groups? string|string[]
function M.hi_clear(groups)
if not groups then
vim.cmd("hi clear")
return
end
if type(groups) ~= "table" then
groups = { groups }
end
for _, g in ipairs(groups) do
api.nvim_set_hl(0, g, {})
end
end
function M.get_file_icon(name, ext, render_data, line_idx, offset)
if not config.get_config().use_icons then return "" end
if not web_devicons then
local ok
ok, web_devicons = pcall(require, "nvim-web-devicons")
if not ok then
config.get_config().use_icons = false
utils.warn(
"nvim-web-devicons is required to use file icons! "
.. "Set `use_icons = false` in your config to stop seeing this message."
)
return ""
end
end
local icon, hl
local icon_key = (name or "") .. "|&|" .. (ext or "")
if icon_cache[icon_key] then
icon, hl = unpack(icon_cache[icon_key])
else
icon, hl = web_devicons.get_icon(name, ext, { default = true })
icon_cache[icon_key] = { icon, hl }
end
if icon then
if hl and render_data then
render_data:add_hl(hl, line_idx, offset, offset + string.len(icon) + 1)
end
return icon .. " ", hl
end
return ""
end
local git_status_hl_map = {
["A"] = "DiffviewStatusAdded",
["?"] = "DiffviewStatusUntracked",
["M"] = "DiffviewStatusModified",
["R"] = "DiffviewStatusRenamed",
["C"] = "DiffviewStatusCopied",
["T"] = "DiffviewStatusTypeChanged",
["U"] = "DiffviewStatusUnmerged",
["X"] = "DiffviewStatusUnknown",
["D"] = "DiffviewStatusDeleted",
["B"] = "DiffviewStatusBroken",
["!"] = "DiffviewStatusIgnored",
}
function M.get_git_hl(status)
return git_status_hl_map[status]
end
function M.get_colors()
return {
white = M.get_fg("Normal") or "White",
red = M.get_fg("Keyword") or "Red",
green = M.get_fg("Character") or "Green",
yellow = M.get_fg("PreProc") or "Yellow",
blue = M.get_fg("Include") or "Blue",
purple = M.get_fg("Define") or "Purple",
cyan = M.get_fg("Conditional") or "Cyan",
dark_red = M.get_fg("Keyword") or "DarkRed",
orange = M.get_fg("Number") or "Orange",
}
end
function M.get_hl_groups()
local colors = M.get_colors()
return {
FilePanelTitle = { fg = M.get_fg("Label") or colors.blue, style = "bold" },
FilePanelCounter = { fg = M.get_fg("Identifier") or colors.purple, style = "bold" },
FilePanelFileName = { fg = M.get_fg("Normal") or colors.white },
Dim1 = { fg = M.get_fg("Comment") or colors.white },
Primary = { fg = M.get_fg("Function") or "Purple" },
Secondary = { fg = M.get_fg("String") or "Orange" },
}
end
M.hl_links = {
Normal = "Normal",
NonText = "NonText",
CursorLine = "CursorLine",
WinSeparator = "WinSeparator",
SignColumn = "Normal",
StatusLine = "StatusLine",
StatusLineNC = "StatusLineNC",
EndOfBuffer = "EndOfBuffer",
FilePanelRootPath = "DiffviewFilePanelTitle",
FilePanelFileName = "Normal",
FilePanelSelected = "Type",
FilePanelPath = "Comment",
FilePanelInsertions = "diffAdded",
FilePanelDeletions = "diffRemoved",
FilePanelConflicts = "DiagnosticSignWarn",
FolderName = "Directory",
FolderSign = "PreProc",
Hash = "Identifier",
Reference = "Function",
ReflogSelector = "Special",
StatusAdded = "diffAdded",
StatusUntracked = "diffAdded",
StatusModified = "diffChanged",
StatusRenamed = "diffChanged",
StatusCopied = "diffChanged",
StatusTypeChange = "diffChanged",
StatusUnmerged = "diffChanged",
StatusUnknown = "diffRemoved",
StatusDeleted = "diffRemoved",
StatusBroken = "diffRemoved",
StatusIgnored = "Comment",
DiffAdd = "DiffAdd",
DiffDelete = "DiffDelete",
DiffChange = "DiffChange",
DiffText = "DiffText",
}
function M.update_diff_hl()
local fg = M.get_fg("DiffDelete", true) or "NONE"
local bg = M.get_bg("DiffDelete", true) or "NONE"
local style = M.get_style("DiffDelete", true) or "NONE"
M.hi("DiffviewDiffAddAsDelete", { fg = fg, bg = bg, style = style })
M.hi_link("DiffviewDiffDeleteDim", "Comment", { default = true })
if config.get_config().enhanced_diff_hl then
M.hi_link("DiffviewDiffDelete", "DiffviewDiffDeleteDim")
end
end
function M.setup()
for name, v in pairs(M.get_hl_groups()) do
v = vim.tbl_extend("force", v, { default = true })
M.hi("Diffview" .. name, v)
end
for from, to in pairs(M.hl_links) do
M.hi_link("Diffview" .. from, to, { default = true })
end
M.update_diff_hl()
end
return M

View File

@ -0,0 +1,279 @@
if not require("diffview.bootstrap") then
return
end
local hl = require("diffview.hl")
local lazy = require("diffview.lazy")
local arg_parser = lazy.require("diffview.arg_parser") ---@module "diffview.arg_parser"
local config = lazy.require("diffview.config") ---@module "diffview.config"
local lib = lazy.require("diffview.lib") ---@module "diffview.lib"
local utils = lazy.require("diffview.utils") ---@module "diffview.utils"
local vcs = lazy.require("diffview.vcs") ---@module "diffview.vcs"
local api = vim.api
local logger = DiffviewGlobal.logger
local pl = lazy.access(utils, "path") ---@type PathLib
local M = {}
function M.setup(user_config)
config.setup(user_config or {})
end
function M.init()
-- Fix the strange behavior that "<afile>" expands non-files
-- as file name in some cases.
--
-- Ref:
-- * sindrets/diffview.nvim#369
-- * neovim/neovim#23943
local function get_tabnr(state)
if vim.fn.has("nvim-0.9.2") ~= 1 then
return tonumber(state.match)
else
return tonumber(state.file)
end
end
local au = api.nvim_create_autocmd
-- Set up highlighting
hl.setup()
-- Set up autocommands
M.augroup = api.nvim_create_augroup("diffview_nvim", {})
au("TabEnter", {
group = M.augroup,
pattern = "*",
callback = function(_)
M.emit("tab_enter")
end,
})
au("TabLeave", {
group = M.augroup,
pattern = "*",
callback = function(_)
M.emit("tab_leave")
end,
})
au("TabClosed", {
group = M.augroup,
pattern = "*",
callback = function(state)
M.close(get_tabnr(state))
end,
})
au("BufWritePost", {
group = M.augroup,
pattern = "*",
callback = function(_)
M.emit("buf_write_post")
end,
})
au("WinClosed", {
group = M.augroup,
pattern = "*",
callback = function(state)
M.emit("win_closed", get_tabnr(state))
end,
})
au("ColorScheme", {
group = M.augroup,
pattern = "*",
callback = function(_)
M.update_colors()
end,
})
au("User", {
group = M.augroup,
pattern = "FugitiveChanged",
callback = function(_)
M.emit("refresh_files")
end,
})
-- Set up user autocommand emitters
DiffviewGlobal.emitter:on("view_opened", function(_)
api.nvim_exec_autocmds("User", { pattern = "DiffviewViewOpened", modeline = false })
end)
DiffviewGlobal.emitter:on("view_closed", function(_)
api.nvim_exec_autocmds("User", { pattern = "DiffviewViewClosed", modeline = false })
end)
DiffviewGlobal.emitter:on("view_enter", function(_)
api.nvim_exec_autocmds("User", { pattern = "DiffviewViewEnter", modeline = false })
end)
DiffviewGlobal.emitter:on("view_leave", function(_)
api.nvim_exec_autocmds("User", { pattern = "DiffviewViewLeave", modeline = false })
end)
DiffviewGlobal.emitter:on("view_post_layout", function(_)
api.nvim_exec_autocmds("User", { pattern = "DiffviewViewPostLayout", modeline = false })
end)
DiffviewGlobal.emitter:on("diff_buf_read", function(_)
api.nvim_exec_autocmds("User", { pattern = "DiffviewDiffBufRead", modeline = false })
end)
DiffviewGlobal.emitter:on("diff_buf_win_enter", function(_)
api.nvim_exec_autocmds("User", { pattern = "DiffviewDiffBufWinEnter", modeline = false })
end)
-- Set up completion wrapper used by `vim.ui.input()`
vim.cmd([[
function! Diffview__ui_input_completion(...) abort
return luaeval("DiffviewGlobal.state.current_completer(
\ unpack(vim.fn.eval('a:000')))")
endfunction
]])
end
---@param args string[]
function M.open(args)
local view = lib.diffview_open(args)
if view then
view:open()
end
end
---@param range? { [1]: integer, [2]: integer }
---@param args string[]
function M.file_history(range, args)
local view = lib.file_history(range, args)
if view then
view:open()
end
end
function M.close(tabpage)
if tabpage then
vim.schedule(function()
lib.dispose_stray_views()
end)
return
end
local view = lib.get_current_view()
if view then
view:close()
lib.dispose_view(view)
end
end
function M.completion(_, cmd_line, cur_pos)
local ctx = arg_parser.scan(cmd_line, { cur_pos = cur_pos, allow_ex_range = true })
local cmd = ctx.args[1]
if cmd and M.completers[cmd] then
return arg_parser.process_candidates(M.completers[cmd](ctx), ctx)
end
end
---Create a temporary adapter to get relevant completions
---@return VCSAdapter?
function M.get_adapter()
local cfile = pl:vim_expand("%")
local top_indicators = utils.vec_join(
vim.bo.buftype == ""
and pl:absolute(cfile)
or nil,
pl:realpath(".")
)
local err, adapter = vcs.get_adapter({ top_indicators = top_indicators })
if err then
logger:warn("[completion] Failed to create adapter: " .. err)
end
return adapter
end
M.completers = {
---@param ctx CmdLineContext
DiffviewOpen = function(ctx)
local has_rev_arg = false
local adapter = M.get_adapter()
for i = 2, math.min(#ctx.args, ctx.divideridx) do
if ctx.args[i]:sub(1, 1) ~= "-" and i ~= ctx.argidx then
has_rev_arg = true
break
end
end
local candidates = {}
if ctx.argidx > ctx.divideridx then
if adapter then
utils.vec_push(candidates, unpack(adapter:path_candidates(ctx.arg_lead)))
else
utils.vec_push(candidates, unpack(vim.fn.getcompletion(ctx.arg_lead, "file", 0)))
end
elseif adapter then
if not has_rev_arg and ctx.arg_lead:sub(1, 1) ~= "-" then
utils.vec_push(candidates, unpack(adapter.comp.open:get_all_names()))
utils.vec_push(candidates, unpack(adapter:rev_candidates(ctx.arg_lead, {
accept_range = true,
})))
else
utils.vec_push(candidates, unpack(
adapter.comp.open:get_completion(ctx.arg_lead)
or adapter.comp.open:get_all_names()
))
end
end
return candidates
end,
---@param ctx CmdLineContext
DiffviewFileHistory = function(ctx)
local adapter = M.get_adapter()
local candidates = {}
if adapter then
utils.vec_push(candidates, unpack(
adapter.comp.file_history:get_completion(ctx.arg_lead)
or adapter.comp.file_history:get_all_names()
))
utils.vec_push(candidates, unpack(adapter:path_candidates(ctx.arg_lead)))
else
utils.vec_push(candidates, unpack(vim.fn.getcompletion(ctx.arg_lead, "file", 0)))
end
return candidates
end,
}
function M.update_colors()
hl.setup()
lib.update_colors()
end
local function _emit(no_recursion, event_name, ...)
local view = lib.get_current_view()
if view and not view.closing:check() then
local that = view.emitter
local fn = no_recursion and that.nore_emit or that.emit
fn(that, event_name, ...)
that = DiffviewGlobal.emitter
fn = no_recursion and that.nore_emit or that.emit
if event_name == "tab_enter" then
fn(that, "view_enter", view)
elseif event_name == "tab_leave" then
fn(that, "view_leave", view)
end
end
end
function M.emit(event_name, ...)
_emit(false, event_name, ...)
end
function M.nore_emit(event_name, ...)
_emit(true, event_name, ...)
end
M.init()
return M

View File

@ -0,0 +1,546 @@
---@diagnostic disable: invisible
local oop = require("diffview.oop")
local async = require("diffview.async")
local lazy = require("diffview.lazy")
local utils = lazy.require("diffview.utils") ---@module "diffview.utils"
local await = async.await
local fmt = string.format
local logger = DiffviewGlobal.logger
local uv = vim.loop
local M = {}
---@alias diffview.Job.OnOutCallback fun(err?: string, line: string, j: diffview.Job)
---@alias diffview.Job.OnExitCallback fun(j: diffview.Job, success: boolean, err?: string)
---@alias diffview.Job.OnRetryCallback fun(j: diffview.Job)
---@alias diffview.Job.FailCond fun(j: diffview.Job): boolean, string
---@alias StdioKind "in"|"out"|"err"
---@class diffview.Job : Waitable
---@operator call: diffview.Job
---@field command string
---@field args string[]
---@field cwd string
---@field retry integer
---@field check_status diffview.Job.FailCond
---@field log_opt Logger.log_job.Opt
---@field writer string|string[]
---@field env string[]
---@field stdout string[]
---@field stderr string[]
---@field handle uv_process_t
---@field pid integer
---@field code integer
---@field signal integer
---@field p_out uv_pipe_t
---@field p_err uv_pipe_t
---@field p_in? uv_pipe_t
---@field buffered_std boolean
---@field on_stdout_listeners diffview.Job.OnOutCallback[]
---@field on_stderr_listeners diffview.Job.OnOutCallback[]
---@field on_exit_listeners diffview.Job.OnExitCallback[]
---@field on_retry_listeners diffview.Job.OnRetryCallback[]
---@field _started boolean
---@field _done boolean
---@field _retry_count integer
local Job = oop.create_class("Job", async.Waitable)
local function prepare_env(env)
local ret = {}
for k, v in pairs(env) do
table.insert(ret, k .. "=" .. v)
end
return ret
end
---Predefined fail conditions.
Job.FAIL_COND = {
---Fail on all non-zero exit codes.
---@param j diffview.Job
non_zero = function(j)
return j.code == 0, fmt("Job exited with a non-zero exit code: %d", j.code)
end,
---Fail if there's no data in stdout.
---@param j diffview.Job
on_empty = function(j)
local msg = fmt("Job expected output, but returned nothing! Code: %d", j.code)
local n = #j.stdout
if n == 0 or (n == 1 and j.stdout[1] == "") then return false, msg end
return true
end,
}
function Job:init(opt)
self.command = opt.command
self.args = opt.args
self.cwd = opt.cwd
self.env = opt.env and prepare_env(opt.env) or prepare_env(uv.os_environ())
self.retry = opt.retry or 0
self.writer = opt.writer
self.buffered_std = utils.sate(opt.buffered_std, true)
self.on_stdout_listeners = {}
self.on_stderr_listeners = {}
self.on_exit_listeners = {}
self.on_retry_listeners = {}
self._started = false
self._done = false
self._retry_count = 0
self.log_opt = vim.tbl_extend("keep", opt.log_opt or {}, {
func = "debug",
no_stdout = true,
debuginfo = debug.getinfo(3, "Sl"),
})
if opt.fail_cond then
if type(opt.fail_cond) == "string" then
self.check_status = Job.FAIL_COND[opt.fail_cond]
assert(self.check_status, fmt("Unknown fail condition: '%s'", opt.fail_cond))
elseif type(opt.fail_cond) == "function" then
self.check_status = opt.fail_cond
else
error("Invalid fail condition: " .. vim.inspect(opt.fail_cond))
end
else
self.check_status = Job.FAIL_COND.non_zero
end
if opt.on_stdout then self:on_stdout(opt.on_stdout) end
if opt.on_stderr then self:on_stderr(opt.on_stderr) end
if opt.on_exit then self:on_exit(opt.on_exit) end
if opt.on_retry then self:on_retry(opt.on_retry) end
end
---@param ... uv_handle_t
local function try_close(...)
local args = { ... }
for i = 1, select("#", ...) do
local handle = args[i]
if handle and not handle:is_closing() then
handle:close()
end
end
end
---@param chunks string[]
---@return string[] lines
local function process_chunks(chunks)
local data = table.concat(chunks)
if data == "" then
return {}
end
local has_eof = data:sub(-1) == "\n"
local ret = vim.split(data, "\r?\n")
if has_eof then ret[#ret] = nil end
return ret
end
---@private
---@param pipe uv_pipe_t
---@param out string[]
---@param err? string
---@param data? string
function Job:buffered_reader(pipe, out, err, data)
if err then
logger:error("[Job:buffered_reader()] " .. err)
end
if data then
out[#out + 1] = data
else
try_close(pipe)
end
end
---@private
---@param pipe uv_pipe_t
---@param out string[]
---@param line_listeners? diffview.Job.OnOutCallback[]
function Job:line_reader(pipe, out, line_listeners)
local line_buffer
---@param err? string
---@param data? string
return function (err, data)
if err then
logger:error("[Job:line_reader()] " .. err)
end
if data then
local has_eol = data:sub(-1) == "\n"
local lines = vim.split(data, "\r?\n")
lines[1] = (line_buffer or "") .. lines[1]
line_buffer = nil
for i, line in ipairs(lines) do
if not has_eol and i == #lines then
line_buffer = line
else
out[#out+1] = line
if line_listeners then
for _, listener in ipairs(line_listeners) do
listener(nil, line, self)
end
end
end
end
else
if line_buffer then
out[#out+1] = line_buffer
if line_listeners then
for _, listener in ipairs(line_listeners) do
listener(nil, line_buffer, self)
end
end
end
try_close(pipe)
end
end
end
---@private
---@param pipe uv_pipe_t
---@param out string[]
---@param kind StdioKind
function Job:handle_reader(pipe, out, kind)
if self.buffered_std then
pipe:read_start(utils.bind(self.buffered_reader, self, pipe, out))
else
local listeners = ({
out = self.on_stdout_listeners,
err = self.on_stderr_listeners,
})[kind] or {}
pipe:read_start(self:line_reader(pipe, out, listeners))
end
end
---@private
---@param pipe uv_pipe_t
---@param data string|string[]
function Job:handle_writer(pipe, data)
if type(data) == "string" then
if data:sub(-1) ~= "\n" then data = data .. "\n" end
pipe:write(data, function(err)
if err then
logger:error("[Job:handle_writer()] " .. err)
end
try_close(pipe)
end)
else
---@cast data string[]
local c = #data
for i, s in ipairs(data) do
if i ~= c then
pipe:write(s .. "\n")
else
pipe:write(s .. "\n", function(err)
if err then
logger:error("[Job:handle_writer()] " .. err)
end
try_close(pipe)
end)
end
end
end
end
---@private
function Job:reset()
try_close(self.handle, self.p_out, self.p_err, self.p_in)
self.handle = nil
self.p_out = nil
self.p_err = nil
self.p_in = nil
self.stdout = {}
self.stderr = {}
self.pid = nil
self.code = nil
self.signal = nil
self._started = false
self._done = false
end
---@param self diffview.Job
---@param callback fun(success: boolean, err?: string)
Job.start = async.wrap(function(self, callback)
self:reset()
self.p_out = uv.new_pipe(false)
self.p_err = uv.new_pipe(false)
assert(self.p_out and self.p_err, "Failed to create pipes!")
if self.writer then
self.p_in = uv.new_pipe(false)
assert(self.p_in, "Failed to create pipes!")
end
self._started = true
local handle, pid
handle, pid = uv.spawn(self.command, {
args = self.args,
stdio = { self.p_in, self.p_out, self.p_err },
cwd = self.cwd,
env = self.env,
hide = true,
},
function(code, signal)
---@cast handle -?
handle:close()
self.p_out:read_stop()
self.p_err:read_stop()
if not self.code then self.code = code end
if not self.signal then self.signal = signal end
try_close(self.p_out, self.p_err, self.p_in)
if self.buffered_std then
self.stdout = process_chunks(self.stdout)
self.stderr = process_chunks(self.stderr)
end
---@type boolean, string?
local ok, err = self:is_success()
local log = not self.log_opt.silent and logger or logger.mock --[[@as Logger ]]
if not ok then
log:error(err)
log:log_job(self, { func = "error", no_stdout = true, debuginfo = self.log_opt.debuginfo })
if self.retry > 0 then
if self._retry_count < self.retry then
self:do_retry(callback)
return
else
log:error("All retries failed!")
end
end
else
if self._retry_count > 0 then
log:info("Retry was successful!")
end
log:log_job(self, self.log_opt)
end
self._retry_count = 0
self._done = true
for _, listener in ipairs(self.on_exit_listeners) do
listener(self, ok, err)
end
callback(ok, err)
end)
if not handle then
try_close(self.p_out, self.p_err, self.p_in)
error("Failed to spawn job!")
end
self.handle = handle
self.pid = pid
self:handle_reader(self.p_out, self.stdout, "out")
self:handle_reader(self.p_err, self.stderr, "err")
if self.p_in then
self:handle_writer(self.p_in, self.writer)
end
end)
---@private
---@param self diffview.Job
---@param callback function
Job.do_retry = async.void(function(self, callback)
self._retry_count = self._retry_count + 1
if not self.log_opt.silent then
logger:fmt_warn("(%d/%d) Retrying job...", self._retry_count, self.retry)
end
await(async.timeout(1))
for _, listener in ipairs(self.on_retry_listeners) do
listener(self)
end
self:start(callback)
end)
---@param self diffview.Job
---@param timeout? integer # Max duration (ms) (default: 30_000)
---@return boolean success
---@return string? err
function Job:sync(timeout)
if not self:is_started() then
self:start()
end
await(async.scheduler())
if self:is_done() then
return self:is_success()
end
local ok, status = vim.wait(timeout or (30 * 1000), function()
return self:is_done()
end, 1)
await(async.scheduler())
if not ok then
if status == -1 then
error("Synchronous job timed out!")
elseif status == -2 then
error("Synchronous job got interrupted!")
end
return false, "Unexpected state"
end
return self:is_success()
end
---@param code integer
---@param signal? integer|uv.aliases.signals # (default: "sigterm")
---@return 0? success
---@return string? err_name
---@return string? err_msg
function Job:kill(code, signal)
if not self.handle then return 0 end
if not self.handle:is_closing() then
self.code = code
self.signal = signal
return self.handle:kill(signal or "sigterm")
end
return 0
end
---@override
---@param self diffview.Job
---@param callback fun(success: boolean, err?: string)
Job.await = async.sync_wrap(function(self, callback)
if self:is_done() then
callback(self:is_success())
elseif self:is_running() then
self:on_exit(function(_, ...) callback(...) end)
else
callback(await(self:start()))
end
end)
---@param jobs diffview.Job[]
function Job.start_all(jobs)
for _, job in ipairs(jobs) do
job:start()
end
end
---@param jobs diffview.Job[]
---@param callback fun(success: boolean, errors?: string[])
Job.join = async.wrap(function(jobs, callback)
-- Start by ensuring all jobs are running
for _, job in ipairs(jobs) do
if not job:is_started() then
job:start()
end
end
local success, errors = true, {}
for _, job in ipairs(jobs) do
local ok, err = await(job)
if not ok then
success = false
errors[#errors + 1] = err
end
end
callback(success, not success and errors or nil)
end)
---@param jobs diffview.Job[]
Job.chain = async.void(function(jobs)
for _, job in ipairs(jobs) do
await(job)
end
end)
---Subscribe to stdout data. Only used if `buffered_std=false`.
---@param callback diffview.Job.OnOutCallback
function Job:on_stdout(callback)
table.insert(self.on_stdout_listeners, callback)
if not self:is_started() then
self.buffered_std = false
end
end
---Subscribe to stderr data. Only used if `buffered_std=false`.
---@param callback diffview.Job.OnOutCallback
function Job:on_stderr(callback)
table.insert(self.on_stderr_listeners, callback)
if not self:is_running() then
self.buffered_std = false
end
end
---@param callback diffview.Job.OnExitCallback
function Job:on_exit(callback)
table.insert(self.on_exit_listeners, callback)
end
---@param callback diffview.Job.OnRetryCallback
function Job:on_retry(callback)
table.insert(self.on_retry_listeners, callback)
end
---@return boolean success
---@return string? err
function Job:is_success()
local ok, err = self:check_status()
if not ok then return false, err end
return true
end
function Job:is_done()
return self._done
end
function Job:is_started()
return self._started
end
function Job:is_running()
return self:is_started() and not self:is_done()
end
M.Job = Job
return M

View File

@ -0,0 +1,125 @@
local fmt = string.format
local lazy = {}
---@class LazyModule : { [string] : unknown }
---@field __get fun(): unknown Load the module if needed, and return it.
---@field __loaded boolean Indicates that the module has been loaded.
---Create a table the triggers a given handler every time it's accessed or
---called, until the handler returns a table. Once the handler has returned a
---table, any subsequent accessing of the wrapper will instead access the table
---returned from the handler.
---@param t any
---@param handler fun(t: any): table?
---@return LazyModule
function lazy.wrap(t, handler)
local export
local ret = {
__get = function()
if export == nil then
---@cast handler function
export = handler(t)
end
return export
end,
__loaded = function()
return export ~= nil
end,
}
return setmetatable(ret, {
__index = function(_, key)
if export == nil then ret.__get() end
---@cast export table
return export[key]
end,
__newindex = function(_, key, value)
if export == nil then ret.__get() end
export[key] = value
end,
__call = function(_, ...)
if export == nil then ret.__get() end
---@cast export table
return export(...)
end,
})
end
---Will only require the module after first either indexing, or calling it.
---
---You can pass a handler function to process the module in some way before
---returning it. This is useful i.e. if you're trying to require the result of
---an exported function.
---
---Example:
---
---```lua
--- local foo = require("bar")
--- local foo = lazy.require("bar")
---
--- local foo = require("bar").baz({ qux = true })
--- local foo = lazy.require("bar", function(module)
--- return module.baz({ qux = true })
--- end)
---```
---@param require_path string
---@param handler? fun(module: any): any
---@return LazyModule
function lazy.require(require_path, handler)
local use_handler = type(handler) == "function"
return lazy.wrap(require_path, function(s)
if use_handler then
---@cast handler function
return handler(require(s))
end
return require(s)
end)
end
---Lazily access a table value. If `x` is a string, it's treated as a lazy
---require.
---
---Example:
---
---```lua
--- -- table:
--- local foo = bar.baz.qux.quux
--- local foo = lazy.access(bar, "baz.qux.quux")
--- local foo = lazy.access(bar, { "baz", "qux", "quux" })
---
--- -- require:
--- local foo = require("bar").baz.qux.quux
--- local foo = lazy.access("bar", "baz.qux.quux")
--- local foo = lazy.access("bar", { "baz", "qux", "quux" })
---```
---@param x table|string Either the table to be accessed, or a module require path.
---@param access_path string|string[] Either a `.` separated string of table keys, or a list.
---@return LazyModule
function lazy.access(x, access_path)
local keys = type(access_path) == "table"
and access_path
or vim.split(access_path --[[@as string ]], ".", { plain = true })
local handler = function(module)
local export = module
for _, key in ipairs(keys) do
export = export[key]
assert(export ~= nil, fmt("Failed to lazy-access! No key '%s' in table!", key))
end
return export
end
if type(x) == "string" then
return lazy.require(x, handler)
else
return lazy.wrap(x, handler)
end
end
return lazy

View File

@ -0,0 +1,233 @@
local lazy = require("diffview.lazy")
local DiffView = lazy.access("diffview.scene.views.diff.diff_view", "DiffView") ---@type DiffView|LazyModule
local FileHistoryView = lazy.access("diffview.scene.views.file_history.file_history_view", "FileHistoryView") ---@type FileHistoryView|LazyModule
local StandardView = lazy.access("diffview.scene.views.standard.standard_view", "StandardView") ---@type StandardView|LazyModule
local arg_parser = lazy.require("diffview.arg_parser") ---@module "diffview.arg_parser"
local config = lazy.require("diffview.config") ---@module "diffview.config"
local vcs = lazy.require("diffview.vcs") ---@module "diffview.vcs"
local utils = lazy.require("diffview.utils") ---@module "diffview.utils"
local api = vim.api
local logger = DiffviewGlobal.logger
local M = {}
---@type View[]
M.views = {}
function M.diffview_open(args)
local default_args = config.get_config().default_args.DiffviewOpen
local argo = arg_parser.parse(utils.flatten({ default_args, args }))
local rev_arg = argo.args[1]
logger:info("[command call] :DiffviewOpen " .. table.concat(utils.flatten({
default_args,
args,
}), " "))
local err, adapter = vcs.get_adapter({
cmd_ctx = {
path_args = argo.post_args,
cpath = argo:get_flag("C", { no_empty = true, expand = true }),
},
})
if err then
utils.err(err)
return
end
---@cast adapter -?
local opts = adapter:diffview_options(argo)
if opts == nil then
return
end
local v = DiffView({
adapter = adapter,
rev_arg = rev_arg,
path_args = adapter.ctx.path_args,
left = opts.left,
right = opts.right,
options = opts.options,
})
if not v:is_valid() then
return
end
table.insert(M.views, v)
logger:debug("DiffView instantiation successful!")
return v
end
---@param range? { [1]: integer, [2]: integer }
---@param args string[]
function M.file_history(range, args)
local default_args = config.get_config().default_args.DiffviewFileHistory
local argo = arg_parser.parse(utils.flatten({ default_args, args }))
logger:info("[command call] :DiffviewFileHistory " .. table.concat(utils.flatten({
default_args,
args,
}), " "))
local err, adapter = vcs.get_adapter({
cmd_ctx = {
path_args = argo.args,
cpath = argo:get_flag("C", { no_empty = true, expand = true }),
},
})
if err then
utils.err(err)
return
end
---@cast adapter -?
local log_options = adapter:file_history_options(range, adapter.ctx.path_args, argo)
if log_options == nil then
return
end
local v = FileHistoryView({
adapter = adapter,
log_options = log_options,
})
if not v:is_valid() then
return
end
table.insert(M.views, v)
logger:debug("FileHistoryView instantiation successful!")
return v
end
---@param view View
function M.add_view(view)
table.insert(M.views, view)
end
---@param view View
function M.dispose_view(view)
for j, v in ipairs(M.views) do
if v == view then
table.remove(M.views, j)
return
end
end
end
---Close and dispose of views that have no tabpage.
function M.dispose_stray_views()
local tabpage_map = {}
for _, id in ipairs(api.nvim_list_tabpages()) do
tabpage_map[id] = true
end
local dispose = {}
for _, view in ipairs(M.views) do
if not tabpage_map[view.tabpage] then
-- Need to schedule here because the tabnr's don't update fast enough.
vim.schedule(function()
view:close()
end)
table.insert(dispose, view)
end
end
for _, view in ipairs(dispose) do
M.dispose_view(view)
end
end
---Get the currently open Diffview.
---@return View?
function M.get_current_view()
local tabpage = api.nvim_get_current_tabpage()
for _, view in ipairs(M.views) do
if view.tabpage == tabpage then
return view
end
end
return nil
end
function M.tabpage_to_view(tabpage)
for _, view in ipairs(M.views) do
if view.tabpage == tabpage then
return view
end
end
end
---Get the first tabpage that is not a view. Tries the previous tabpage first.
---If there are no non-view tabpages: returns nil.
---@return number|nil
function M.get_prev_non_view_tabpage()
local tabs = api.nvim_list_tabpages()
if #tabs > 1 then
local seen = {}
for _, view in ipairs(M.views) do
seen[view.tabpage] = true
end
local prev_tab = utils.tabnr_to_id(vim.fn.tabpagenr("#")) or -1
if api.nvim_tabpage_is_valid(prev_tab) and not seen[prev_tab] then
return prev_tab
else
for _, id in ipairs(tabs) do
if not seen[id] then
return id
end
end
end
end
end
---@param bufnr integer
---@param ignore? vcs.File[]
---@return boolean
function M.is_buf_in_use(bufnr, ignore)
local ignore_map = ignore and utils.vec_slice(ignore) or {}
utils.add_reverse_lookup(ignore_map)
for _, view in ipairs(M.views) do
if view:instanceof(StandardView.__get()) then
---@cast view StandardView
for _, file in ipairs(view.cur_entry and view.cur_entry.layout:files() or {}) do
if file:is_valid() and file.bufnr == bufnr then
if not ignore_map[file] then
return true
end
end
end
end
end
return false
end
function M.update_colors()
for _, view in ipairs(M.views) do
if view:instanceof(StandardView.__get()) then
---@cast view StandardView
if view.panel:buf_loaded() then
view.panel:render()
view.panel:redraw()
end
end
end
end
return M

View File

@ -0,0 +1,411 @@
local async = require("diffview.async")
local lazy = require("diffview.lazy")
local oop = require("diffview.oop")
local Mock = lazy.access("diffview.mock", "Mock") ---@type Mock|LazyModule
local Semaphore = lazy.access("diffview.control", "Semaphore") ---@type Semaphore|LazyModule
local loop = lazy.require("diffview.debounce") ---@module "diffview.debounce"
local utils = lazy.require("diffview.utils") ---@module "diffview.utils"
local api = vim.api
local await, pawait = async.await, async.pawait
local fmt = string.format
local pl = lazy.access(utils, "path") ---@type PathLib
local uv = vim.loop
local M = {}
---@class Logger.TimeOfDay
---@field hours integer
---@field mins integer
---@field secs integer
---@field micros integer
---@field tz string
---@field timestamp integer
---@class Logger.Time
---@field timestamp integer # Unix time stamp
---@field micros integer # Microsecond offset
---Get high resolution time of day
---@param time? Logger.Time
---@return Logger.TimeOfDay
local function time_of_day(time)
local secs, micros
if time then
secs, micros = time.timestamp, (time.micros or 0)
else
secs, micros = uv.gettimeofday()
assert(secs, micros)
end
local tzs = os.date("%z", secs) --[[@as string ]]
local sign = tzs:match("[+-]") == "-" and -1 or 1
local tz_h, tz_m = tzs:match("[+-]?(%d%d)(%d%d)")
tz_h = tz_h * sign
tz_m = tz_m * sign
local ret = {}
ret.hours = math.floor(((secs / (60 * 60)) % 24) + tz_h)
ret.mins = math.floor(((secs / 60) % 60) + tz_m)
ret.secs = (secs % 60)
ret.micros = micros
ret.tz = tzs
ret.timestamp = secs
return ret
end
---@alias Logger.LogFunc fun(self: Logger, ...)
---@alias Logger.FmtLogFunc fun(self: Logger, formatstring: string, ...)
---@alias Logger.LazyLogFunc fun(self: Logger, work: (fun(): ...))
---@class Logger.Context
---@field debuginfo debuginfo
---@field time Logger.Time
---@field label string
---@class Logger : diffview.Object
---@operator call : Logger
---@field private outfile_status Logger.OutfileStatus
---@field private level integer # Max level. Messages of higher level will be ignored. NOTE: Higher level -> lower severity.
---@field private msg_buffer (string|function)[]
---@field private msg_sem Semaphore
---@field private batch_interval integer # Minimum time (ms) between each time batched messages are written to the output file.
---@field private batch_handle? Closeable
---@field private ctx? Logger.Context
---@field plugin string
---@field outfile string
---@field trace Logger.LogFunc
---@field debug Logger.LogFunc
---@field info Logger.LogFunc
---@field warn Logger.LogFunc
---@field error Logger.LogFunc
---@field fatal Logger.LogFunc
---@field fmt_trace Logger.FmtLogFunc
---@field fmt_debug Logger.FmtLogFunc
---@field fmt_info Logger.FmtLogFunc
---@field fmt_warn Logger.FmtLogFunc
---@field fmt_error Logger.FmtLogFunc
---@field fmt_fatal Logger.FmtLogFunc
---@field lazy_trace Logger.LazyLogFunc
---@field lazy_debug Logger.LazyLogFunc
---@field lazy_info Logger.LazyLogFunc
---@field lazy_warn Logger.LazyLogFunc
---@field lazy_error Logger.LazyLogFunc
---@field lazy_fatal Logger.LazyLogFunc
local Logger = oop.create_class("Logger")
---@enum Logger.OutfileStatus
Logger.OutfileStatus = oop.enum({
UNKNOWN = 1,
READY = 2,
ERROR = 3,
})
---@enum Logger.LogLevels
Logger.LogLevels = oop.enum({
fatal = 1,
error = 2,
warn = 3,
info = 4,
debug = 5,
trace = 6,
})
Logger.mock = Mock()
function Logger:init(opt)
opt = opt or {}
self.plugin = opt.plugin or "diffview"
self.outfile = opt.outfile or fmt("%s/%s.log", vim.fn.stdpath("cache"), self.plugin)
self.outfile_status = Logger.OutfileStatus.UNKNOWN
self.level = DiffviewGlobal.debug_level > 0 and Logger.LogLevels.debug or Logger.LogLevels.info
self.msg_buffer = {}
self.msg_sem = Semaphore(1)
self.batch_interval = opt.batch_interval or 3000
-- Flush msg buffer before exiting
api.nvim_create_autocmd("VimLeavePre", {
callback = function()
if self.batch_handle then
self.batch_handle.close()
await(self:flush())
end
end,
})
end
---@return Logger.Time
function Logger.time_now()
local secs, micros = uv.gettimeofday()
assert(secs, micros)
return {
timestamp = secs,
micros = micros,
}
end
---@param num number
---@param precision number
---@return number
local function to_precision(num, precision)
if num % 1 == 0 then return num end
local pow = math.pow(10, precision)
return math.floor(num * pow) / pow
end
---@param object any
---@return string
function Logger.dstring(object)
local tp = type(object)
if tp == "thread"
or tp == "function"
or tp == "userdata"
then
return fmt("<%s %p>", tp, object)
elseif tp == "number" then
return tostring(to_precision(object, 3))
elseif tp == "table" then
local mt = getmetatable(object)
if mt and mt.__tostring then
return tostring(object)
elseif utils.islist(object) then
if #object == 0 then return "[]" end
local s = ""
for i = 1, table.maxn(object) do
if i > 1 then s = s .. ", " end
s = s .. Logger.dstring(object[i])
end
return "[ " .. s .. " ]"
end
return vim.inspect(object)
end
return tostring(object)
end
local dstring = Logger.dstring
local function dvalues(...)
local args = { ... }
local ret = {}
for i = 1, select("#", ...) do
ret[i] = dstring(args[i])
end
return ret
end
---@private
---@param level_name string
---@param lazy_eval boolean
---@param x function|any
---@param ... any
function Logger:_log(level_name, lazy_eval, x, ...)
local ctx = self.ctx or {}
local info = ctx.debuginfo or debug.getinfo(3, "Sl")
local lineinfo = info.short_src .. ":" .. info.currentline
local time = ctx.time or Logger.time_now()
local tod = time_of_day(time)
local date = fmt(
"%s %02d:%02d:%02d.%03d %s",
os.date("%F", time.timestamp),
tod.hours,
tod.mins,
tod.secs,
math.floor(tod.micros / 1000),
tod.tz
)
if lazy_eval then
self:queue_msg(function()
return fmt(
"[%-6s%s] %s: %s%s\n",
level_name:upper(),
date,
lineinfo,
ctx.label and fmt("[%s] ", ctx.label) or "",
table.concat(dvalues(x()), " ")
)
end)
else
self:queue_msg(
fmt(
"[%-6s%s] %s: %s%s\n",
level_name:upper(),
date,
lineinfo,
ctx.label and fmt("[%s] ", ctx.label) or "",
table.concat(dvalues(x, ...), " ")
)
)
end
end
---@diagnostic disable: invisible
---@private
---@param self Logger
---@param msg string
Logger.queue_msg = async.void(function(self, msg)
if self.outfile_status == Logger.OutfileStatus.ERROR then
-- We already failed to prepare the log file
return
elseif self.outfile_status == Logger.OutfileStatus.UNKNOWN then
local ok, err = pawait(pl.touch, pl, self.outfile, { parents = true })
if not ok then
error("Failed to prepare log file! Details:\n" .. err)
end
self.outfile_status = Logger.OutfileStatus.READY
end
local permit = await(self.msg_sem:acquire()) --[[@as Permit ]]
table.insert(self.msg_buffer, msg)
permit:forget()
if self.batch_handle then return end
self.batch_handle = loop.set_timeout(
async.void(function()
await(self:flush())
self.batch_handle = nil
end),
self.batch_interval
)
end)
---@private
---@param self Logger
Logger.flush = async.void(function(self)
if next(self.msg_buffer) then
local permit = await(self.msg_sem:acquire()) --[[@as Permit ]]
-- Eval lazy messages
for i = 1, #self.msg_buffer do
if type(self.msg_buffer[i]) == "function" then
self.msg_buffer[i] = self.msg_buffer[i]()
end
end
local fd, err = uv.fs_open(self.outfile, "a", tonumber("0644", 8))
assert(fd, err)
uv.fs_write(fd, table.concat(self.msg_buffer))
uv.fs_close(fd)
self.msg_buffer = {}
permit:forget()
end
end)
---@param min_level integer
---@return Logger
function Logger:lvl(min_level)
if DiffviewGlobal.debug_level >= min_level then
return self
end
return Logger.mock --[[@as Logger ]]
end
---@param ctx Logger.Context
function Logger:set_context(ctx)
self.ctx = ctx
end
function Logger:clear_context()
self.ctx = nil
end
do
-- Create methods
for level, name in ipairs(Logger.LogLevels --[[@as string[] ]]) do
---@param self Logger
Logger[name] = function(self, ...)
if self.level < level then return end
self:_log(name, false, ...)
end
---@param self Logger
Logger["fmt_" .. name] = function(self, formatstring, ...)
if self.level < level then return end
self:_log(name, false, fmt(formatstring, ...))
end
---@param self Logger
Logger["lazy_" .. name] = function(self, func)
if self.level < level then return end
self:_log(name, true, func)
end
end
end
---@diagnostic enable: invisible
---@class Logger.log_job.Opt
---@field func function|string
---@field label string
---@field no_stdout boolean
---@field no_stderr boolean
---@field silent boolean
---@field debug_level integer
---@field debuginfo debuginfo
---@param job diffview.Job
---@param opt? Logger.log_job.Opt
function Logger:log_job(job, opt)
opt = opt or {}
if opt.silent then return end
if opt.debug_level and DiffviewGlobal.debug_level < opt.debug_level then
return
end
self:set_context({
debuginfo = opt.debuginfo or debug.getinfo(2, "Sl"),
time = Logger.time_now(),
label = opt.label,
})
local args = vim.tbl_map(function(arg)
-- Simple shell escape. NOTE: not valid for windows shell.
return fmt("'%s'", arg:gsub("'", [['"'"']]))
end, job.args) --[[@as vector ]]
local log_func = self.debug
if type(opt.func) == "string" then
log_func = self[opt.func]
elseif type(opt.func) == "function" then
log_func = opt.func --[[@as function ]]
end
log_func(self, fmt("[job-info] Exit code: %s", job.code))
log_func(self, fmt(" [cmd] %s %s", job.command, table.concat(args, " ")))
if job.cwd then
log_func(self, fmt(" [cwd] %s", job.cwd))
end
if not opt.no_stdout and job.stdout[1] then
log_func(self, " [stdout] " .. table.concat(job.stdout, "\n"))
end
if not opt.no_stderr and job.stderr[1] then
log_func(self, " [stderr] " .. table.concat(job.stderr, "\n"))
end
self:clear_context()
end
M.Logger = Logger
return M

View File

@ -0,0 +1,41 @@
--[[
A class for creating mock objects. Accessing any key in the object returns
itself. Calling the object does nothing.
--]]
local M = {}
local mock_mt = {}
local function tbl_clone(t)
local ret = {}
for k, v in pairs(t) do ret[k] = v end
return ret
end
---@class Mock
---@operator call : Mock
local Mock = setmetatable({}, mock_mt)
function mock_mt.__index(_, key)
return mock_mt[key]
end
function mock_mt.__call(_, internals)
local mt = {
__index = function(self, k)
if Mock[k] then
return Mock[k]
else
return self
end
end,
__call = function()
return nil
end,
}
local this = setmetatable(tbl_clone(internals or {}), mt)
return this
end
M.Mock = Mock
return M

View File

@ -0,0 +1,257 @@
local async = require("diffview.async")
local lazy = require("diffview.lazy")
local oop = require("diffview.oop")
local Job = lazy.access("diffview.job", "Job") ---@type diffview.Job|LazyModule
local utils = lazy.require("diffview.utils") ---@module "diffview.utils"
local await = async.await
local fmt = string.format
local logger = DiffviewGlobal.logger
local M = {}
---@alias MultiJob.OnExitCallback fun(mj: MultiJob, success: boolean, err?: string)
---@alias MultiJob.OnRetryCallback fun(mj: MultiJob, jobs: diffview.Job[])
---@alias MultiJob.FailCond fun(mj: MultiJob): boolean, diffview.Job[]?, string?
---@class MultiJob : Waitable
---@operator call : MultiJob
---@field jobs diffview.Job[]
---@field retry integer
---@field check_status MultiJob.FailCond
---@field on_exit_listeners MultiJob.OnExitCallback[]
---@field _started boolean
---@field _done boolean
local MultiJob = oop.create_class("MultiJob")
---Predefined fail conditions.
MultiJob.FAIL_COND = {
---Fail if any of the jobs termintated with a non-zero exit code.
---@param mj MultiJob
non_zero = function(mj)
local failed = {}
for _, job in ipairs(mj.jobs) do
if job.code ~= 0 then
failed[#failed + 1] = job
end
end
if next(failed) then
return false, failed, "Job(s) exited with a non-zero exit code!"
end
return true
end,
---Fail if any of the jobs had no data in stdout.
---@param mj MultiJob
on_empty = function(mj)
local failed = {}
for _, job in ipairs(mj.jobs) do
if #job.stdout == 1 and job.stdout[1] == ""
or #job.stdout == 0
then
failed[#failed + 1] = job
end
end
if next(failed) then
return false, failed, "Job(s) expected output, but returned nothing!"
end
return true
end,
}
function MultiJob:init(jobs, opt)
self.jobs = jobs
self.retry = opt.retry or 0
self.on_exit_listeners = {}
self.on_retry_listeners = {}
self._started = false
self._done = false
self.log_opt = vim.tbl_extend("keep", opt.log_opt or {}, {
func = "debug",
no_stdout = true,
debuginfo = debug.getinfo(3, "Sl"),
})
if opt.fail_cond then
if type(opt.fail_cond) == "string" then
self.check_status = MultiJob.FAIL_COND[opt.fail_cond]
assert(self.check_status, fmt("Unknown fail condition: '%s'", opt.fail_cond))
elseif type(opt.fail_cond) == "function" then
self.check_status = opt.fail_cond
else
error("Invalid fail condition: " .. vim.inspect(opt.fail_cond))
end
else
self.check_status = MultiJob.FAIL_COND.non_zero
end
if opt.on_exit then self:on_exit(opt.on_exit) end
if opt.on_retry then self:on_retry(opt.on_retry) end
end
---@private
function MultiJob:reset()
self._started = false
self._done = false
end
---@param self MultiJob
MultiJob.start = async.wrap(function(self, callback)
---@diagnostic disable: invisible
for _, job in ipairs(self.jobs) do
if job:is_running() then
error("A job is still running!")
end
end
self:reset()
self._started = true
local jobs = self.jobs
local retry_status
for i = 1, self.retry + 1 do
if i > 1 then
for _, listener in ipairs(self.on_retry_listeners) do
listener(self, jobs)
end
end
Job.start_all(jobs)
await(Job.join(jobs))
local ok, err
ok, jobs, err = self:check_status()
if ok then break end
---@cast jobs -?
if i == self.retry + 1 then
retry_status = 1
else
retry_status = 0
if not self.log_opt.silent then
logger:error(err)
for _, job in ipairs(jobs) do
logger:log_job(job, { func = "error", no_stdout = true })
end
logger:fmt_error("(%d/%d) Retrying failed jobs...", i, self.retry)
end
await(async.timeout(1))
end
end
if not self.log_opt.silent then
if retry_status == 0 then
logger:info("Retry was successful!")
elseif retry_status == 1 then
logger:error("All retries failed!")
end
end
self._done = true
local ok, err = self:is_success()
for _, listener in ipairs(self.on_exit_listeners) do
listener(self, ok, err)
end
callback(ok, err)
---@diagnostic enable: invisible
end)
---@override
---@param self MultiJob
---@param callback fun(success: boolean, err?: string)
MultiJob.await = async.sync_wrap(function(self, callback)
if self:is_done() then
callback(self:is_success())
elseif self:is_running() then
self:on_exit(function(_, ...) callback(...) end)
else
callback(await(self:start()))
end
end)
---@return boolean success
---@return string? err
function MultiJob:is_success()
local ok, _, err = self:check_status()
if not ok then return false, err end
return true
end
---@param callback MultiJob.OnExitCallback
function MultiJob:on_exit(callback)
table.insert(self.on_exit_listeners, callback)
end
---@param callback MultiJob.OnRetryCallback
function MultiJob:on_retry(callback)
table.insert(self.on_retry_listeners, callback)
end
function MultiJob:is_done()
return self._done
end
function MultiJob:is_started()
return self._started
end
function MultiJob:is_running()
return self:is_started() and not self:is_done()
end
---@return string[]
function MultiJob:stdout()
return utils.flatten(
---@param value diffview.Job
vim.tbl_map(function(value)
return value.stdout
end, self.jobs)
)
end
---@return string[]
function MultiJob:stderr()
return utils.flatten(
---@param value diffview.Job
vim.tbl_map(function(value)
return value.stderr
end, self.jobs)
)
end
---@param code integer
---@param signal? integer|uv.aliases.signals # (default: "sigterm")
---@return 0|nil success
function MultiJob:kill(code, signal)
---@type 0?
local ret = 0
for _, job in ipairs(self.jobs) do
if job:is_running() then
local success = job:kill(code, signal)
if not success then ret = nil end
end
end
return ret
end
M.MultiJob = MultiJob
return M

View File

@ -0,0 +1,227 @@
local lazy = require("diffview.lazy")
local utils = lazy.require("diffview.utils") ---@module "diffview.utils"
local fmt = string.format
local M = {}
function M.abstract_stub()
error("Unimplemented abstract method!")
end
---@generic T
---@param t T
---@return T
function M.enum(t)
utils.add_reverse_lookup(t)
return t
end
---Wrap metatable methods to ensure they're called with the instance as `self`.
---@param func function
---@param instance table
---@return function
local function wrap_mt_func(func, instance)
return function(_, k)
return func(instance, k)
end
end
local mt_func_names = {
"__index",
"__tostring",
"__eq",
"__add",
"__sub",
"__mul",
"__div",
"__mod",
"__pow",
"__unm",
"__len",
"__lt",
"__le",
"__concat",
"__newindex",
"__call",
}
local function new_instance(class, ...)
local inst = { class = class }
local mt = { __index = class }
for _, mt_name in ipairs(mt_func_names) do
local class_mt_func = class[mt_name]
if type(class_mt_func) == "function" then
mt[mt_name] = wrap_mt_func(class_mt_func, inst)
elseif class_mt_func ~= nil then
mt[mt_name] = class_mt_func
end
end
local self = setmetatable(inst, mt)
self:init(...)
return self
end
local function tostring(class)
return fmt("<class %s>", class.__name)
end
---@generic T : diffview.Object
---@generic U : diffview.Object
---@param name string
---@param super_class? T
---@return U new_class
function M.create_class(name, super_class)
super_class = super_class or M.Object
return setmetatable(
{
__name = name,
super_class = super_class,
},
{
__index = super_class,
__call = new_instance,
__tostring = tostring,
}
)
end
local function classm_safeguard(x)
assert(x.class == nil, "Class method should not be invoked from an instance!")
end
local function instancem_safeguard(x)
assert(type(x.class) == "table", "Instance method must be called from a class instance!")
end
---@class diffview.Object
---@field protected __name string
---@field private __init_caller? table
---@field class table|diffview.Object
---@field super_class table|diffview.Object
local Object = M.create_class("Object")
M.Object = Object
function Object:__tostring()
return fmt("<a %s>", self.class.__name)
end
-- ### CLASS METHODS ###
---@return string
function Object:name()
classm_safeguard(self)
return self.__name
end
---Check if this class is an ancestor of the given instance. `A` is an ancestor
---of `b` if - and only if - `b` is an instance of a subclass of `A`.
---@param other any
---@return boolean
function Object:ancestorof(other)
classm_safeguard(self)
if not M.is_instance(other) then return false end
return other:instanceof(self)
end
---@return string
function Object:classpath()
classm_safeguard(self)
local ret = self.__name
local cur = self.super_class
while cur do
ret = cur.__name .. "." .. ret
cur = cur.super_class
end
return ret
end
-- ### INSTANCE METHODS ###
---Call constructor.
function Object:init(...) end
---Call super constructor.
---@param ... any
function Object:super(...)
instancem_safeguard(self)
local next_super
-- Keep track of what class is currently calling the constructor such that we
-- can avoid loops.
if self.__init_caller then
next_super = self.__init_caller.super_class
else
next_super = self.super_class
end
if not next_super then return end
self.__init_caller = next_super
next_super.init(self, ...)
self.__init_caller = nil
end
---@param other diffview.Object
---@return boolean
function Object:instanceof(other)
instancem_safeguard(self)
local cur = self.class
while cur do
if cur == other then return true end
cur = cur.super_class
end
return false
end
---@param x any
---@return boolean
function M.is_class(x)
if type(x) ~= "table" then return false end
return type(rawget(x, "__name")) == "string" and x.instanceof == Object.instanceof
end
---@param x any
---@return boolean
function M.is_instance(x)
if type(x) ~= "table" then return false end
return M.is_class(x.class)
end
---@class Symbol
---@operator call : Symbol
---@field public name? string
---@field public id integer
---@field private _id_counter integer
local Symbol = M.create_class("Symbol")
M.Symbol = Symbol
---@private
Symbol._id_counter = 1
---@param name? string
function Symbol:init(name)
self.name = name
self.id = Symbol._id_counter
Symbol._id_counter = Symbol._id_counter + 1
end
function Symbol:__tostring()
if self.name then
return fmt("<Symbol('%s)>", self.name)
else
return fmt("<Symbol(#%d)>", self.id)
end
end
return M

View File

@ -0,0 +1,662 @@
local async = require("diffview.async")
local lazy = require("diffview.lazy")
local oop = require("diffview.oop")
local utils = lazy.require("diffview.utils") ---@module "diffview.utils"
local await = async.await
local fmt = string.format
local uv = vim.loop
local M = {}
local is_windows = uv.os_uname().version:match("Windows")
local function handle_uv_err(x, err, err_msg)
if not x then
error(err .. " " .. err_msg, 2)
end
return x
end
-- Ref: https://learn.microsoft.com/en-us/dotnet/standard/io/file-path-formats
local WINDOWS_PATH_SPECIFIER = {
dos_dev = "^[\\/][\\/][.?][\\/]", -- DOS Device path
unc = "^[\\/][\\/]", -- UNC path
rel_drive = "^[\\/]", -- Relative drive
drive = [[^[a-zA-Z]:]],
}
table.insert(WINDOWS_PATH_SPECIFIER, WINDOWS_PATH_SPECIFIER.dos_dev)
table.insert(WINDOWS_PATH_SPECIFIER, WINDOWS_PATH_SPECIFIER.unc)
table.insert(WINDOWS_PATH_SPECIFIER, WINDOWS_PATH_SPECIFIER.rel_drive)
table.insert(WINDOWS_PATH_SPECIFIER, WINDOWS_PATH_SPECIFIER.drive)
---@class PathLib
---@operator call : PathLib
---@field sep "/"|"\\"
---@field os "unix"|"windows" Determines the type of paths we're dealing with.
---@field cwd string Leave as `nil` to always use current cwd.
local PathLib = oop.create_class("PathLib")
function PathLib:init(o)
self.os = o.os or (is_windows and "windows" or "unix")
assert(vim.tbl_contains({ "unix", "windows" }, self.os), "Invalid OS type!")
self._is_windows = self.os == "windows"
self.sep = o.separator or (self._is_windows and "\\" or "/")
self.cwd = o.cwd and self:convert(o.cwd) or nil
end
---@private
function PathLib:_cwd()
return self.cwd or self:convert(uv.cwd())
end
---@private
---@return ...
function PathLib:_clean(...)
local argc = select("#", ...)
if argc == 1 and select(1, ...) ~= nil then
return self:convert(...)
end
local paths = { ... }
for i = 1, argc do
if paths[i] ~= nil then
paths[i] = self:convert(paths[i])
end
end
return unpack(paths, 1, argc)
end
---@private
---@param path string
function PathLib:_split_root(path)
local root = self:root(path)
if not root then return "", path end
return root, path:sub(#root + 1)
end
---Check if a given path is a URI.
---@param path string
---@return boolean
function PathLib:is_uri(path)
return string.match(path, "^%w+://") ~= nil
end
---Get the URI scheme of a given URI.
---@param path string
---@return string
function PathLib:get_uri_scheme(path)
return string.match(path, "^(%w+://)")
end
---Change the path separators in a path. Removes duplicate separators.
---@param path string
---@param sep? "/"|"\\"
---@return string
function PathLib:convert(path, sep)
sep = sep or self.sep
local prefix
local p = tostring(path)
if self:is_uri(path) then
sep = "/"
prefix, p = path:match("^(%w+://)(.*)")
elseif self._is_windows then
for _, pat in ipairs(WINDOWS_PATH_SPECIFIER) do
prefix = path:match(pat)
if prefix then
prefix = prefix:gsub("[\\/]", sep)
p = path:sub(#prefix + 1)
break
end
end
end
p, _ = p:gsub("[\\/]+", sep)
return (prefix or "") .. p
end
---Convert a path to use the appropriate path separators for the current OS.
---@param path string
---@return string
function PathLib:to_os(path)
return self:convert(path, self._is_windows and "\\" or "/")
end
---Check if a given path is absolute.
---@param path string
---@return boolean
function PathLib:is_abs(path)
path = self:_clean(path)
if self._is_windows then
for _, pat in ipairs(WINDOWS_PATH_SPECIFIER) do
if path:match(pat) ~= nil then return true end
end
return false
else
return path:sub(1, 1) == self.sep
end
end
---Get the absolute path of a given path. This is resolved using either the
---`cwd` field if it's defined. Otherwise the current cwd is used instead.
---@param path string
---@param cwd? string
---@return string
function PathLib:absolute(path, cwd)
path, cwd = self:_clean(path, cwd)
path = self:expand(path)
cwd = cwd or self:_cwd()
if self:is_uri(path) then
return path
end
if self:is_abs(path) then
return self:normalize(path, { cwd = cwd, absolute = true })
end
return self:normalize(self:join(cwd, path), { cwd = cwd, absolute = true })
end
---Check if the given path is the root.
---@param path string
---@return boolean
function PathLib:is_root(path)
path = self:remove_trailing(self:_clean(path))
if self:is_abs(path) then
if self._is_windows then
for _, pat in ipairs(WINDOWS_PATH_SPECIFIER) do
local prefix = path:match(pat)
if prefix and #path == #prefix then return true end
end
return false
else
return path == self.sep
end
end
return false
end
---Get the root of an absolute path. Returns nil if the path is not absolute.
---@param path string
---@return string|nil
function PathLib:root(path)
path = tostring(path)
if self:is_abs(path) then
if self._is_windows then
for _, pat in ipairs(WINDOWS_PATH_SPECIFIER) do
local root = path:match(pat)
if root then return root end
end
else
return self.sep
end
end
end
---@class PathLibNormalizeSpec
---@field cwd string
---@field absolute boolean
---Normalize a given path, resolving relative segments.
---@param path string
---@param opt? PathLibNormalizeSpec
---@return string
function PathLib:normalize(path, opt)
path = self:_clean(path)
if self:is_uri(path) then
return path
end
opt = opt or {}
local cwd = opt.cwd and self:_clean(opt.cwd) or self:_cwd()
local absolute = vim.F.if_nil(opt.absolute, false)
local root = self:root(path)
if root and self:is_root(path) then
return path
end
if not self:is_abs(path) then
local relpath = self:relative(path, cwd, true)
path = self:add_trailing(cwd) .. relpath
end
local parts = self:explode(path)
if root then
table.remove(parts, 1)
if self._is_windows and root == root:match(WINDOWS_PATH_SPECIFIER.rel_drive) then
-- Resolve relative drive
-- path="/foo/bar/baz", cwd="D:/lorem/ipsum" -> "D:/foo/bar/baz"
root = self:root(cwd)
end
end
local normal = path
if #parts > 1 then
local i = 2
local upc = 0
repeat
if parts[i] == "." then
table.remove(parts, i)
i = i - 1
elseif parts[i] == ".." then
if i == 1 then
upc = upc + 1
end
table.remove(parts, i)
if i > 1 then
table.remove(parts, i - 1)
i = i - 2
else
i = i - 1
end
end
i = i + 1
until i > #parts
normal = self:join(root, unpack(parts))
if not absolute and upc == 0 then
normal = self:relative(normal, cwd, true)
end
end
return normal == "" and "." or normal
end
---Expand environment variables and `~`.
---@param path string
---@return string
function PathLib:expand(path)
local segments = self:explode(path)
local idx = 1
if segments[1] == "~" then
segments[1] = uv.os_homedir()
idx = 2
end
for i = idx, #segments do
local env_var = segments[i]:match("^%$(%S+)$")
if env_var then
segments[i] = uv.os_getenv(env_var) or env_var
end
end
return self:join(unpack(segments))
end
---Joins an ordered list of path segments into a path string.
---@vararg ... string|string[] Paths
---@return string
function PathLib:join(...)
local segments = { ... }
if type(segments[1]) == "table" then
segments = segments[1]
end
local ret = ""
for i = 1, table.maxn(segments) do
local cur = segments[i]
if cur and cur ~= "" then
if #ret > 0 and not ret:sub(-1, -1):match("[\\/]") then
ret = ret .. self.sep
end
ret = ret .. cur
end
end
return self:_clean(ret)
end
---Explodes the path into an ordered list of path segments.
---@param path string
---@return string[]
function PathLib:explode(path)
path = self:_clean(path)
local parts = {}
local i = 1
if self:is_uri(path) then
local scheme, p = path:match("^(%w+://)(.*)")
parts[i] = scheme
path = p
i = i + 1
end
local root
root, path = self:_split_root(path)
if root ~= "" then
parts[i] = root
if path:sub(1, 1) == self.sep then
path = path:sub(2)
end
end
for part in path:gmatch(string.format("([^%s]+)%s?", self.sep, self.sep)) do
parts[#parts+1] = part
end
return parts
end
---Add a trailing separator, unless already present.
---@param path string
---@return string
function PathLib:add_trailing(path)
local root
root, path = self:_split_root(path)
if #path == 0 then return root .. path end
if path:sub(-1) == self.sep then
return root .. path
end
return root .. path .. self.sep
end
---Remove any trailing separator, if present.
---@param path string
---@return string
function PathLib:remove_trailing(path)
local root
root, path = self:_split_root(path)
local p, _ = path:gsub(self.sep .. "$", "")
return root .. p
end
---Get the basename of the given path.
---@param path string
---@return string
function PathLib:basename(path)
path = self:remove_trailing(self:_clean(path))
local i = path:match("^.*()" .. self.sep)
if not i then
return path
end
return path:sub(i + 1, #path)
end
---Get the extension of the given path.
---@param path string
---@return string|nil
function PathLib:extension(path)
path = self:basename(path)
return path:match(".+%.(.*)")
end
---Get the path to the parent directory of the given path. Returns `nil` if the
---path has no parent.
---@param path string
---@param n? integer Nth parent. (default: 1)
---@return string?
function PathLib:parent(path, n)
if type(n) ~= "number" or n < 1 then
n = 1
end
local parts = self:explode(path)
local root = self:root(path)
if root and n == #parts then
return root
elseif n >= #parts then
return
end
return self:join(unpack(parts, 1, #parts - n))
end
---Get a path relative to another path.
---@param path string
---@param relative_to string
---@param no_resolve? boolean Don't normalize paths first.
---@return string
function PathLib:relative(path, relative_to, no_resolve)
path, relative_to = self:_clean(path, relative_to)
if not no_resolve then
local abs = self:is_abs(path)
path = self:normalize(path, { absolute = abs })
relative_to = self:normalize(relative_to, { absolute = abs })
end
if relative_to == "" then
return path
elseif relative_to == path then
return ""
end
local p, _ = path:gsub("^" .. vim.pesc(self:add_trailing(relative_to)), "")
return p
end
---Shorten a path by truncating the head.
---@param path string
---@param max_length integer
---@return string
function PathLib:truncate(path, max_length)
path = self:_clean(path)
if #path > max_length - 1 then
path = path:sub(#path - max_length + 1, #path)
local i = path:match("()" .. self.sep)
if not i then
return "" .. path
end
return "" .. path:sub(i, -1)
else
return path
end
end
---@param path string
---@return string|nil
function PathLib:realpath(path)
local p = uv.fs_realpath(path)
if p then
return self:convert(p)
end
end
---@param path string
---@return string|nil
function PathLib:readlink(path)
local p = uv.fs_readlink(path)
if p then
return self:convert(p)
end
end
---@param path string
---@param nosuf? boolean
---@param list falsy
---@return string
---@overload fun(self: PathLib, path: string, nosuf: boolean, list: true): string[]
function PathLib:vim_expand(path, nosuf, list)
if list then
return vim.tbl_map(function(v)
return self:convert(v)
end, vim.fn.expand(path, nosuf, list))
end
return self:convert(vim.fn.expand(path, nosuf, list) --[[@as string ]])
end
---@param path string
---@return string
function PathLib:vim_fnamemodify(path, mods)
return self:convert(vim.fn.fnamemodify(path, mods))
end
---@param path string
---@return table?
function PathLib:stat(path)
return uv.fs_stat(path)
end
---@param path string
---@return string?
function PathLib:type(path)
local p = uv.fs_realpath(path)
if p then
local stat = uv.fs_stat(p)
if stat then
return stat.type
end
end
end
---@param path string
---@return boolean
function PathLib:is_dir(path)
return self:type(path) == "directory"
end
---Check for read access to a given path.
---@param path string
---@return boolean
function PathLib:readable(path)
local p = uv.fs_realpath(path)
if p then
return not not uv.fs_access(p, "R")
end
return false
end
---@class PathLib.touch.Opt
---@field mode? integer
---@field parents? boolean
---@param self PathLib
---@param path string
---@param opt PathLib.touch.Opt
PathLib.touch = async.void(function(self, path, opt)
opt = opt or {}
local mode = opt.mode or tonumber("0644", 8)
path = self:_clean(path)
local stat = self:stat(path)
if stat then
-- Path exists: just update utime
local time = os.time()
handle_uv_err(uv.fs_utime(path, time, time))
return
end
if opt.parents then
local parent = self:parent(path)
if parent then
await(self:mkdir(self:parent(path), { parents = true }))
end
end
local fd = handle_uv_err(uv.fs_open(path, "w", mode))
handle_uv_err(uv.fs_close(fd))
end)
---@class PathLib.mkdir.Opt
---@field mode? integer
---@field parents? boolean
---@param self PathLib
---@param path string
---@param opt? table
PathLib.mkdir = async.void(function(self, path, opt)
opt = opt or {}
local mode = opt.mode or tonumber("0700", 8)
path = self:absolute(path)
if not opt.parents then
handle_uv_err(uv.fs_mkdir(path, mode))
return
end
local cur_path
for _, part in ipairs(self:explode(path)) do
cur_path = cur_path and self:join(cur_path, part) or part
local stat = self:stat(cur_path)
if not stat then
handle_uv_err(uv.fs_mkdir(cur_path, mode))
else
if stat.type ~= "directory" then
error(fmt("Cannot create directory '%s': Not a directory", cur_path))
end
end
end
end)
---Delete a name and possibly the file it refers to.
---@param self PathLib
---@param path string
---@param callback? function
---@diagnostic disable-next-line: unused-local
PathLib.unlink = async.wrap(function(self, path, callback)
---@cast callback -?
uv.fs_unlink(path, function(err, ok)
if not ok then
error(err)
end
callback()
end)
end)
function PathLib:chain(...)
local t = {
__result = utils.tbl_pack(...)
}
return setmetatable(t, {
__index = function(chain, k)
if k == "get" then
return function(_)
return utils.tbl_unpack(t.__result)
end
else
return function(_, ...)
t.__result = utils.tbl_pack(self[k](self, utils.tbl_unpack(t.__result), ...))
return chain
end
end
end
})
end
M.PathLib = PathLib
return M

View File

@ -0,0 +1,92 @@
local oop = require("diffview.oop")
local utils = require("diffview.utils")
local uv = vim.loop
local M = {}
---@class PerfTimer : diffview.Object
---@operator call : PerfTimer
---@field subject string|nil
---@field first integer Start time (ns)
---@field last integer Stop time (ns)
---@field final_time number Final time (ms)
---@field laps number[] List of lap times (ms)
local PerfTimer = oop.create_class("PerfTimer")
---PerfTimer constructor.
---@param subject string|nil
function PerfTimer:init(subject)
self.subject = subject
self.laps = {}
self.first = uv.hrtime()
end
function PerfTimer:reset()
self.laps = {}
self.first = uv.hrtime()
self.final_time = nil
end
---Record a lap time.
---@param subject string|nil
function PerfTimer:lap(subject)
self.laps[#self.laps + 1] = {
subject or #self.laps + 1,
(uv.hrtime() - self.first) / 1000000,
}
end
---Set final time.
---@return number
function PerfTimer:time()
self.last = uv.hrtime() - self.first
self.final_time = self.last / 1000000
return self.final_time
end
function PerfTimer:__tostring()
if not self.final_time then
self:time()
end
if #self.laps == 0 then
return string.format(
"%s %.3f ms",
utils.str_right_pad((self.subject or "TIME") .. ":", 24),
self.final_time
)
else
local s = (self.subject or "LAPS") .. ":\n"
local last = 0
for _, lap in ipairs(self.laps) do
s = s
.. string.format(
">> %s %.3f ms\t(%.3f ms)\n",
utils.str_right_pad(lap[1], 36),
lap[2],
lap[2] - last
)
last = lap[2]
end
return s .. string.format("== %s %.3f ms", utils.str_right_pad("FINAL TIME", 36), self.final_time)
end
end
---Get the relative performance difference in percent.
---@static
---@param a PerfTimer
---@param b PerfTimer
---@return string
function PerfTimer.difference(a, b)
local delta = (b.final_time - a.final_time) / a.final_time
local negative = delta < 0
return string.format("%s%.2f%%", not negative and "+" or "", delta * 100)
end
M.PerfTimer = PerfTimer
return M

View File

@ -0,0 +1,532 @@
local oop = require("diffview.oop")
local utils = require("diffview.utils")
local api = vim.api
local M = {}
local uid_counter = 0
---Duration of the last redraw in ms.
M.last_draw_time = 0
---@class renderer.HlData
---@field group string
---@field line_idx integer
---@field first integer 0 indexed, inclusive
---@field last integer Exclusive
---@class renderer.HlList
---@field offset integer
---@field [integer] renderer.HlData
---@class CompStruct
---@field _name string
---@field comp RenderComponent
---@field [integer|string] CompStruct
---@class CompSchema
---@field name? string
---@field context? table
---@field [integer] CompSchema
---@class RenderComponent : diffview.Object
---@field name string
---@field context? table
---@field parent RenderComponent
---@field lines string[]
---@field hl renderer.HlList
---@field line_buffer string
---@field components RenderComponent[]
---@field lstart integer 0 indexed, Inclusive
---@field lend integer Exclusive
---@field height integer
---@field data_root RenderData
local RenderComponent = oop.create_class("RenderComponent")
---RenderComponent constructor.
function RenderComponent:init(name)
self.name = name or RenderComponent.next_uid()
self.lines = {}
self.hl = {}
self.line_buffer = ""
self.components = {}
self.lstart = -1
self.lend = -1
self.height = 0
end
---@param parent RenderComponent
---@param comp_struct CompStruct
---@param schema CompSchema
local function create_subcomponents(parent, comp_struct, schema)
for i, v in ipairs(schema) do
v.name = v.name or RenderComponent.next_uid()
local sub_comp = parent:create_component()
---@cast sub_comp RenderComponent
sub_comp.name = v.name
sub_comp.context = v.context
sub_comp.parent = parent
comp_struct[i] = {
_name = v.name,
comp = sub_comp,
}
comp_struct[v.name] = comp_struct[i]
if #v > 0 then
create_subcomponents(sub_comp, comp_struct[i], v)
end
end
end
function RenderComponent.next_uid()
local uid = "comp_" .. uid_counter
uid_counter = uid_counter + 1
return uid
end
---Create a new compoenent
---@param schema? CompSchema
---@return RenderComponent, CompStruct
function RenderComponent.create_static_component(schema)
local comp_struct
---@diagnostic disable-next-line: need-check-nil
local new_comp = RenderComponent(schema and schema.name or nil)
if schema then
new_comp.context = schema.context
comp_struct = { _name = new_comp.name, comp = new_comp }
create_subcomponents(new_comp, comp_struct, schema)
end
return new_comp, comp_struct
end
---Create and add a new component.
---@param schema? CompSchema
---@overload fun(): RenderComponent
---@overload fun(schema: CompSchema): CompStruct
function RenderComponent:create_component(schema)
local new_comp, comp_struct = RenderComponent.create_static_component(schema)
new_comp.data_root = self.data_root
self:add_component(new_comp)
if comp_struct then
return comp_struct
end
return new_comp
end
---@param component RenderComponent
function RenderComponent:add_component(component)
component.parent = self
self.components[#self.components + 1] = component
end
---@param component RenderComponent
function RenderComponent:remove_component(component)
for i, c in ipairs(self.components) do
if c == component then
table.remove(self.components, i)
return true
end
end
return false
end
---@param line string?
---@param hl_group string?
function RenderComponent:add_line(line, hl_group)
if line and hl_group then
local first = #self.line_buffer
self:add_hl(hl_group, #self.lines, first, first + #line)
end
self.lines[#self.lines + 1] = self.line_buffer .. (line or "")
self.line_buffer = ""
end
---@param group string
---@param line_idx integer
---@param first integer
---@param last integer
function RenderComponent:add_hl(group, line_idx, first, last)
self.hl[#self.hl + 1] = {
group = group,
line_idx = line_idx,
first = first,
last = last,
}
end
---@param text string
---@param hl_group string?
function RenderComponent:add_text(text, hl_group)
if hl_group then
local first = #self.line_buffer
self:add_hl(hl_group, #self.lines, first, first + #text)
end
self.line_buffer = self.line_buffer .. text
end
---Finalize current line
function RenderComponent:ln()
self.lines[#self.lines + 1] = self.line_buffer
self.line_buffer = ""
end
function RenderComponent:clear()
self.lines = {}
self.hl = {}
self.lstart = -1
self.lend = -1
self.height = 0
for _, c in ipairs(self.components) do
c:clear()
end
end
function RenderComponent:destroy()
self.lines = nil
self.hl = nil
self.parent = nil
self.context = nil
self.data_root = nil
for _, c in ipairs(self.components) do
c:destroy()
end
self.components = nil
end
function RenderComponent:isleaf()
return (not next(self.components))
end
---@param line integer
---@return RenderComponent?
function RenderComponent:get_comp_on_line(line)
line = line - 1
local ret
self:deep_some(function(child)
if line >= child.lstart and line < child.lend and child:isleaf() then
ret = child
return true
end
end)
return ret
end
---@param callback fun(comp: RenderComponent, i: integer, parent: RenderComponent): boolean?
function RenderComponent:some(callback)
for i, child in ipairs(self.components) do
if callback(child, i, self) then
return
end
end
end
---@param callback fun(comp: RenderComponent, i: integer, parent: RenderComponent): boolean?
function RenderComponent:deep_some(callback)
local function wrap(comp, i, parent)
if callback(comp, i, parent) then
return true
else
return comp:some(wrap)
end
end
self:some(wrap)
end
function RenderComponent:leaves()
local leaves = {}
self:deep_some(function(comp)
if #comp.components == 0 then
leaves[#leaves + 1] = comp
end
return false
end)
return leaves
end
function RenderComponent:pretty_print()
local keys = { "name", "lstart", "lend" }
local function recurse(depth, comp)
local outer_padding = string.rep(" ", depth * 2)
print(outer_padding .. "{")
local inner_padding = outer_padding .. " "
for _, k in ipairs(keys) do
print(string.format("%s%s = %s,", inner_padding, k, vim.inspect(comp[k])))
end
if #comp.lines > 0 then
print(string.format("%slines = {", inner_padding))
for _, line in ipairs(comp.lines) do
print(string.format("%s %s,", inner_padding, vim.inspect(line)))
end
print(string.format("%s},", inner_padding))
end
for _, child in ipairs(comp.components) do
recurse(depth + 1, child)
end
print(outer_padding .. "},")
end
recurse(0, self)
end
---@class RenderData : diffview.Object
---@field lines string[]
---@field hl renderer.HlList
---@field components RenderComponent[]
---@field namespace integer
local RenderData = oop.create_class("RenderData")
---RenderData constructor.
function RenderData:init(ns_name)
self.lines = {}
self.hl = {}
self.components = {}
self.namespace = api.nvim_create_namespace(ns_name)
end
---Create and add a new component.
---@param schema table
---@return RenderComponent|CompStruct
function RenderData:create_component(schema)
local comp_struct
local new_comp = RenderComponent(schema and schema.name or nil)
new_comp.data_root = self
self:add_component(new_comp)
if schema then
new_comp.context = schema.context
comp_struct = { _name = new_comp.name, comp = new_comp }
create_subcomponents(new_comp, comp_struct, schema)
return comp_struct
end
return new_comp
end
---@param component RenderComponent
function RenderData:add_component(component)
self.components[#self.components + 1] = component
end
---@param component RenderComponent
function RenderData:remove_component(component)
for i, c in ipairs(self.components) do
if c == component then
table.remove(self.components, i)
return true
end
end
return false
end
---@param group string
---@param line_idx integer
---@param first integer
---@param last integer
function RenderData:add_hl(group, line_idx, first, last)
self.hl[#self.hl + 1] = {
group = group,
line_idx = line_idx,
first = first,
last = last,
}
end
function RenderData:clear()
self.lines = {}
self.hl = {}
for _, c in ipairs(self.components) do
c:clear()
end
end
function RenderData:destroy()
self.lines = nil
self.hl = nil
for _, c in ipairs(self.components) do
c:destroy()
end
self.components = {}
end
function M.destroy_comp_struct(schema)
schema.comp = nil
for k, v in pairs(schema) do
if type(v) == "table" then
M.destroy_comp_struct(v)
schema[k] = nil
end
end
end
---Create a function to enable easily constraining the cursor to a given list of
---components.
---@param components RenderComponent[]
function M.create_cursor_constraint(components)
local stack = utils.vec_slice(components, 1)
utils.merge_sort(stack, function(a, b)
return a.lstart <= b.lstart
end)
---Given a cursor delta or target: returns the next valid line index inside a
---contraining component. When the cursor is trying to move out of a
---constraint, the next component is determined by the direction the cursor is
---moving.
---@param winid_or_opt number|{from: number, to: number}
---@param delta number The amount of change from the current cursor position.
---Not needed if the first argument is a table.
---@return number
return function(winid_or_opt, delta)
local line_from, line_to
if type(winid_or_opt) == "number" then
local cursor = api.nvim_win_get_cursor(winid_or_opt)
line_from, line_to = cursor[1] - 1, cursor[1] - 1 + delta
else
line_from, line_to = winid_or_opt.from - 1, winid_or_opt.to - 1
end
local min, max = math.min(line_from, line_to), math.max(line_from, line_to)
local nearest_dist, dist, target = math.huge, nil, {}
local top, bot
local fstack = {}
for _, comp in ipairs(stack) do
if comp.height > 0 then
fstack[#fstack + 1] = comp
if min <= comp.lend and max >= comp.lstart then
if not top then
top = { idx = #fstack, comp = comp }
bot = top
else
bot = { idx = #fstack, comp = comp }
end
end
dist = math.min(math.abs(line_to - comp.lstart), math.abs(line_to - comp.lend))
if dist < nearest_dist then
nearest_dist = dist
target = { idx = #fstack, comp = comp }
end
end
end
if not top and target.comp then
return utils.clamp(line_to + 1, target.comp.lstart + 1, target.comp.lend)
elseif top then
if line_to < line_from then
-- moving up
if line_to < top.comp.lstart and top.idx > 1 then
target = { idx = top.idx - 1, comp = fstack[top.idx - 1] }
else
target = top
end
return utils.clamp(line_to + 1, target.comp.lstart + 1, target.comp.lend)
else
-- moving down
if line_to >= bot.comp.lend and bot.idx < #fstack then
target = { idx = bot.idx + 1, comp = fstack[bot.idx + 1] }
else
target = bot
end
return utils.clamp(line_to + 1, target.comp.lstart + 1, target.comp.lend)
end
end
return line_from
end
end
---@param line_idx integer
---@param lines string[]
---@param hl_data renderer.HlData[]
---@param component RenderComponent
---@return integer
local function process_component(line_idx, lines, hl_data, component)
if #component.components > 0 then
component.lstart = line_idx
for _, c in ipairs(component.components) do
line_idx = process_component(line_idx, lines, hl_data, c)
end
component.lend = line_idx
component.height = component.lend - component.lstart
return line_idx
else
for _, line in ipairs(component.lines) do
lines[#lines + 1] = line
end
component.hl.offset = line_idx
hl_data[#hl_data + 1] = component.hl
component.height = #component.lines
if component.height > 0 then
component.lstart = line_idx
component.lend = line_idx + component.height
else
component.lstart = line_idx
component.lend = line_idx
end
return component.lend
end
end
---Render the given render data to the given buffer.
---@param bufid integer
---@param data RenderData
function M.render(bufid, data)
if not api.nvim_buf_is_loaded(bufid) then
return
end
local last = vim.loop.hrtime()
local was_modifiable = api.nvim_buf_get_option(bufid, "modifiable")
api.nvim_buf_set_option(bufid, "modifiable", true)
local lines, hl_data
local line_idx = 0
if #data.components > 0 then
lines = {}
hl_data = {}
for _, c in ipairs(data.components) do
line_idx = process_component(line_idx, lines, hl_data, c)
end
else
lines = data.lines
hl_data = { data.hl }
end
api.nvim_buf_set_lines(bufid, 0, -1, false, lines)
api.nvim_buf_clear_namespace(bufid, data.namespace, 0, -1)
for _, t in ipairs(hl_data) do
for _, hl in ipairs(t) do
api.nvim_buf_add_highlight(
bufid,
data.namespace,
hl.group,
hl.line_idx + (t.offset or 0),
hl.first,
hl.last
)
end
end
api.nvim_buf_set_option(bufid, "modifiable", was_modifiable)
M.last_draw_time = (vim.loop.hrtime() - last) / 1000000
end
M.RenderComponent = RenderComponent
M.RenderData = RenderData
return M

View File

@ -0,0 +1,50 @@
local oop = require("diffview.oop")
---@class Scanner : diffview.Object
---@operator call : Scanner
---@field lines string[]
---@field line_idx integer
local Scanner = oop.create_class("Scanner")
---@param source string|string[]
function Scanner:init(source)
if type(source) == "table" then
self.lines = source
else
self.lines = vim.split(source, "\r?\n")
end
self.line_idx = 0
end
---Peek the nth line after the current line.
---@param n? integer # (default: 1)
---@return string?
function Scanner:peek_line(n)
return self.lines[self.line_idx + math.max(1, n or 1)]
end
function Scanner:cur_line()
return self.lines[self.line_idx]
end
function Scanner:cur_line_idx()
return self.line_idx
end
---Advance the scanner to the next line.
---@return string?
function Scanner:next_line()
self.line_idx = self.line_idx + 1
return self.lines[self.line_idx]
end
---Advance the scanner by n lines.
---@param n? integer # (default: 1)
---@return string?
function Scanner:skip_line(n)
self.line_idx = self.line_idx + math.max(1, n or 1)
return self.lines[self.line_idx]
end
return Scanner

View File

@ -0,0 +1,360 @@
local lazy = require("diffview.lazy")
local oop = require("diffview.oop")
local Diff1 = lazy.access("diffview.scene.layouts.diff_1", "Diff1") ---@type Diff1|LazyModule
local Diff2 = lazy.access("diffview.scene.layouts.diff_2", "Diff2") ---@type Diff2|LazyModule
local File = lazy.access("diffview.vcs.file", "File") ---@type vcs.File|LazyModule
local RevType = lazy.access("diffview.vcs.rev", "RevType") ---@type RevType|LazyModule
local utils = lazy.require("diffview.utils") ---@module "diffview.utils"
local api = vim.api
local pl = lazy.access(utils, "path") ---@type PathLib
local M = {}
local fstat_cache = {}
---@class GitStats
---@field additions integer
---@field deletions integer
---@field conflicts integer
---@class RevMap
---@field a Rev
---@field b Rev
---@field c Rev
---@field d Rev
---@class FileEntry : diffview.Object
---@field adapter GitAdapter
---@field path string
---@field oldpath string
---@field absolute_path string
---@field parent_path string
---@field basename string
---@field extension string
---@field revs RevMap
---@field layout Layout
---@field status string
---@field stats GitStats
---@field kind vcs.FileKind
---@field commit Commit|nil
---@field merge_ctx vcs.MergeContext?
---@field active boolean
---@field opened boolean
local FileEntry = oop.create_class("FileEntry")
---@class FileEntry.init.Opt
---@field adapter GitAdapter
---@field path string
---@field oldpath string
---@field revs RevMap
---@field layout Layout
---@field status string
---@field stats GitStats
---@field kind vcs.FileKind
---@field commit? Commit
---@field merge_ctx? vcs.MergeContext
---FileEntry constructor
---@param opt FileEntry.init.Opt
function FileEntry:init(opt)
self.adapter = opt.adapter
self.path = opt.path
self.oldpath = opt.oldpath
self.absolute_path = pl:absolute(opt.path, opt.adapter.ctx.toplevel)
self.parent_path = pl:parent(opt.path) or ""
self.basename = pl:basename(opt.path)
self.extension = pl:extension(opt.path)
self.revs = opt.revs
self.layout = opt.layout
self.status = opt.status
self.stats = opt.stats
self.kind = opt.kind
self.commit = opt.commit
self.merge_ctx = opt.merge_ctx
self.active = false
self.opened = false
end
function FileEntry:destroy()
for _, f in ipairs(self.layout:files()) do
f:destroy()
end
self.layout:destroy()
end
---@param new_head Rev
function FileEntry:update_heads(new_head)
for _, file in ipairs(self.layout:files()) do
if file.rev.track_head then
file:dispose_buffer()
file.rev = new_head
end
end
end
---@param flag boolean
function FileEntry:set_active(flag)
self.active = flag
for _, f in ipairs(self.layout:files()) do
f.active = flag
end
end
---@param target_layout Layout
function FileEntry:convert_layout(target_layout)
local get_data
for _, file in ipairs(self.layout:files()) do
if file.get_data then
get_data = file.get_data
break
end
end
local function create_file(rev, symbol)
return File({
adapter = self.adapter,
path = symbol == "a" and self.oldpath or self.path,
kind = self.kind,
commit = self.commit,
get_data = get_data,
rev = rev,
nulled = select(2, pcall(target_layout.should_null, rev, self.status, symbol)),
}) --[[@as vcs.File ]]
end
self.layout = target_layout({
a = utils.tbl_access(self.layout, "a.file") or create_file(self.revs.a, "a"),
b = utils.tbl_access(self.layout, "b.file") or create_file(self.revs.b, "b"),
c = utils.tbl_access(self.layout, "c.file") or create_file(self.revs.c, "c"),
d = utils.tbl_access(self.layout, "d.file") or create_file(self.revs.d, "d"),
})
self:update_merge_context()
end
---@param stat? table
function FileEntry:validate_stage_buffers(stat)
stat = stat or pl:stat(pl:join(self.adapter.ctx.dir, "index"))
local cached_stat = utils.tbl_access(fstat_cache, { self.adapter.ctx.toplevel, "index" })
if stat and (not cached_stat or cached_stat.mtime < stat.mtime.sec) then
for _, f in ipairs(self.layout:files()) do
if f.rev.type == RevType.STAGE and f:is_valid() then
if f.rev.stage > 0 then
-- We only care about stage 0 here
f:dispose_buffer()
else
local is_modified = vim.bo[f.bufnr].modified
if f.blob_hash then
local new_hash = self.adapter:file_blob_hash(f.path)
if new_hash and new_hash ~= f.blob_hash then
if is_modified then
utils.warn((
"A file was changed in the index since you started editing it!"
.. " Be careful not to lose any staged changes when writing to this buffer: %s"
):format(api.nvim_buf_get_name(f.bufnr)))
else
f:dispose_buffer()
end
end
elseif not is_modified then
-- Should be very rare that we don't have an index-buffer's blob
-- hash. But in that case, we can't warn the user when a file
-- changes in the index while they're editing its index buffer.
f:dispose_buffer()
end
end
end
end
end
end
---Update winbar info
---@param ctx? vcs.MergeContext
function FileEntry:update_merge_context(ctx)
ctx = ctx or self.merge_ctx
if ctx then self.merge_ctx = ctx else return end
local layout = self.layout --[[@as Diff4 ]]
if layout.a then
layout.a.file.winbar = (" OURS (Current changes) %s %s"):format(
(ctx.ours.hash):sub(1, 10),
ctx.ours.ref_names and ("(" .. ctx.ours.ref_names .. ")") or ""
)
end
if layout.b then
layout.b.file.winbar = " LOCAL (Working tree)"
end
if layout.c then
layout.c.file.winbar = (" THEIRS (Incoming changes) %s %s"):format(
(ctx.theirs.hash):sub(1, 10),
ctx.theirs.ref_names and ("(" .. ctx.theirs.ref_names .. ")") or ""
)
end
if layout.d then
layout.d.file.winbar = (" BASE (Common ancestor) %s %s"):format(
(ctx.base.hash):sub(1, 10),
ctx.base.ref_names and ("(" .. ctx.base.ref_names .. ")") or ""
)
end
end
---Derive custom folds from the hunks in a diff patch.
---@param diff diff.FileEntry
function FileEntry:update_patch_folds(diff)
if not self.layout:instanceof(Diff2.__get()) then return end
local layout = self.layout --[[@as Diff2 ]]
local folds = {
a = utils.tbl_set(layout.a.file, { "custom_folds" }, { type = "diff_patch" }),
b = utils.tbl_set(layout.b.file, { "custom_folds" }, { type = "diff_patch" }),
}
local lcount_a = api.nvim_buf_line_count(layout.a.file.bufnr)
local lcount_b = api.nvim_buf_line_count(layout.b.file.bufnr)
local prev_last_old, prev_last_new = 0, 0
for i = 1, #diff.hunks + 1 do
local hunk = diff.hunks[i]
local first_old, last_old, first_new, last_new
if hunk then
first_old = hunk.old_row
last_old = first_old + hunk.old_size - 1
first_new = hunk.new_row
last_new = first_new + hunk.new_size - 1
else
first_old = lcount_a + 1
first_new = lcount_b + 1
end
if first_old - prev_last_old > 1 then
local prev_fold = folds.a[#folds.a]
if prev_fold and (prev_last_old + 1) - prev_fold[2] == 1 then
-- This fold is right next to the previous: merge the folds
prev_fold[2] = first_old - 1
else
table.insert(folds.a, { prev_last_old + 1, first_old - 1 })
end
-- print("old:", folds.a[#folds.a][1], folds.a[#folds.a][2])
end
if first_new - prev_last_new > 1 then
local prev_fold = folds.b[#folds.b]
if prev_fold and (prev_last_new + 1) - prev_fold[2] == 1 then
-- This fold is right next to the previous: merge the folds
prev_fold[2] = first_new - 1
else
table.insert(folds.b, { prev_last_new + 1, first_new - 1 })
end
-- print("new:", folds.b[#folds.b][1], folds.b[#folds.b][2])
end
prev_last_old = last_old
prev_last_new = last_new
end
end
---Check if the entry has custom diff patch folds.
---@return boolean
function FileEntry:has_patch_folds()
for _, file in ipairs(self.layout:files()) do
if not file.custom_folds or file.custom_folds.type ~= "diff_patch" then
return false
end
end
return true
end
---@return boolean
function FileEntry:is_null_entry()
return self.path == "null" and self.layout:get_main_win().file == File.NULL_FILE
end
---@static
---@param adapter VCSAdapter
function FileEntry.update_index_stat(adapter, stat)
stat = stat or pl:stat(pl:join(adapter.ctx.toplevel, "index"))
if stat then
if not fstat_cache[adapter.ctx.toplevel] then
fstat_cache[adapter.ctx.toplevel] = {}
end
fstat_cache[adapter.ctx.toplevel].index = {
mtime = stat.mtime.sec,
}
end
end
---@class FileEntry.with_layout.Opt : FileEntry.init.Opt
---@field nulled boolean
---@field get_data git.FileDataProducer?
---@param layout_class Layout (class)
---@param opt FileEntry.with_layout.Opt
---@return FileEntry
function FileEntry.with_layout(layout_class, opt)
local function create_file(rev, symbol)
return File({
adapter = opt.adapter,
path = symbol == "a" and opt.oldpath or opt.path,
kind = opt.kind,
commit = opt.commit,
get_data = opt.get_data,
rev = rev,
nulled = utils.sate(
opt.nulled,
select(2, pcall(layout_class.should_null, rev, opt.status, symbol))
),
}) --[[@as vcs.File ]]
end
return FileEntry({
adapter = opt.adapter,
path = opt.path,
oldpath = opt.oldpath,
status = opt.status,
stats = opt.stats,
kind = opt.kind,
commit = opt.commit,
revs = opt.revs,
layout = layout_class({
a = create_file(opt.revs.a, "a"),
b = create_file(opt.revs.b, "b"),
c = create_file(opt.revs.c, "c"),
d = create_file(opt.revs.d, "d"),
}),
})
end
function FileEntry.new_null_entry(adapter)
return FileEntry({
adapter = adapter,
path = "null",
kind = "working",
binary = false,
nulled = true,
layout = Diff1({
b = File.NULL_FILE,
})
})
end
M.FileEntry = FileEntry
return M

View File

@ -0,0 +1,312 @@
local async = require("diffview.async")
local lazy = require("diffview.lazy")
local oop = require("diffview.oop")
local EventEmitter = lazy.access("diffview.events", "EventEmitter") ---@type EventEmitter|LazyModule
local utils = lazy.require("diffview.utils") ---@module "diffview.utils"
local api = vim.api
local await = async.await
local M = {}
---@class Layout : diffview.Object
---@field windows Window[]
---@field emitter EventEmitter
---@field pivot_producer fun(): integer?
---@field name string
---@field state table
local Layout = oop.create_class("Layout")
function Layout:init(opt)
opt = opt or {}
self.windows = opt.windows or {}
self.emitter = opt.emitter or EventEmitter()
self.state = {}
end
---@diagnostic disable: unused-local, missing-return
---@abstract
---@param self Layout
---@param pivot? integer The window ID of the window around which the layout will be created.
Layout.create = async.void(function(self, pivot) oop.abstract_stub() end)
---@abstract
---@param rev Rev
---@param status string Git status symbol.
---@param sym string
---@return boolean
function Layout.should_null(rev, status, sym) oop.abstract_stub() end
---@abstract
---@param self Layout
---@param entry FileEntry
Layout.use_entry = async.void(function(self, entry) oop.abstract_stub() end)
---@abstract
---@return Window
function Layout:get_main_win() oop.abstract_stub() end
---@diagnostic enable: unused-local, missing-return
function Layout:destroy()
for _, win in ipairs(self.windows) do
win:destroy()
end
end
function Layout:clone()
local clone = self.class({ emitter = self.emitter }) --[[@as Layout ]]
for i, win in ipairs(self.windows) do
clone.windows[i]:set_id(win.id)
clone.windows[i]:set_file(win.file)
end
return clone
end
function Layout:create_pre()
self.state.save_equalalways = vim.o.equalalways
vim.opt.equalalways = true
end
---@param self Layout
Layout.create_post = async.void(function(self)
await(self:open_files())
vim.opt.equalalways = self.state.save_equalalways
end)
---Check if any of the windows in the lauout are focused.
---@return boolean
function Layout:is_focused()
for _, win in ipairs(self.windows) do
if win:is_focused() then return true end
end
return false
end
---@param ... Window
function Layout:use_windows(...)
local wins = { ... }
for i = 1, select("#", ...) do
local win = wins[i]
win.parent = self
if utils.vec_indexof(self.windows, win) == -1 then
table.insert(self.windows, win)
end
end
end
---Find or create a window that can be used as a pivot during layout
---creation.
---@return integer winid
function Layout:find_pivot()
local last_win = api.nvim_get_current_win()
for _, win in ipairs(self.windows) do
if win:is_valid() then
local ret
api.nvim_win_call(win.id, function()
vim.cmd("aboveleft vsp")
ret = api.nvim_get_current_win()
end)
return ret
end
end
if vim.is_callable(self.pivot_producer) then
local ret = self.pivot_producer()
if ret then
return ret
end
end
vim.cmd("1windo belowright vsp")
local pivot = api.nvim_get_current_win()
if api.nvim_win_is_valid(last_win) then
api.nvim_set_current_win(last_win)
end
return pivot
end
---@return vcs.File[]
function Layout:files()
return utils.tbl_fmap(self.windows, function(v)
return v.file
end)
end
---Check if the buffers for all the files in the layout are loaded.
---@return boolean
function Layout:is_files_loaded()
for _, f in ipairs(self:files()) do
if not f:is_valid() then
return false
end
end
return true
end
---@param self Layout
Layout.open_files = async.void(function(self)
if #self:files() < #self.windows then
self:open_null()
self.emitter:emit("files_opened")
return
end
vim.cmd("diffoff!")
if not self:is_files_loaded() then
self:open_null()
-- Wait for all files to be loaded before opening
for _, win in ipairs(self.windows) do
await(win:load_file())
end
end
await(async.scheduler())
for _, win in ipairs(self.windows) do
await(win:open_file())
end
self:sync_scroll()
self.emitter:emit("files_opened")
end)
function Layout:open_null()
for _, win in ipairs(self.windows) do
win:open_null()
end
end
---Recover a broken layout.
---@param pivot? integer
function Layout:recover(pivot)
pivot = pivot or self:find_pivot()
---@cast pivot -?
for _, win in ipairs(self.windows) do
if win.id ~= pivot then
pcall(api.nvim_win_close, win.id, true)
end
end
self.windows = {}
self:create(pivot)
end
---@alias Layout.State { [Window]: boolean, valid: boolean }
---Check the validity of all composing layout windows.
---@return Layout.State
function Layout:validate()
if not next(self.windows) then
return { valid = false }
end
local state = { valid = true }
for _, win in ipairs(self.windows) do
state[win] = win:is_valid()
if not state[win] then
state.valid = false
end
end
return state
end
---Check the validity if the layout.
---@return boolean
function Layout:is_valid()
return self:validate().valid
end
---@return boolean
function Layout:is_nulled()
if not self:is_valid() then return false end
for _, win in ipairs(self.windows) do
if not win:is_nulled() then return false end
end
return true
end
---Validate the layout and recover if necessary.
function Layout:ensure()
local state = self:validate()
if not state.valid then
self:recover()
end
end
---Save window local options.
function Layout:save_winopts()
for _, win in ipairs(self.windows) do
win:_save_winopts()
end
end
---Restore saved window local options.
function Layout:restore_winopts()
for _, win in ipairs(self.windows) do
win:_restore_winopts()
end
end
function Layout:detach_files()
for _, win in ipairs(self.windows) do
win:detach_file()
end
end
---Sync the scrollbind.
function Layout:sync_scroll()
local curwin = api.nvim_get_current_win()
local target, max = nil, 0
for _, win in ipairs(self.windows) do
local lcount = api.nvim_buf_line_count(api.nvim_win_get_buf(win.id))
if lcount > max then target, max = win, lcount end
end
local main_win = self:get_main_win()
local cursor = api.nvim_win_get_cursor(main_win.id)
for _, win in ipairs(self.windows) do
api.nvim_win_call(win.id, function()
if win == target then
-- Scroll to trigger the scrollbind and sync the windows. This works more
-- consistently than calling `:syncbind`.
vim.cmd("norm! " .. api.nvim_replace_termcodes("<c-e><c-y>", true, true, true))
end
if win.id ~= curwin then
api.nvim_exec_autocmds("WinLeave", { modeline = false })
end
end)
end
-- Cursor will sometimes move +- the value of 'scrolloff'
api.nvim_win_set_cursor(target.id, cursor)
end
M.Layout = Layout
return M

View File

@ -0,0 +1,169 @@
local async = require("diffview.async")
local lazy = require("diffview.lazy")
local Layout = require("diffview.scene.layout").Layout
local oop = require("diffview.oop")
local Diff3 = lazy.access("diffview.scene.layouts.diff_3", "Diff3") ---@type Diff3|LazyModule
local Diff4 = lazy.access("diffview.scene.layouts.diff_4", "Diff4") ---@type Diff4|LazyModule
local File = lazy.access("diffview.vcs.file", "File") ---@type vcs.File|LazyModule
local Rev = lazy.access("diffview.vcs.rev", "Rev") ---@type Rev|LazyModule
local RevType = lazy.access("diffview.vcs.rev", "RevType") ---@type RevType|LazyModule
local Window = lazy.access("diffview.scene.window", "Window") ---@type Window|LazyModule
local api = vim.api
local await = async.await
local M = {}
---@class Diff1 : Layout
---@field b Window
local Diff1 = oop.create_class("Diff1", Layout)
---@alias Diff1.WindowSymbol "b"
---@class Diff1.init.Opt
---@field b vcs.File
---@field winid_b integer
Diff1.name = "diff1_plain"
---@param opt Diff1.init.Opt
function Diff1:init(opt)
self:super()
self.b = Window({ file = opt.b, id = opt.winid_b })
self:use_windows(self.b)
end
---@override
---@param self Diff1
---@param pivot integer?
Diff1.create = async.void(function(self, pivot)
self:create_pre()
local curwin
pivot = pivot or self:find_pivot()
assert(api.nvim_win_is_valid(pivot), "Layout creation requires a valid window pivot!")
for _, win in ipairs(self.windows) do
if win.id ~= pivot then
win:close(true)
end
end
api.nvim_win_call(pivot, function()
vim.cmd("aboveleft vsp")
curwin = api.nvim_get_current_win()
if self.b then
self.b:set_id(curwin)
else
self.b = Window({ id = curwin })
end
end)
api.nvim_win_close(pivot, true)
self.windows = { self.b }
await(self:create_post())
end)
---@param file vcs.File
function Diff1:set_file_b(file)
self.b:set_file(file)
file.symbol = "b"
end
---@param self Diff1
---@param entry FileEntry
Diff1.use_entry = async.void(function(self, entry)
local layout = entry.layout --[[@as Diff1 ]]
assert(layout:instanceof(Diff1))
self:set_file_b(layout.b.file)
if self:is_valid() then
await(self:open_files())
end
end)
function Diff1:get_main_win()
return self.b
end
---@param layout Diff3
---@return Diff3
function Diff1:to_diff3(layout)
assert(layout:instanceof(Diff3.__get()))
local main = self:get_main_win().file
return layout({
a = File({
adapter = main.adapter,
path = main.path,
kind = main.kind,
commit = main.commit,
get_data = main.get_data,
rev = Rev(RevType.STAGE, 2),
nulled = false, -- FIXME
}),
b = self.b.file,
c = File({
adapter = main.adapter,
path = main.path,
kind = main.kind,
commit = main.commit,
get_data = main.get_data,
rev = Rev(RevType.STAGE, 3),
nulled = false, -- FIXME
}),
})
end
---@param layout Diff4
---@return Diff4
function Diff1:to_diff4(layout)
assert(layout:instanceof(Diff4.__get()))
local main = self:get_main_win().file
return layout({
a = File({
adapter = main.adapter,
path = main.path,
kind = main.kind,
commit = main.commit,
get_data = main.get_data,
rev = Rev(RevType.STAGE, 2),
nulled = false, -- FIXME
}),
b = self.b.file,
c = File({
adapter = main.adapter,
path = main.path,
kind = main.kind,
commit = main.commit,
get_data = main.get_data,
rev = Rev(RevType.STAGE, 3),
nulled = false, -- FIXME
}),
d = File({
adapter = main.adapter,
path = main.path,
kind = main.kind,
commit = main.commit,
get_data = main.get_data,
rev = Rev(RevType.STAGE, 1),
nulled = false, -- FIXME
})
})
end
---FIXME
---@override
---@param rev Rev
---@param status string Git status symbol.
---@param sym Diff1.WindowSymbol
function Diff1.should_null(rev, status, sym)
return false
end
M.Diff1 = Diff1
return M

View File

@ -0,0 +1,91 @@
local async = require("diffview.async")
local RevType = require("diffview.vcs.rev").RevType
local Window = require("diffview.scene.window").Window
local Layout = require("diffview.scene.layout").Layout
local oop = require("diffview.oop")
local await = async.await
local M = {}
---@class Diff2 : Layout
---@field a Window
---@field b Window
local Diff2 = oop.create_class("Diff2", Layout)
---@alias Diff2.WindowSymbol "a"|"b"
---@class Diff2.init.Opt
---@field a vcs.File
---@field b vcs.File
---@field winid_a integer
---@field winid_b integer
---@param opt Diff2.init.Opt
function Diff2:init(opt)
self:super()
self.a = Window({ file = opt.a, id = opt.winid_a })
self.b = Window({ file = opt.b, id = opt.winid_b })
self:use_windows(self.a, self.b)
end
---@param file vcs.File
function Diff2:set_file_a(file)
self.a:set_file(file)
file.symbol = "a"
end
---@param file vcs.File
function Diff2:set_file_b(file)
self.b:set_file(file)
file.symbol = "b"
end
---@param self Diff2
---@param entry FileEntry
Diff2.use_entry = async.void(function(self, entry)
local layout = entry.layout --[[@as Diff2 ]]
assert(layout:instanceof(Diff2))
self:set_file_a(layout.a.file)
self:set_file_b(layout.b.file)
if self:is_valid() then
await(self:open_files())
end
end)
function Diff2:get_main_win()
return self.b
end
---@override
---@param rev Rev
---@param status string Git status symbol.
---@param sym Diff2.WindowSymbol
function Diff2.should_null(rev, status, sym)
assert(sym == "a" or sym == "b")
if rev.type == RevType.LOCAL then
return status == "D"
elseif rev.type == RevType.COMMIT then
if sym == "a" then
return vim.tbl_contains({ "?", "A" }, status)
end
return false
elseif rev.type == RevType.STAGE then
if sym == "a" then
return vim.tbl_contains({ "?", "A" }, status)
elseif sym == "b" then
return status == "D"
end
end
error(("Unexpected state! %s, %s, %s"):format(rev, status, sym))
end
M.Diff2 = Diff2
return M

View File

@ -0,0 +1,71 @@
local async = require("diffview.async")
local Window = require("diffview.scene.window").Window
local Diff2 = require("diffview.scene.layouts.diff_2").Diff2
local oop = require("diffview.oop")
local api = vim.api
local await = async.await
local M = {}
---@class Diff2Hor : Diff2
local Diff2Hor = oop.create_class("Diff2Hor", Diff2)
Diff2Hor.name = "diff2_horizontal"
---@class Diff2Hor.init.Opt
---@field a vcs.File
---@field b vcs.File
---@field winid_a integer
---@field winid_b integer
---@param opt Diff2Hor.init.Opt
function Diff2Hor:init(opt)
self:super(opt)
end
---@override
---@param self Diff2Hor
---@param pivot integer?
Diff2Hor.create = async.void(function(self, pivot)
self:create_pre()
local curwin
pivot = pivot or self:find_pivot()
assert(api.nvim_win_is_valid(pivot), "Layout creation requires a valid window pivot!")
for _, win in ipairs(self.windows) do
if win.id ~= pivot then
win:close(true)
end
end
api.nvim_win_call(pivot, function()
vim.cmd("aboveleft vsp")
curwin = api.nvim_get_current_win()
if self.a then
self.a:set_id(curwin)
else
self.a = Window({ id = curwin })
end
end)
api.nvim_win_call(pivot, function()
vim.cmd("aboveleft vsp")
curwin = api.nvim_get_current_win()
if self.b then
self.b:set_id(curwin)
else
self.b = Window({ id = curwin })
end
end)
api.nvim_win_close(pivot, true)
self.windows = { self.a, self.b }
await(self:create_post())
end)
M.Diff2Hor = Diff2Hor
return M

View File

@ -0,0 +1,73 @@
local async = require("diffview.async")
local Window = require("diffview.scene.window").Window
local Diff2 = require("diffview.scene.layouts.diff_2").Diff2
local oop = require("diffview.oop")
local api = vim.api
local await = async.await
local M = {}
---@class Diff2Ver : Diff2
---@field a Window
---@field b Window
local Diff2Ver = oop.create_class("Diff2Ver", Diff2)
Diff2Ver.name = "diff2_vertical"
---@class Diff2Ver.init.Opt
---@field a vcs.File
---@field b vcs.File
---@field winid_a integer
---@field winid_b integer
---@param opt Diff2Hor.init.Opt
function Diff2Ver:init(opt)
self:super(opt)
end
---@override
---@param self Diff2Ver
---@param pivot integer?
Diff2Ver.create = async.void(function(self, pivot)
self:create_pre()
local curwin
pivot = pivot or self:find_pivot()
assert(api.nvim_win_is_valid(pivot), "Layout creation requires a valid window pivot!")
for _, win in ipairs(self.windows) do
if win.id ~= pivot then
win:close(true)
end
end
api.nvim_win_call(pivot, function()
vim.cmd("aboveleft sp")
curwin = api.nvim_get_current_win()
if self.a then
self.a:set_id(curwin)
else
self.a = Window({ id = curwin })
end
end)
api.nvim_win_call(pivot, function()
vim.cmd("aboveleft sp")
curwin = api.nvim_get_current_win()
if self.b then
self.b:set_id(curwin)
else
self.b = Window({ id = curwin })
end
end)
api.nvim_win_close(pivot, true)
self.windows = { self.a, self.b }
await(self:create_post())
end)
M.Diff2Ver = Diff2Ver
return M

View File

@ -0,0 +1,119 @@
local async = require("diffview.async")
local lazy = require("diffview.lazy")
local Window = require("diffview.scene.window").Window
local Layout = require("diffview.scene.layout").Layout
local oop = require("diffview.oop")
local Diff1 = lazy.access("diffview.scene.layouts.diff_1", "Diff1") ---@type Diff1|LazyModule
local Diff4 = lazy.access("diffview.scene.layouts.diff_4", "Diff4") ---@type Diff4|LazyModule
local File = lazy.access("diffview.vcs.file", "File") ---@type vcs.File|LazyModule
local Rev = lazy.access("diffview.vcs.rev", "Rev") ---@type Rev|LazyModule
local RevType = lazy.access("diffview.vcs.rev", "RevType") ---@type RevType|LazyModule
local await = async.await
local M = {}
---@class Diff3 : Layout
---@field a Window
---@field b Window
---@field c Window
local Diff3 = oop.create_class("Diff3", Layout)
---@alias Diff3.WindowSymbol "a"|"b"|"c"
---@class Diff3.init.Opt
---@field a vcs.File
---@field b vcs.File
---@field c vcs.File
---@field winid_a integer
---@field winid_b integer
---@field winid_c integer
---@param opt Diff3.init.Opt
function Diff3:init(opt)
self:super()
self.a = Window({ file = opt.a, id = opt.winid_a })
self.b = Window({ file = opt.b, id = opt.winid_b })
self.c = Window({ file = opt.c, id = opt.winid_c })
self:use_windows(self.a, self.b, self.c)
end
---@param file vcs.File
function Diff3:set_file_a(file)
self.a:set_file(file)
file.symbol = "a"
end
---@param file vcs.File
function Diff3:set_file_b(file)
self.b:set_file(file)
file.symbol = "b"
end
---@param file vcs.File
function Diff3:set_file_c(file)
self.c:set_file(file)
file.symbol = "c"
end
---@param self Diff3
---@param entry FileEntry
Diff3.use_entry = async.void(function(self, entry)
local layout = entry.layout --[[@as Diff3 ]]
assert(layout:instanceof(Diff3))
self:set_file_a(layout.a.file)
self:set_file_b(layout.b.file)
self:set_file_c(layout.c.file)
if self:is_valid() then
await(self:open_files())
end
end)
function Diff3:get_main_win()
return self.b
end
---@param layout Diff1
---@return Diff1
function Diff3:to_diff1(layout)
assert(layout:instanceof(Diff1.__get()))
return layout({ a = self:get_main_win().file })
end
---@param layout Diff4
---@return Diff4
function Diff3:to_diff4(layout)
assert(layout:instanceof(Diff4.__get()))
local main = self:get_main_win().file
return layout({
a = self.a.file,
b = self.b.file,
c = self.c.file,
d = File({
adapter = main.adapter,
path = main.path,
kind = main.kind,
commit = main.commit,
get_data = main.get_data,
rev = Rev(RevType.STAGE, 1),
nulled = false, -- FIXME
})
})
end
---FIXME
---@override
---@param rev Rev
---@param status string Git status symbol.
---@param sym Diff3.WindowSymbol
function Diff3.should_null(rev, status, sym)
return false
end
M.Diff3 = Diff3
return M

View File

@ -0,0 +1,78 @@
local async = require("diffview.async")
local Window = require("diffview.scene.window").Window
local Diff3 = require("diffview.scene.layouts.diff_3").Diff3
local oop = require("diffview.oop")
local api = vim.api
local await = async.await
local M = {}
---@class Diff3Hor : Diff3
---@field a Window
---@field b Window
---@field c Window
local Diff3Hor = oop.create_class("Diff3Hor", Diff3)
Diff3Hor.name = "diff3_horizontal"
function Diff3Hor:init(opt)
self:super(opt)
end
---@override
---@param self Diff3Hor
---@param pivot integer?
Diff3Hor.create = async.void(function(self, pivot)
self:create_pre()
local curwin
pivot = pivot or self:find_pivot()
assert(api.nvim_win_is_valid(pivot), "Layout creation requires a valid window pivot!")
for _, win in ipairs(self.windows) do
if win.id ~= pivot then
win:close(true)
end
end
api.nvim_win_call(pivot, function()
vim.cmd("aboveleft vsp")
curwin = api.nvim_get_current_win()
if self.a then
self.a:set_id(curwin)
else
self.a = Window({ id = curwin })
end
end)
api.nvim_win_call(pivot, function()
vim.cmd("aboveleft vsp")
curwin = api.nvim_get_current_win()
if self.b then
self.b:set_id(curwin)
else
self.b = Window({ id = curwin })
end
end)
api.nvim_win_call(pivot, function()
vim.cmd("aboveleft vsp")
curwin = api.nvim_get_current_win()
if self.c then
self.c:set_id(curwin)
else
self.c = Window({ id = curwin })
end
end)
api.nvim_win_close(pivot, true)
self.windows = { self.a, self.b, self.c }
await(self:create_post())
end)
M.Diff3Hor = Diff3Hor
return M

View File

@ -0,0 +1,78 @@
local async = require("diffview.async")
local Window = require("diffview.scene.window").Window
local Diff3 = require("diffview.scene.layouts.diff_3").Diff3
local oop = require("diffview.oop")
local api = vim.api
local await = async.await
local M = {}
---@class Diff3Mixed : Diff3
---@field a Window
---@field b Window
---@field c Window
local Diff3Mixed = oop.create_class("Diff3Mixed", Diff3)
Diff3Mixed.name = "diff3_mixed"
function Diff3Mixed:init(opt)
self:super(opt)
end
---@override
---@param self Diff3Mixed
---@param pivot integer?
Diff3Mixed.create = async.void(function(self, pivot)
self:create_pre()
local curwin
pivot = pivot or self:find_pivot()
assert(api.nvim_win_is_valid(pivot), "Layout creation requires a valid window pivot!")
for _, win in ipairs(self.windows) do
if win.id ~= pivot then
win:close(true)
end
end
api.nvim_win_call(pivot, function()
vim.cmd("belowright sp")
curwin = api.nvim_get_current_win()
if self.b then
self.b:set_id(curwin)
else
self.b = Window({ id = curwin })
end
end)
api.nvim_win_call(pivot, function()
vim.cmd("aboveleft vsp")
curwin = api.nvim_get_current_win()
if self.a then
self.a:set_id(curwin)
else
self.a = Window({ id = curwin })
end
end)
api.nvim_win_call(pivot, function()
vim.cmd("aboveleft vsp")
curwin = api.nvim_get_current_win()
if self.c then
self.c:set_id(curwin)
else
self.c = Window({ id = curwin })
end
end)
api.nvim_win_close(pivot, true)
self.windows = { self.a, self.b, self.c }
await(self:create_post())
end)
M.Diff3Mixed = Diff3Mixed
return M

View File

@ -0,0 +1,78 @@
local async = require("diffview.async")
local Window = require("diffview.scene.window").Window
local Diff3 = require("diffview.scene.layouts.diff_3").Diff3
local oop = require("diffview.oop")
local api = vim.api
local await = async.await
local M = {}
---@class Diff3Ver : Diff3
---@field a Window
---@field b Window
---@field c Window
local Diff3Ver = oop.create_class("Diff3Ver", Diff3)
Diff3Ver.name = "diff3_vertical"
function Diff3Ver:init(opt)
self:super(opt)
end
---@override
---@param self Diff3Ver
---@param pivot integer?
Diff3Ver.create = async.void(function(self, pivot)
self:create_pre()
local curwin
pivot = pivot or self:find_pivot()
assert(api.nvim_win_is_valid(pivot), "Layout creation requires a valid window pivot!")
for _, win in ipairs(self.windows) do
if win.id ~= pivot then
win:close(true)
end
end
api.nvim_win_call(pivot, function()
vim.cmd("aboveleft sp")
curwin = api.nvim_get_current_win()
if self.a then
self.a:set_id(curwin)
else
self.a = Window({ id = curwin })
end
end)
api.nvim_win_call(pivot, function()
vim.cmd("aboveleft sp")
curwin = api.nvim_get_current_win()
if self.b then
self.b:set_id(curwin)
else
self.b = Window({ id = curwin })
end
end)
api.nvim_win_call(pivot, function()
vim.cmd("aboveleft sp")
curwin = api.nvim_get_current_win()
if self.c then
self.c:set_id(curwin)
else
self.c = Window({ id = curwin })
end
end)
api.nvim_win_close(pivot, true)
self.windows = { self.a, self.b, self.c }
await(self:create_post())
end)
M.Diff3Ver = Diff3Ver
return M

View File

@ -0,0 +1,116 @@
local async = require("diffview.async")
local lazy = require("diffview.lazy")
local Window = require("diffview.scene.window").Window
local Layout = require("diffview.scene.layout").Layout
local oop = require("diffview.oop")
local Diff1 = lazy.access("diffview.scene.layouts.diff_1", "Diff1") ---@type Diff1|LazyModule
local Diff3 = lazy.access("diffview.scene.layouts.diff_3", "Diff3") ---@type Diff3|LazyModule
local await = async.await
local M = {}
---@class Diff4 : Layout
---@field a Window
---@field b Window
---@field c Window
---@field d Window
local Diff4 = oop.create_class("Diff4", Layout)
---@alias Diff4.WindowSymbol "a"|"b"|"c"|"d"
---@class Diff4.init.Opt
---@field a vcs.File
---@field b vcs.File
---@field c vcs.File
---@field d vcs.File
---@field winid_a integer
---@field winid_b integer
---@field winid_c integer
---@field winid_d integer
---@param opt Diff4.init.Opt
function Diff4:init(opt)
self:super()
self.a = Window({ file = opt.a, id = opt.winid_a })
self.b = Window({ file = opt.b, id = opt.winid_b })
self.c = Window({ file = opt.c, id = opt.winid_c })
self.d = Window({ file = opt.d, id = opt.winid_d })
self:use_windows(self.a, self.b, self.c, self.d)
end
---@param file vcs.File
function Diff4:set_file_a(file)
self.a:set_file(file)
file.symbol = "a"
end
---@param file vcs.File
function Diff4:set_file_b(file)
self.b:set_file(file)
file.symbol = "b"
end
---@param file vcs.File
function Diff4:set_file_c(file)
self.c:set_file(file)
file.symbol = "c"
end
---@param file vcs.File
function Diff4:set_file_d(file)
self.d:set_file(file)
file.symbol = "d"
end
---@param self Diff4
---@param entry FileEntry
Diff4.use_entry = async.void(function(self, entry)
local layout = entry.layout --[[@as Diff4 ]]
assert(layout:instanceof(Diff4))
self:set_file_a(layout.a.file)
self:set_file_b(layout.b.file)
self:set_file_c(layout.c.file)
self:set_file_d(layout.d.file)
if self:is_valid() then
await(self:open_files())
end
end)
function Diff4:get_main_win()
return self.b
end
---@param layout Diff1
---@return Diff1
function Diff4:to_diff1(layout)
assert(layout:instanceof(Diff1.__get()))
return layout({ a = self:get_main_win().file })
end
---@param layout Diff3
---@return Diff3
function Diff4:to_diff3(layout)
assert(layout:instanceof(Diff3.__get()))
return layout({
a = self.a.file,
b = self.b.file,
c = self.c.file,
})
end
---FIXME
---@override
---@param rev Rev
---@param status string Git status symbol.
---@param sym Diff4.WindowSymbol
function Diff4.should_null(rev, status, sym)
return false
end
M.Diff4 = Diff4
return M

View File

@ -0,0 +1,90 @@
local async = require("diffview.async")
local Window = require("diffview.scene.window").Window
local Diff4 = require("diffview.scene.layouts.diff_4").Diff4
local oop = require("diffview.oop")
local api = vim.api
local await = async.await
local M = {}
---@class Diff4Mixed : Diff4
---@field a Window
---@field b Window
---@field c Window
---@field d Window
local Diff4Mixed = oop.create_class("Diff4Mixed", Diff4)
Diff4Mixed.name = "diff4_mixed"
function Diff4Mixed:init(opt)
self:super(opt)
end
---@override
---@param self Diff4Mixed
---@param pivot integer?
Diff4Mixed.create = async.void(function(self, pivot)
self:create_pre()
local curwin
pivot = pivot or self:find_pivot()
assert(api.nvim_win_is_valid(pivot), "Layout creation requires a valid window pivot!")
for _, win in ipairs(self.windows) do
if win.id ~= pivot then
win:close(true)
end
end
api.nvim_win_call(pivot, function()
vim.cmd("belowright sp")
curwin = api.nvim_get_current_win()
if self.b then
self.b:set_id(curwin)
else
self.b = Window({ id = curwin })
end
end)
api.nvim_win_call(pivot, function()
vim.cmd("aboveleft vsp")
curwin = api.nvim_get_current_win()
if self.a then
self.a:set_id(curwin)
else
self.a = Window({ id = curwin })
end
end)
api.nvim_win_call(pivot, function()
vim.cmd("aboveleft vsp")
curwin = api.nvim_get_current_win()
if self.d then
self.d:set_id(curwin)
else
self.d = Window({ id = curwin })
end
end)
api.nvim_win_call(pivot, function()
vim.cmd("aboveleft vsp")
curwin = api.nvim_get_current_win()
if self.c then
self.c:set_id(curwin)
else
self.c = Window({ id = curwin })
end
end)
api.nvim_win_close(pivot, true)
self.windows = { self.a, self.b, self.c, self.d }
await(self:create_post())
end)
M.Diff4Mixed = Diff4Mixed
return M

View File

@ -0,0 +1,166 @@
local lazy = require("diffview.lazy")
local Diff1 = lazy.access("diffview.scene.layouts.diff_1", "Diff1") ---@type Diff1|LazyModule
local Diff2Hor = lazy.access("diffview.scene.layouts.diff_2_hor", "Diff2Hor") ---@type Diff2Hor|LazyModule
local Diff2Ver = lazy.access("diffview.scene.layouts.diff_2_ver", "Diff2Ver") ---@type Diff2Ver|LazyModule
local Diff3Hor = lazy.access("diffview.scene.layouts.diff_3_hor", "Diff3Hor") ---@type Diff3Hor|LazyModule
local Diff3Ver = lazy.access("diffview.scene.layouts.diff_3_ver", "Diff3Ver") ---@type Diff3Ver|LazyModule
local Diff4Mixed = lazy.access("diffview.scene.layouts.diff_4_mixed", "Diff4Mixed") ---@type Diff4Mixed|LazyModule
local EventEmitter = lazy.access("diffview.events", "EventEmitter") ---@type EventEmitter|LazyModule
local File = lazy.access("diffview.vcs.file", "File") ---@type vcs.File|LazyModule
local Signal = lazy.access("diffview.control", "Signal") ---@type Signal|LazyModule
local config = lazy.require("diffview.config") ---@module "diffview.config"
local oop = lazy.require("diffview.oop") ---@module "diffview.oop"
local utils = lazy.require("diffview.utils") ---@module "diffview.utils"
local api = vim.api
local M = {}
---@enum LayoutMode
local LayoutMode = oop.enum({
HORIZONTAL = 1,
VERTICAL = 2,
})
---@class View : diffview.Object
---@field tabpage integer
---@field emitter EventEmitter
---@field default_layout Layout (class)
---@field ready boolean
---@field closing Signal
local View = oop.create_class("View")
---@diagnostic disable unused-local
---@abstract
function View:init_layout() oop.abstract_stub() end
---@abstract
function View:post_open() oop.abstract_stub() end
---@diagnostic enable unused-local
---View constructor
function View:init(opt)
opt = opt or {}
self.emitter = opt.emitter or EventEmitter()
self.default_layout = opt.default_layout or View.get_default_layout()
self.ready = utils.sate(opt.ready, false)
self.closing = utils.sate(opt.closing, Signal())
local function wrap_event(event)
DiffviewGlobal.emitter:on(event, function(_, view, ...)
local cur_view = require("diffview.lib").get_current_view()
if (view and view == self) or (not view and cur_view == self) then
self.emitter:emit(event, view, ...)
end
end)
end
wrap_event("view_closed")
end
function View:open()
vim.cmd("tab split")
self.tabpage = api.nvim_get_current_tabpage()
self:init_layout()
self:post_open()
DiffviewGlobal.emitter:emit("view_opened", self)
DiffviewGlobal.emitter:emit("view_enter", self)
end
function View:close()
self.closing:send()
if self.tabpage and api.nvim_tabpage_is_valid(self.tabpage) then
DiffviewGlobal.emitter:emit("view_leave", self)
if #api.nvim_list_tabpages() == 1 then
vim.cmd("tabnew")
end
local pagenr = api.nvim_tabpage_get_number(self.tabpage)
vim.cmd("tabclose " .. pagenr)
end
DiffviewGlobal.emitter:emit("view_closed", self)
end
function View:is_cur_tabpage()
return self.tabpage == api.nvim_get_current_tabpage()
end
---@return boolean
local function prefer_horizontal()
return vim.tbl_contains(vim.opt.diffopt:get(), "vertical")
end
---@return Diff1
function View.get_default_diff1()
return Diff1.__get()
end
---@return Diff2
function View.get_default_diff2()
if prefer_horizontal() then
return Diff2Hor.__get()
else
return Diff2Ver.__get()
end
end
---@return Diff3
function View.get_default_diff3()
if prefer_horizontal() then
return Diff3Hor.__get()
else
return Diff3Ver.__get()
end
end
---@return Diff4
function View.get_default_diff4()
return Diff4Mixed.__get()
end
---@return LayoutName|-1
function View.get_default_layout_name()
return config.get_config().view.default.layout
end
---@return Layout # (class) The default layout class.
function View.get_default_layout()
local name = View.get_default_layout_name()
if name == -1 then
return View.get_default_diff2()
end
return config.name_to_layout(name --[[@as string ]])
end
---@return Layout
function View.get_default_merge_layout()
local name = config.get_config().view.merge_tool.layout
if name == -1 then
return View.get_default_diff3()
end
return config.name_to_layout(name)
end
---@return Diff2
function View.get_temp_layout()
local layout_class = View.get_default_layout()
return layout_class({
a = File.NULL_FILE,
b = File.NULL_FILE,
})
end
M.LayoutMode = LayoutMode
M.View = View
return M

View File

@ -0,0 +1,556 @@
local async = require("diffview.async")
local lazy = require("diffview.lazy")
local oop = require("diffview.oop")
local CommitLogPanel = lazy.access("diffview.ui.panels.commit_log_panel", "CommitLogPanel") ---@type CommitLogPanel|LazyModule
local Diff = lazy.access("diffview.diff", "Diff") ---@type Diff|LazyModule
local EditToken = lazy.access("diffview.diff", "EditToken") ---@type EditToken|LazyModule
local EventName = lazy.access("diffview.events", "EventName") ---@type EventName|LazyModule
local FileDict = lazy.access("diffview.vcs.file_dict", "FileDict") ---@type FileDict|LazyModule
local FileEntry = lazy.access("diffview.scene.file_entry", "FileEntry") ---@type FileEntry|LazyModule
local FilePanel = lazy.access("diffview.scene.views.diff.file_panel", "FilePanel") ---@type FilePanel|LazyModule
local PerfTimer = lazy.access("diffview.perf", "PerfTimer") ---@type PerfTimer|LazyModule
local RevType = lazy.access("diffview.vcs.rev", "RevType") ---@type RevType|LazyModule
local StandardView = lazy.access("diffview.scene.views.standard.standard_view", "StandardView") ---@type StandardView|LazyModule
local config = lazy.require("diffview.config") ---@module "diffview.config"
local debounce = lazy.require("diffview.debounce") ---@module "diffview.debounce"
local utils = lazy.require("diffview.utils") ---@module "diffview.utils"
local vcs_utils = lazy.require("diffview.vcs.utils") ---@module "diffview.vcs.utils"
local GitAdapter = lazy.access("diffview.vcs.adapters.git", "GitAdapter") ---@type GitAdapter|LazyModule
local api = vim.api
local await = async.await
local fmt = string.format
local logger = DiffviewGlobal.logger
local pl = lazy.access(utils, "path") ---@type PathLib
local M = {}
---@class DiffViewOptions
---@field show_untracked? boolean
---@field selected_file? string Path to the preferred initially selected file.
---@class DiffView : StandardView
---@operator call : DiffView
---@field adapter VCSAdapter
---@field rev_arg string
---@field path_args string[]
---@field left Rev
---@field right Rev
---@field options DiffViewOptions
---@field panel FilePanel
---@field commit_log_panel CommitLogPanel
---@field files FileDict
---@field file_idx integer
---@field merge_ctx? vcs.MergeContext
---@field initialized boolean
---@field valid boolean
---@field watcher uv_fs_poll_t # UV fs poll handle.
local DiffView = oop.create_class("DiffView", StandardView.__get())
---DiffView constructor
function DiffView:init(opt)
self.valid = false
self.files = FileDict()
self.adapter = opt.adapter
self.path_args = opt.path_args
self.rev_arg = opt.rev_arg
self.left = opt.left
self.right = opt.right
self.initialized = false
self.options = opt.options or {}
self.options.selected_file = self.options.selected_file
and pl:chain(self.options.selected_file)
:absolute()
:relative(self.adapter.ctx.toplevel)
:get()
self:super({
panel = FilePanel(
self.adapter,
self.files,
self.path_args,
self.rev_arg or self.adapter:rev_to_pretty_string(self.left, self.right)
),
})
self.attached_bufs = {}
self.emitter:on("file_open_post", utils.bind(self.file_open_post, self))
self.valid = true
end
function DiffView:post_open()
vim.cmd("redraw")
self.commit_log_panel = CommitLogPanel(self.adapter, {
name = fmt("diffview://%s/log/%d/%s", self.adapter.ctx.dir, self.tabpage, "commit_log"),
})
if config.get_config().watch_index and self.adapter:instanceof(GitAdapter.__get()) then
self.watcher = vim.loop.new_fs_poll()
self.watcher:start(
self.adapter.ctx.dir .. "/index",
1000,
---@diagnostic disable-next-line: unused-local
vim.schedule_wrap(function(err, prev, cur)
if not err then
if self:is_cur_tabpage() then
self:update_files()
end
end
end)
)
end
self:init_event_listeners()
vim.schedule(function()
self:file_safeguard()
if self.files:len() == 0 then
self:update_files()
end
self.ready = true
end)
end
---@param e Event
---@param new_entry FileEntry
---@param old_entry FileEntry
---@diagnostic disable-next-line: unused-local
function DiffView:file_open_post(e, new_entry, old_entry)
if new_entry.layout:is_nulled() then return end
if new_entry.kind == "conflicting" then
local file = new_entry.layout:get_main_win().file
local count_conflicts = vim.schedule_wrap(function()
local conflicts = vcs_utils.parse_conflicts(api.nvim_buf_get_lines(file.bufnr, 0, -1, false))
new_entry.stats = new_entry.stats or {}
new_entry.stats.conflicts = #conflicts
self.panel:render()
self.panel:redraw()
end)
count_conflicts()
if file.bufnr and not self.attached_bufs[file.bufnr] then
self.attached_bufs[file.bufnr] = true
local work = debounce.throttle_trailing(
1000,
true,
vim.schedule_wrap(function()
if not self:is_cur_tabpage() or self.cur_entry ~= new_entry then
self.attached_bufs[file.bufnr] = false
return
end
count_conflicts()
end)
)
api.nvim_create_autocmd(
{ "TextChanged", "TextChangedI" },
{
buffer = file.bufnr,
callback = function()
if not self.attached_bufs[file.bufnr] then
work:close()
return true
end
work()
end,
}
)
end
end
end
---@override
function DiffView:close()
if not self.closing:check() then
self.closing:send()
if self.watcher then
self.watcher:stop()
self.watcher:close()
end
for _, file in self.files:iter() do
file:destroy()
end
self.commit_log_panel:destroy()
DiffView.super_class.close(self)
end
end
---@private
---@param self DiffView
---@param file FileEntry
DiffView._set_file = async.void(function(self, file)
self.panel:render()
self.panel:redraw()
vim.cmd("redraw")
self.cur_layout:detach_files()
local cur_entry = self.cur_entry
self.emitter:emit("file_open_pre", file, cur_entry)
self.nulled = false
await(self:use_entry(file))
self.emitter:emit("file_open_post", file, cur_entry)
if not self.cur_entry.opened then
self.cur_entry.opened = true
DiffviewGlobal.emitter:emit("file_open_new", file)
end
end)
---Open the next file.
---@param highlight? boolean Bring the cursor to the file entry in the panel.
---@return FileEntry?
function DiffView:next_file(highlight)
self:ensure_layout()
if self:file_safeguard() then return end
if self.files:len() > 1 or self.nulled then
local cur = self.panel:next_file()
if cur then
if highlight or not self.panel:is_focused() then
self.panel:highlight_file(cur)
end
self:_set_file(cur)
return cur
end
end
end
---Open the previous file.
---@param highlight? boolean Bring the cursor to the file entry in the panel.
---@return FileEntry?
function DiffView:prev_file(highlight)
self:ensure_layout()
if self:file_safeguard() then return end
if self.files:len() > 1 or self.nulled then
local cur = self.panel:prev_file()
if cur then
if highlight or not self.panel:is_focused() then
self.panel:highlight_file(cur)
end
self:_set_file(cur)
return cur
end
end
end
---Set the active file.
---@param self DiffView
---@param file FileEntry
---@param focus? boolean Bring focus to the diff buffers.
---@param highlight? boolean Bring the cursor to the file entry in the panel.
DiffView.set_file = async.void(function(self, file, focus, highlight)
---@diagnostic disable: invisible
self:ensure_layout()
if self:file_safeguard() or not file then return end
for _, f in self.files:iter() do
if f == file then
self.panel:set_cur_file(file)
if highlight or not self.panel:is_focused() then
self.panel:highlight_file(file)
end
await(self:_set_file(file))
if focus then
api.nvim_set_current_win(self.cur_layout:get_main_win().id)
end
end
end
---@diagnostic enable: invisible
end)
---Set the active file.
---@param self DiffView
---@param path string
---@param focus? boolean Bring focus to the diff buffers.
---@param highlight? boolean Bring the cursor to the file entry in the panel.
DiffView.set_file_by_path = async.void(function(self, path, focus, highlight)
---@type FileEntry
for _, file in self.files:iter() do
if file.path == path then
await(self:set_file(file, focus, highlight))
return
end
end
end)
---Get an updated list of files.
---@param self DiffView
---@param callback fun(err?: string[], files: FileDict)
DiffView.get_updated_files = async.wrap(function(self, callback)
vcs_utils.diff_file_list(
self.adapter,
self.left,
self.right,
self.path_args,
self.options,
{
default_layout = DiffView.get_default_layout(),
merge_layout = DiffView.get_default_merge_layout(),
},
callback
)
end)
---Update the file list, including stats and status for all files.
DiffView.update_files = debounce.debounce_trailing(
100,
true,
---@param self DiffView
---@param callback fun(err?: string[])
async.wrap(function(self, callback)
await(async.scheduler())
-- Never update unless the view is in focus
if self.tabpage ~= api.nvim_get_current_tabpage() then
callback({ "The update was cancelled." })
return
end
---@type PerfTimer
local perf = PerfTimer("[DiffView] Status Update")
self:ensure_layout()
-- If left is tracking HEAD and right is LOCAL: Update HEAD rev.
local new_head
if self.left.track_head and self.right.type == RevType.LOCAL then
new_head = self.adapter:head_rev()
if new_head and self.left.commit ~= new_head.commit then
self.left = new_head
else
new_head = nil
end
perf:lap("updated head rev")
end
local index_stat = pl:stat(pl:join(self.adapter.ctx.dir, "index"))
---@type string[]?, FileDict
local err, new_files = await(self:get_updated_files())
await(async.scheduler())
if err then
utils.err("Failed to update files in a diff view!", true)
logger:error("[DiffView] Failed to update files!")
callback(err)
return
end
-- Stop the update if the view is no longer in focus.
if self.tabpage ~= api.nvim_get_current_tabpage() then
callback({ "The update was cancelled." })
return
end
perf:lap("received new file list")
local files = {
{ cur_files = self.files.conflicting, new_files = new_files.conflicting },
{ cur_files = self.files.working, new_files = new_files.working },
{ cur_files = self.files.staged, new_files = new_files.staged },
}
for _, v in ipairs(files) do
-- We diff the old file list against the new file list in order to find
-- the most efficient way to morph the current list into the new. This
-- way we avoid having to discard and recreate buffers for files that
-- exist in both lists.
---@param aa FileEntry
---@param bb FileEntry
local diff = Diff(v.cur_files, v.new_files, function(aa, bb)
return aa.path == bb.path and aa.oldpath == bb.oldpath
end)
local script = diff:create_edit_script()
local ai = 1
local bi = 1
for _, opr in ipairs(script) do
if opr == EditToken.NOOP then
-- Update status and stats
local a_stats = v.cur_files[ai].stats
local b_stats = v.new_files[bi].stats
if a_stats then
v.cur_files[ai].stats = vim.tbl_extend("force", a_stats, b_stats or {})
else
v.cur_files[ai].stats = v.new_files[bi].stats
end
v.cur_files[ai].status = v.new_files[bi].status
v.cur_files[ai]:validate_stage_buffers(index_stat)
if new_head then
v.cur_files[ai]:update_heads(new_head)
end
ai = ai + 1
bi = bi + 1
elseif opr == EditToken.DELETE then
if self.panel.cur_file == v.cur_files[ai] then
local file_list = self.panel:ordered_file_list()
if file_list[1] == self.panel.cur_file then
self.panel:set_cur_file(nil)
else
self.panel:set_cur_file(self.panel:prev_file())
end
end
v.cur_files[ai]:destroy()
table.remove(v.cur_files, ai)
elseif opr == EditToken.INSERT then
table.insert(v.cur_files, ai, v.new_files[bi])
ai = ai + 1
bi = bi + 1
elseif opr == EditToken.REPLACE then
if self.panel.cur_file == v.cur_files[ai] then
local file_list = self.panel:ordered_file_list()
if file_list[1] == self.panel.cur_file then
self.panel:set_cur_file(nil)
else
self.panel:set_cur_file(self.panel:prev_file())
end
end
v.cur_files[ai]:destroy()
v.cur_files[ai] = v.new_files[bi]
ai = ai + 1
bi = bi + 1
end
end
end
perf:lap("updated file list")
self.merge_ctx = next(new_files.conflicting) and self.adapter:get_merge_context() or nil
if self.merge_ctx then
for _, entry in ipairs(self.files.conflicting) do
entry:update_merge_context(self.merge_ctx)
end
end
FileEntry.update_index_stat(self.adapter, index_stat)
self.files:update_file_trees()
self.panel:update_components()
self.panel:render()
self.panel:redraw()
perf:lap("panel redrawn")
self.panel:reconstrain_cursor()
if utils.vec_indexof(self.panel:ordered_file_list(), self.panel.cur_file) == -1 then
self.panel:set_cur_file(nil)
end
-- Set initially selected file
if not self.initialized and self.options.selected_file then
for _, file in self.files:iter() do
if file.path == self.options.selected_file then
self.panel:set_cur_file(file)
break
end
end
end
self:set_file(self.panel.cur_file or self.panel:next_file(), false, not self.initialized)
self.update_needed = false
perf:time()
logger:lvl(5):debug(perf)
logger:fmt_info(
"[%s] Completed update for %d files successfully (%.3f ms)",
self.class:name(),
self.files:len(),
perf.final_time
)
self.emitter:emit("files_updated", self.files)
callback()
end)
)
---Ensures there are files to load, and loads the null buffer otherwise.
---@return boolean
function DiffView:file_safeguard()
if self.files:len() == 0 then
local cur = self.panel.cur_file
if cur then
cur.layout:detach_files()
end
self.cur_layout:open_null()
self.nulled = true
return true
end
return false
end
function DiffView:on_files_staged(callback)
self.emitter:on(EventName.FILES_STAGED, callback)
end
function DiffView:init_event_listeners()
local listeners = require("diffview.scene.views.diff.listeners")(self)
for event, callback in pairs(listeners) do
self.emitter:on(event, callback)
end
end
---Infer the current selected file. If the file panel is focused: return the
---file entry under the cursor. Otherwise return the file open in the view.
---Returns nil if no file is open in the view, or there is no entry under the
---cursor in the file panel.
---@param allow_dir? boolean Allow directory nodes from the file tree.
---@return (FileEntry|DirData)?
function DiffView:infer_cur_file(allow_dir)
if self.panel:is_focused() then
---@type any
local item = self.panel:get_item_at_cursor()
if not item then return end
if not allow_dir and type(item.collapsed) == "boolean" then return end
return item
else
return self.panel.cur_file
end
end
---Check whether or not the instantiation was successful.
---@return boolean
function DiffView:is_valid()
return self.valid
end
M.DiffView = DiffView
return M

View File

@ -0,0 +1,404 @@
local config = require("diffview.config")
local oop = require("diffview.oop")
local renderer = require("diffview.renderer")
local utils = require("diffview.utils")
local Panel = require("diffview.ui.panel").Panel
local api = vim.api
local M = {}
---@class TreeOptions
---@field flatten_dirs boolean
---@field folder_statuses "never"|"only_folded"|"always"
---@class FilePanel : Panel
---@field adapter VCSAdapter
---@field files FileDict
---@field path_args string[]
---@field rev_pretty_name string|nil
---@field cur_file FileEntry
---@field listing_style "list"|"tree"
---@field tree_options TreeOptions
---@field render_data RenderData
---@field components CompStruct
---@field constrain_cursor function
---@field help_mapping string
local FilePanel = oop.create_class("FilePanel", Panel)
FilePanel.winopts = vim.tbl_extend("force", Panel.winopts, {
cursorline = true,
winhl = {
"EndOfBuffer:DiffviewEndOfBuffer",
"Normal:DiffviewNormal",
"CursorLine:DiffviewCursorLine",
"WinSeparator:DiffviewWinSeparator",
"SignColumn:DiffviewNormal",
"StatusLine:DiffviewStatusLine",
"StatusLineNC:DiffviewStatuslineNC",
opt = { method = "prepend" },
},
})
FilePanel.bufopts = vim.tbl_extend("force", Panel.bufopts, {
filetype = "DiffviewFiles",
})
---FilePanel constructor.
---@param adapter VCSAdapter
---@param files FileEntry[]
---@param path_args string[]
function FilePanel:init(adapter, files, path_args, rev_pretty_name)
local conf = config.get_config()
self:super({
config = conf.file_panel.win_config,
bufname = "DiffviewFilePanel",
})
self.adapter = adapter
self.files = files
self.path_args = path_args
self.rev_pretty_name = rev_pretty_name
self.listing_style = conf.file_panel.listing_style
self.tree_options = conf.file_panel.tree_options
self:on_autocmd("BufNew", {
callback = function()
self:setup_buffer()
end,
})
end
---@override
function FilePanel:open()
FilePanel.super_class.open(self)
vim.cmd("wincmd =")
end
function FilePanel:setup_buffer()
local conf = config.get_config()
local default_opt = { silent = true, nowait = true, buffer = self.bufid }
for _, mapping in ipairs(conf.keymaps.file_panel) do
local opt = vim.tbl_extend("force", default_opt, mapping[4] or {}, { buffer = self.bufid })
vim.keymap.set(mapping[1], mapping[2], mapping[3], opt)
end
local help_keymap = config.find_help_keymap(conf.keymaps.file_panel)
if help_keymap then self.help_mapping = help_keymap[2] end
end
function FilePanel:update_components()
local conflicting_files
local working_files
local staged_files
if self.listing_style == "list" then
conflicting_files = { name = "files" }
working_files = { name = "files" }
staged_files = { name = "files" }
for _, file in ipairs(self.files.conflicting) do
table.insert(conflicting_files, {
name = "file",
context = file,
})
end
for _, file in ipairs(self.files.working) do
table.insert(working_files, {
name = "file",
context = file,
})
end
for _, file in ipairs(self.files.staged) do
table.insert(staged_files, {
name = "file",
context = file,
})
end
elseif self.listing_style == "tree" then
self.files.conflicting_tree:update_statuses()
self.files.working_tree:update_statuses()
self.files.staged_tree:update_statuses()
conflicting_files = utils.tbl_merge(
{ name = "files" },
self.files.conflicting_tree:create_comp_schema({
flatten_dirs = self.tree_options.flatten_dirs,
})
)
working_files = utils.tbl_merge(
{ name = "files" },
self.files.working_tree:create_comp_schema({
flatten_dirs = self.tree_options.flatten_dirs,
})
)
staged_files = utils.tbl_merge(
{ name = "files" },
self.files.staged_tree:create_comp_schema({
flatten_dirs = self.tree_options.flatten_dirs,
})
)
end
---@type CompStruct
self.components = self.render_data:create_component({
{ name = "path" },
{
name = "conflicting",
{ name = "title" },
conflicting_files,
{ name = "margin" },
},
{
name = "working",
{ name = "title" },
working_files,
{ name = "margin" },
},
{
name = "staged",
{ name = "title" },
staged_files,
{ name = "margin" },
},
{
name = "info",
{ name = "title" },
{ name = "entries" },
},
})
self.constrain_cursor = renderer.create_cursor_constraint({
self.components.conflicting.files.comp,
self.components.working.files.comp,
self.components.staged.files.comp,
})
end
---@return FileEntry[]
function FilePanel:ordered_file_list()
if self.listing_style == "list" then
local list = {}
for _, file in self.files:iter() do
list[#list + 1] = file
end
return list
else
local nodes = utils.vec_join(
self.files.conflicting_tree.root:leaves(),
self.files.working_tree.root:leaves(),
self.files.staged_tree.root:leaves()
)
return vim.tbl_map(function(node)
return node.data
end, nodes) --[[@as vector ]]
end
end
function FilePanel:set_cur_file(file)
if self.cur_file then
self.cur_file:set_active(false)
end
self.cur_file = file
if self.cur_file then
self.cur_file:set_active(true)
end
end
function FilePanel:prev_file()
local files = self:ordered_file_list()
if not self.cur_file and self.files:len() > 0 then
self:set_cur_file(files[1])
return self.cur_file
end
local i = utils.vec_indexof(files, self.cur_file)
if i ~= -1 then
self:set_cur_file(files[(i - vim.v.count1 - 1) % #files + 1])
return self.cur_file
end
end
function FilePanel:next_file()
local files = self:ordered_file_list()
if not self.cur_file and self.files:len() > 0 then
self:set_cur_file(files[1])
return self.cur_file
end
local i = utils.vec_indexof(files, self.cur_file)
if i ~= -1 then
self:set_cur_file(files[(i + vim.v.count1 - 1) % #files + 1])
return self.cur_file
end
end
---Get the file entry under the cursor.
---@return (FileEntry|DirData)?
function FilePanel:get_item_at_cursor()
if not self:is_open() and self:buf_loaded() then return end
local line = api.nvim_win_get_cursor(self.winid)[1]
local comp = self.components.comp:get_comp_on_line(line)
if comp and comp.name == "file" then
return comp.context
elseif comp and comp.name == "dir_name" then
return comp.parent.context
end
end
---Get the parent directory data of the item under the cursor.
---@return DirData?
---@return RenderComponent?
function FilePanel:get_dir_at_cursor()
if self.listing_style ~= "tree" then return end
if not self:is_open() and self:buf_loaded() then return end
local line = api.nvim_win_get_cursor(self.winid)[1]
local comp = self.components.comp:get_comp_on_line(line)
if not comp then return end
if comp.name == "dir_name" then
local dir_comp = comp.parent
return dir_comp.context, dir_comp
elseif comp.name == "file" then
local dir_comp = comp.parent.parent
return dir_comp.context, dir_comp
end
end
function FilePanel:highlight_file(file)
if not (self:is_open() and self:buf_loaded()) then
return
end
if self.listing_style == "list" then
for _, file_list in ipairs({
self.components.conflicting.files,
self.components.working.files,
self.components.staged.files,
}) do
for _, comp_struct in ipairs(file_list) do
if file == comp_struct.comp.context then
utils.set_cursor(self.winid, comp_struct.comp.lstart + 1, 0)
end
end
end
else -- tree
for _, comp_struct in ipairs({
self.components.conflicting.files,
self.components.working.files,
self.components.staged.files,
}) do
comp_struct.comp:deep_some(function(cur)
if file == cur.context then
local was_concealed = false
local dir = cur.parent.parent
while dir and dir.name == "directory" do
if dir.context and dir.context.collapsed then
was_concealed = true
dir.context.collapsed = false
end
dir = utils.tbl_access(dir, { "parent", "parent" })
end
if was_concealed then
self:render()
self:redraw()
end
utils.set_cursor(self.winid, cur.lstart + 1, 0)
return true
end
return false
end)
end
end
-- Needed to update the cursorline highlight when the panel is not focused.
utils.update_win(self.winid)
end
function FilePanel:highlight_cur_file()
if self.cur_file then
self:highlight_file(self.cur_file)
end
end
function FilePanel:highlight_prev_file()
if not (self:is_open() and self:buf_loaded()) or self.files:len() == 0 then
return
end
pcall(
api.nvim_win_set_cursor,
self.winid,
{ self.constrain_cursor(self.winid, -vim.v.count1), 0 }
)
utils.update_win(self.winid)
end
function FilePanel:highlight_next_file()
if not (self:is_open() and self:buf_loaded()) or self.files:len() == 0 then
return
end
pcall(api.nvim_win_set_cursor, self.winid, {
self.constrain_cursor(self.winid, vim.v.count1),
0,
})
utils.update_win(self.winid)
end
function FilePanel:reconstrain_cursor()
if not (self:is_open() and self:buf_loaded()) or self.files:len() == 0 then
return
end
pcall(api.nvim_win_set_cursor, self.winid, {
self.constrain_cursor(self.winid, 0),
0,
})
end
---@param item DirData|any
---@param open boolean
function FilePanel:set_item_fold(item, open)
if type(item.collapsed) == "boolean" and open == item.collapsed then
item.collapsed = not open
self:render()
self:redraw()
if item.collapsed then
self.components.comp:deep_some(function(comp, _, _)
if comp.context == item then
utils.set_cursor(self.winid, comp.lstart + 1)
return true
end
end)
end
end
end
function FilePanel:toggle_item_fold(item)
self:set_item_fold(item, item.collapsed)
end
function FilePanel:render()
require("diffview.scene.views.diff.render")(self)
end
M.FilePanel = FilePanel
return M

View File

@ -0,0 +1,321 @@
local async = require("diffview.async")
local lazy = require("diffview.lazy")
local EventName = lazy.access("diffview.events", "EventName") ---@type EventName|LazyModule
local RevType = lazy.access("diffview.vcs.rev", "RevType") ---@type RevType|LazyModule
local actions = lazy.require("diffview.actions") ---@module "diffview.actions"
local utils = lazy.require("diffview.utils") ---@module "diffview.utils"
local vcs_utils = lazy.require("diffview.vcs.utils") ---@module "diffview.vcs.utils"
local api = vim.api
local await = async.await
---@param view DiffView
return function(view)
return {
tab_enter = function()
local file = view.panel.cur_file
if file then
view:set_file(file, false, true)
end
if view.ready then
view:update_files()
end
end,
tab_leave = function()
local file = view.panel.cur_file
if file then
file.layout:detach_files()
end
for _, f in view.panel.files:iter() do
f.layout:restore_winopts()
end
end,
buf_write_post = function()
if view.adapter:has_local(view.left, view.right) then
view.update_needed = true
if api.nvim_get_current_tabpage() == view.tabpage then
view:update_files()
end
end
end,
file_open_new = function(_, entry)
api.nvim_win_call(view.cur_layout:get_main_win().id, function()
utils.set_cursor(0, 1, 0)
if view.cur_entry and view.cur_entry.kind == "conflicting" then
actions.next_conflict()
vim.cmd("norm! zz")
end
end)
view.cur_layout:sync_scroll()
end,
---@diagnostic disable-next-line: unused-local
files_updated = function(_, files)
view.initialized = true
end,
close = function()
if view.panel:is_focused() then
view.panel:close()
elseif view:is_cur_tabpage() then
view:close()
end
end,
select_next_entry = function()
view:next_file(true)
end,
select_prev_entry = function()
view:prev_file(true)
end,
next_entry = function()
view.panel:highlight_next_file()
end,
prev_entry = function()
view.panel:highlight_prev_file()
end,
select_entry = function()
if view.panel:is_open() then
---@type any
local item = view.panel:get_item_at_cursor()
if item then
if type(item.collapsed) == "boolean" then
view.panel:toggle_item_fold(item)
else
view:set_file(item, false)
end
end
end
end,
focus_entry = function()
if view.panel:is_open() then
---@type any
local item = view.panel:get_item_at_cursor()
if item then
if type(item.collapsed) == "boolean" then
view.panel:toggle_item_fold(item)
else
view:set_file(item, true)
end
end
end
end,
open_commit_log = function()
if view.left.type == RevType.STAGE and view.right.type == RevType.LOCAL then
utils.info("Changes not committed yet. No log available for these changes.")
return
end
local range = view.adapter.Rev.to_range(view.left, view.right)
if range then
view.commit_log_panel:update(range)
end
end,
toggle_stage_entry = function()
if not (view.left.type == RevType.STAGE and view.right.type == RevType.LOCAL) then
return
end
local item = view:infer_cur_file(true)
if item then
local success
if item.kind == "working" or item.kind == "conflicting" then
success = view.adapter:add_files({ item.path })
elseif item.kind == "staged" then
success = view.adapter:reset_files({ item.path })
end
if not success then
utils.err(("Failed to stage/unstage file: '%s'"):format(item.path))
return
end
if type(item.collapsed) == "boolean" then
---@cast item DirData
---@type FileTree
local tree
if item.kind == "conflicting" then
tree = view.panel.files.conflicting_tree
elseif item.kind == "working" then
tree = view.panel.files.working_tree
else
tree = view.panel.files.staged_tree
end
---@type Node
local item_node
tree.root:deep_some(function(node, _, _)
if node == item._node then
item_node = node
return true
end
end)
if item_node then
local next_leaf = item_node:next_leaf()
if next_leaf then
view:set_file(next_leaf.data)
else
view:set_file(view.panel.files[1])
end
end
else
view.panel:set_cur_file(item)
view:next_file()
end
view:update_files(
vim.schedule_wrap(function()
view.panel:highlight_cur_file()
end)
)
view.emitter:emit(EventName.FILES_STAGED, view)
end
end,
stage_all = function()
local args = vim.tbl_map(function(file)
return file.path
end, utils.vec_join(view.files.working, view.files.conflicting))
if #args > 0 then
local success = view.adapter:add_files(args)
if not success then
utils.err("Failed to stage files!")
return
end
view:update_files(function()
view.panel:highlight_cur_file()
end)
view.emitter:emit(EventName.FILES_STAGED, view)
end
end,
unstage_all = function()
local success = view.adapter:reset_files()
if not success then
utils.err("Failed to unstage files!")
return
end
view:update_files()
view.emitter:emit(EventName.FILES_STAGED, view)
end,
restore_entry = async.void(function()
if view.right.type ~= RevType.LOCAL then
utils.err("The right side of the diff is not local! Aborting file restoration.")
return
end
local commit
if view.left.type ~= RevType.STAGE then
commit = view.left.commit
end
local file = view:infer_cur_file()
if not file then return end
local bufid = utils.find_file_buffer(file.path)
if bufid and vim.bo[bufid].modified then
utils.err("The file is open with unsaved changes! Aborting file restoration.")
return
end
await(vcs_utils.restore_file(view.adapter, file.path, file.kind, commit))
view:update_files()
end),
listing_style = function()
if view.panel.listing_style == "list" then
view.panel.listing_style = "tree"
else
view.panel.listing_style = "list"
end
view.panel:update_components()
view.panel:render()
view.panel:redraw()
end,
toggle_flatten_dirs = function()
view.panel.tree_options.flatten_dirs = not view.panel.tree_options.flatten_dirs
view.panel:update_components()
view.panel:render()
view.panel:redraw()
end,
focus_files = function()
view.panel:focus()
end,
toggle_files = function()
view.panel:toggle(true)
end,
refresh_files = function()
view:update_files()
end,
open_all_folds = function()
if not view.panel:is_focused() or view.panel.listing_style ~= "tree" then return end
for _, file_set in ipairs({
view.panel.components.conflicting.files,
view.panel.components.working.files,
view.panel.components.staged.files,
}) do
file_set.comp:deep_some(function(comp, _, _)
if comp.name == "directory" then
(comp.context --[[@as DirData ]]).collapsed = false
end
end)
end
view.panel:render()
view.panel:redraw()
end,
close_all_folds = function()
if not view.panel:is_focused() or view.panel.listing_style ~= "tree" then return end
for _, file_set in ipairs({
view.panel.components.conflicting.files,
view.panel.components.working.files,
view.panel.components.staged.files,
}) do
file_set.comp:deep_some(function(comp, _, _)
if comp.name == "directory" then
(comp.context --[[@as DirData ]]).collapsed = true
end
end)
end
view.panel:render()
view.panel:redraw()
end,
open_fold = function()
if not view.panel:is_focused() then return end
local dir = view.panel:get_dir_at_cursor()
if dir then view.panel:set_item_fold(dir, true) end
end,
close_fold = function()
if not view.panel:is_focused() then return end
local dir, comp = view.panel:get_dir_at_cursor()
if dir and comp then
if not dir.collapsed then
view.panel:set_item_fold(dir, false)
else
local dir_parent = utils.tbl_access(comp, "parent.parent")
if dir_parent and dir_parent.name == "directory" then
view.panel:set_item_fold(dir_parent.context, false)
end
end
end
end,
toggle_fold = function()
if not view.panel:is_focused() then return end
local dir = view.panel:get_dir_at_cursor()
if dir then view.panel:toggle_item_fold(dir) end
end,
}
end

View File

@ -0,0 +1,204 @@
local config = require("diffview.config")
local hl = require("diffview.hl")
local utils = require("diffview.utils")
local pl = utils.path
---@param comp RenderComponent
---@param show_path boolean
---@param depth integer|nil
local function render_file(comp, show_path, depth)
---@type FileEntry
local file = comp.context
comp:add_text(file.status .. " ", hl.get_git_hl(file.status))
if depth then
comp:add_text(string.rep(" ", depth * 2 + 2))
end
local icon, icon_hl = hl.get_file_icon(file.basename, file.extension)
comp:add_text(icon, icon_hl)
comp:add_text(file.basename, file.active and "DiffviewFilePanelSelected" or "DiffviewFilePanelFileName")
if file.stats then
if file.stats.additions then
comp:add_text(" " .. file.stats.additions, "DiffviewFilePanelInsertions")
comp:add_text(", ")
comp:add_text(tostring(file.stats.deletions), "DiffviewFilePanelDeletions")
elseif file.stats.conflicts then
local has_conflicts = file.stats.conflicts > 0
comp:add_text(
" " .. (has_conflicts and file.stats.conflicts or config.get_config().signs.done),
has_conflicts and "DiffviewFilePanelConflicts" or "DiffviewFilePanelInsertions"
)
end
end
if file.kind == "conflicting" and not (file.stats and file.stats.conflicts) then
comp:add_text(" !", "DiffviewFilePanelConflicts")
end
if show_path then
comp:add_text(" " .. file.parent_path, "DiffviewFilePanelPath")
end
comp:ln()
end
---@param comp RenderComponent
local function render_file_list(comp)
for _, file_comp in ipairs(comp.components) do
render_file(file_comp, true)
end
end
---@param ctx DirData
---@param tree_options TreeOptions
---@return string
local function get_dir_status_text(ctx, tree_options)
local folder_statuses = tree_options.folder_statuses
if folder_statuses == "always" or (folder_statuses == "only_folded" and ctx.collapsed) then
return ctx.status
end
return " "
end
---@param depth integer
---@param comp RenderComponent
local function render_file_tree_recurse(depth, comp)
local conf = config.get_config()
if comp.name == "file" then
render_file(comp, false, depth)
return
end
if comp.name ~= "directory" then return end
-- Directory component structure:
-- {
-- name = "directory",
-- context = <DirData>,
-- { name = "dir_name" },
-- { name = "items", ...<files> },
-- }
local dir = comp.components[1]
local items = comp.components[2]
local ctx = comp.context --[[@as DirData ]]
dir:add_text(
get_dir_status_text(ctx, conf.file_panel.tree_options) .. " ",
hl.get_git_hl(ctx.status)
)
dir:add_text(string.rep(" ", depth * 2))
dir:add_text(ctx.collapsed and conf.signs.fold_closed or conf.signs.fold_open, "DiffviewNonText")
if conf.use_icons then
dir:add_text(
" " .. (ctx.collapsed and conf.icons.folder_closed or conf.icons.folder_open) .. " ",
"DiffviewFolderSign"
)
end
dir:add_text(ctx.name, "DiffviewFolderName")
dir:ln()
if not ctx.collapsed then
for _, item in ipairs(items.components) do
render_file_tree_recurse(depth + 1, item)
end
end
end
---@param comp RenderComponent
local function render_file_tree(comp)
for _, c in ipairs(comp.components) do
render_file_tree_recurse(0, c)
end
end
---@param listing_style "list"|"tree"
---@param comp RenderComponent
local function render_files(listing_style, comp)
if listing_style == "list" then
return render_file_list(comp)
end
render_file_tree(comp)
end
---@param panel FilePanel
return function(panel)
if not panel.render_data then
return
end
panel.render_data:clear()
local conf = config.get_config()
local width = panel:infer_width()
local comp = panel.components.path.comp
comp:add_line(
pl:truncate(pl:vim_fnamemodify(panel.adapter.ctx.toplevel, ":~"), width - 6),
"DiffviewFilePanelRootPath"
)
if conf.show_help_hints and panel.help_mapping then
comp:add_text("Help: ", "DiffviewFilePanelPath")
comp:add_line(panel.help_mapping, "DiffviewFilePanelCounter")
comp:add_line()
end
if #panel.files.conflicting > 0 then
comp = panel.components.conflicting.title.comp
comp:add_text("Conflicts ", "DiffviewFilePanelTitle")
comp:add_text("(" .. #panel.files.conflicting .. ")", "DiffviewFilePanelCounter")
comp:ln()
render_files(panel.listing_style, panel.components.conflicting.files.comp)
panel.components.conflicting.margin.comp:add_line()
end
local has_other_files = #panel.files.conflicting > 0 or #panel.files.staged > 0
-- Don't show the 'Changes' section if it's empty and we have other visible
-- sections.
if #panel.files.working > 0 or not has_other_files then
comp = panel.components.working.title.comp
comp:add_text("Changes ", "DiffviewFilePanelTitle")
comp:add_text("(" .. #panel.files.working .. ")", "DiffviewFilePanelCounter")
comp:ln()
render_files(panel.listing_style, panel.components.working.files.comp)
panel.components.working.margin.comp:add_line()
end
if #panel.files.staged > 0 then
comp = panel.components.staged.title.comp
comp:add_text("Staged changes ", "DiffviewFilePanelTitle")
comp:add_text("(" .. #panel.files.staged .. ")", "DiffviewFilePanelCounter")
comp:ln()
render_files(panel.listing_style, panel.components.staged.files.comp)
panel.components.staged.margin.comp:add_line()
end
if panel.rev_pretty_name or (panel.path_args and #panel.path_args > 0) then
local extra_info = utils.vec_join({ panel.rev_pretty_name }, panel.path_args or {})
comp = panel.components.info.title.comp
comp:add_line("Showing changes for:", "DiffviewFilePanelTitle")
comp = panel.components.info.entries.comp
for _, arg in ipairs(extra_info) do
local relpath = pl:relative(arg, panel.adapter.ctx.toplevel)
if relpath == "" then relpath = "." end
comp:add_line(pl:truncate(relpath, width - 5), "DiffviewFilePanelPath")
end
end
end

Some files were not shown because too many files have changed in this diff Show More