Update generated nvim config
This commit is contained in:
12
config/neovim/store/lazy-plugins/NrrwRgn/.gitignore
vendored
Normal file
12
config/neovim/store/lazy-plugins/NrrwRgn/.gitignore
vendored
Normal 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
|
||||
48
config/neovim/store/lazy-plugins/NrrwRgn/Makefile
Normal file
48
config/neovim/store/lazy-plugins/NrrwRgn/Makefile
Normal 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))
|
||||
2463
config/neovim/store/lazy-plugins/NrrwRgn/NrrwRgn.vmb
Normal file
2463
config/neovim/store/lazy-plugins/NrrwRgn/NrrwRgn.vmb
Normal file
File diff suppressed because it is too large
Load Diff
60
config/neovim/store/lazy-plugins/NrrwRgn/README.md
Normal file
60
config/neovim/store/lazy-plugins/NrrwRgn/README.md
Normal file
@ -0,0 +1,60 @@
|
||||
# NrrwRgn plugin [](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:
|
||||

|
||||
|
||||
## 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__
|
||||
1637
config/neovim/store/lazy-plugins/NrrwRgn/autoload/nrrwrgn.vim
Executable file
1637
config/neovim/store/lazy-plugins/NrrwRgn/autoload/nrrwrgn.vim
Executable file
File diff suppressed because it is too large
Load Diff
818
config/neovim/store/lazy-plugins/NrrwRgn/doc/NarrowRegion.txt
Normal file
818
config/neovim/store/lazy-plugins/NrrwRgn/doc/NarrowRegion.txt
Normal 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
|
||||
50
config/neovim/store/lazy-plugins/NrrwRgn/doc/tags
Normal file
50
config/neovim/store/lazy-plugins/NrrwRgn/doc/tags
Normal 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()*
|
||||
91
config/neovim/store/lazy-plugins/NrrwRgn/plugin/NrrwRgn.vim
Normal file
91
config/neovim/store/lazy-plugins/NrrwRgn/plugin/NrrwRgn.vim
Normal 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\:\"
|
||||
58
config/neovim/store/lazy-plugins/NrrwRgn/post.pl
Executable file
58
config/neovim/store/lazy-plugins/NrrwRgn/post.pl
Executable 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");
|
||||
BIN
config/neovim/store/lazy-plugins/NrrwRgn/screencast.gif
Normal file
BIN
config/neovim/store/lazy-plugins/NrrwRgn/screencast.gif
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 402 KiB |
8
config/neovim/store/lazy-plugins/NrrwRgn/test/runtest.sh
Executable file
8
config/neovim/store/lazy-plugins/NrrwRgn/test/runtest.sh
Executable 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
|
||||
@ -0,0 +1 @@
|
||||
foobar 1.txt
|
||||
@ -0,0 +1 @@
|
||||
foobar 1.txt some more stuff
|
||||
@ -0,0 +1 @@
|
||||
foobar 2.txt
|
||||
@ -0,0 +1 @@
|
||||
foobar 2.txt some more stuff
|
||||
@ -0,0 +1 @@
|
||||
foobar 3.txt
|
||||
@ -0,0 +1 @@
|
||||
foobar 3.txt some more stuff
|
||||
23
config/neovim/store/lazy-plugins/NrrwRgn/test/test1/cmd.sh
Executable file
23
config/neovim/store/lazy-plugins/NrrwRgn/test/test1/cmd.sh
Executable 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
|
||||
@ -0,0 +1 @@
|
||||
## Test 1: Multi Narrowed Window for several distinct buffers ##
|
||||
@ -0,0 +1,4 @@
|
||||
This is a single test.
|
||||
Line 2
|
||||
Line 3
|
||||
Line 4
|
||||
@ -0,0 +1,6 @@
|
||||
This is a single test.
|
||||
Line 2
|
||||
Line 3
|
||||
Line 4
|
||||
Added Line
|
||||
Added after Narrowing Line
|
||||
24
config/neovim/store/lazy-plugins/NrrwRgn/test/test2/cmd.sh
Executable file
24
config/neovim/store/lazy-plugins/NrrwRgn/test/test2/cmd.sh
Executable 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
|
||||
@ -0,0 +1 @@
|
||||
## Test 2: Single Narrowed Window for a single buffers ##
|
||||
@ -0,0 +1,4 @@
|
||||
This is a single test.
|
||||
Line 2
|
||||
Line 3
|
||||
Line 4
|
||||
@ -0,0 +1,6 @@
|
||||
This is a single test.
|
||||
Line 2
|
||||
Line 3
|
||||
Line 4
|
||||
Added Line
|
||||
Added after Narrowing Line
|
||||
26
config/neovim/store/lazy-plugins/NrrwRgn/test/test3/cmd.sh
Executable file
26
config/neovim/store/lazy-plugins/NrrwRgn/test/test3/cmd.sh
Executable 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
|
||||
@ -0,0 +1 @@
|
||||
## Test 2: Single Narrowed Window for a single buffer, but has another split window ##
|
||||
@ -0,0 +1,4 @@
|
||||
This is a single test.
|
||||
Line 2
|
||||
Line 3
|
||||
Line 4
|
||||
@ -0,0 +1,6 @@
|
||||
This is a single test.
|
||||
Line 2
|
||||
Line 3
|
||||
Line 4
|
||||
Added Line
|
||||
Added another Line
|
||||
26
config/neovim/store/lazy-plugins/NrrwRgn/test/test4/cmd.sh
Executable file
26
config/neovim/store/lazy-plugins/NrrwRgn/test/test4/cmd.sh
Executable 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
|
||||
@ -0,0 +1 @@
|
||||
## Test 4: Single Narrowed Window for a single buffer, switching back and forth between buffers several times (issue #44)
|
||||
@ -0,0 +1,4 @@
|
||||
This is a single test.
|
||||
Line 2
|
||||
Line 3
|
||||
Line 4
|
||||
@ -0,0 +1,6 @@
|
||||
This is a single test.
|
||||
Line 2
|
||||
Line 3
|
||||
Line 4
|
||||
Added Line
|
||||
[]
|
||||
25
config/neovim/store/lazy-plugins/NrrwRgn/test/test5/cmd.sh
Executable file
25
config/neovim/store/lazy-plugins/NrrwRgn/test/test5/cmd.sh
Executable 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
|
||||
@ -0,0 +1 @@
|
||||
## Test 5: Make sure, highlighting is removed after closing narrowed window
|
||||
@ -0,0 +1,4 @@
|
||||
This is a single test.
|
||||
Line 2
|
||||
Line 3
|
||||
Line 4
|
||||
@ -0,0 +1,6 @@
|
||||
This is a single test.
|
||||
Line 2
|
||||
Line 3
|
||||
Line 4
|
||||
Added Line
|
||||
[]
|
||||
25
config/neovim/store/lazy-plugins/NrrwRgn/test/test6/cmd.sh
Executable file
25
config/neovim/store/lazy-plugins/NrrwRgn/test/test6/cmd.sh
Executable 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
|
||||
@ -0,0 +1 @@
|
||||
## Test 6: Make sure, highlighting is removed after closing narrowed window (for visual narrowed region)
|
||||
@ -0,0 +1,4 @@
|
||||
This is a single test.
|
||||
Line 2
|
||||
Line 3
|
||||
Line 4
|
||||
@ -0,0 +1,5 @@
|
||||
This is a single test.
|
||||
Line 2
|
||||
Line 3
|
||||
Line 4
|
||||
[]
|
||||
25
config/neovim/store/lazy-plugins/NrrwRgn/test/test7/cmd.sh
Executable file
25
config/neovim/store/lazy-plugins/NrrwRgn/test/test7/cmd.sh
Executable 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
|
||||
@ -0,0 +1 @@
|
||||
## Test 6: Make sure, highlighting is removed after aborting narrowed window (issue #44)
|
||||
31
config/neovim/store/lazy-plugins/NrrwRgn/todo.txt
Normal file
31
config/neovim/store/lazy-plugins/NrrwRgn/todo.txt
Normal 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
|
||||
1
config/neovim/store/lazy-plugins/diffview.nvim/.github/FUNDING.yml
vendored
Normal file
1
config/neovim/store/lazy-plugins/diffview.nvim/.github/FUNDING.yml
vendored
Normal file
@ -0,0 +1 @@
|
||||
github: [sindrets]
|
||||
138
config/neovim/store/lazy-plugins/diffview.nvim/.github/ISSUE_TEMPLATE/bug_report.yml
vendored
Normal file
138
config/neovim/store/lazy-plugins/diffview.nvim/.github/ISSUE_TEMPLATE/bug_report.yml
vendored
Normal 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
|
||||
1
config/neovim/store/lazy-plugins/diffview.nvim/.github/ISSUE_TEMPLATE/config.yml
vendored
Normal file
1
config/neovim/store/lazy-plugins/diffview.nvim/.github/ISSUE_TEMPLATE/config.yml
vendored
Normal file
@ -0,0 +1 @@
|
||||
blank_issues_enabled: true
|
||||
3
config/neovim/store/lazy-plugins/diffview.nvim/.gitignore
vendored
Normal file
3
config/neovim/store/lazy-plugins/diffview.nvim/.gitignore
vendored
Normal file
@ -0,0 +1,3 @@
|
||||
/doc/tags
|
||||
/.tests/
|
||||
/.dev/
|
||||
@ -0,0 +1,4 @@
|
||||
-- Global objects defined by C code.
|
||||
read_globals = {
|
||||
"vim"
|
||||
}
|
||||
15
config/neovim/store/lazy-plugins/diffview.nvim/.luarc.json
Normal file
15
config/neovim/store/lazy-plugins/diffview.nvim/.luarc.json
Normal 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"
|
||||
]
|
||||
}
|
||||
16
config/neovim/store/lazy-plugins/diffview.nvim/LICENSE
Normal file
16
config/neovim/store/lazy-plugins/diffview.nvim/LICENSE
Normal 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/>.
|
||||
33
config/neovim/store/lazy-plugins/diffview.nvim/Makefile
Normal file
33
config/neovim/store/lazy-plugins/diffview.nvim/Makefile
Normal 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
|
||||
524
config/neovim/store/lazy-plugins/diffview.nvim/README.md
Normal file
524
config/neovim/store/lazy-plugins/diffview.nvim/README.md
Normal file
@ -0,0 +1,524 @@
|
||||
# Diffview.nvim
|
||||
|
||||
Single tabpage interface for easily cycling through diffs for all modified files
|
||||
for any git rev.
|
||||
|
||||

|
||||
|
||||
|
||||
## 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
|
||||
|
||||

|
||||
|
||||
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
|
||||
|
||||

|
||||
|
||||
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 -->
|
||||
287
config/neovim/store/lazy-plugins/diffview.nvim/USAGE.md
Normal file
287
config/neovim/store/lazy-plugins/diffview.nvim/USAGE.md
Normal 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.
|
||||
|
||||

|
||||
|
||||
> 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.
|
||||
|
||||

|
||||
|
||||
## 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.
|
||||
|
||||

|
||||
|
||||
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 -->
|
||||
1662
config/neovim/store/lazy-plugins/diffview.nvim/doc/diffview.txt
Normal file
1662
config/neovim/store/lazy-plugins/diffview.nvim/doc/diffview.txt
Normal file
File diff suppressed because it is too large
Load Diff
@ -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:
|
||||
@ -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:
|
||||
114
config/neovim/store/lazy-plugins/diffview.nvim/doc/tags
Normal file
114
config/neovim/store/lazy-plugins/diffview.nvim/doc/tags
Normal 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*
|
||||
@ -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
|
||||
@ -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
|
||||
@ -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
|
||||
@ -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
|
||||
@ -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
|
||||
@ -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
|
||||
@ -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
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
@ -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
|
||||
@ -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
|
||||
@ -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
|
||||
@ -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
|
||||
@ -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
|
||||
@ -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
|
||||
@ -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
|
||||
@ -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
|
||||
@ -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
|
||||
@ -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
|
||||
@ -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
|
||||
@ -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
|
||||
@ -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
|
||||
@ -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
|
||||
@ -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
|
||||
@ -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
|
||||
@ -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
|
||||
@ -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
|
||||
@ -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
|
||||
@ -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
|
||||
@ -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
|
||||
@ -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
|
||||
@ -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
|
||||
@ -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
|
||||
@ -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
|
||||
@ -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
|
||||
@ -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
|
||||
@ -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
|
||||
@ -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
|
||||
@ -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
|
||||
@ -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
|
||||
@ -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
|
||||
@ -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
Reference in New Issue
Block a user