Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 20 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -139,6 +139,7 @@ Following mappings are defined within popup window.
| `D` | Toggle all unified diff hunks of the commit |
| `r` | Toggle word diff hunks only in current file of the commit |
| `R` | Toggle all word diff hunks of current commit |
| `c` | Yank/copy the current commit hash to `v:register` |
| `?` | Show mappings help |

### Mappings
Expand Down Expand Up @@ -261,6 +262,25 @@ Setting `v:true` means adding margins in popup window. Blank lines at the top an
content are inserted. And every line is indented with one whitespace character.
Setting `v:false` to this variable removes all the margins.

#### `g:git_messenger_vimpopup_enabled` (Default: `v:false`)

When this value is set to `v:true`, enables the use of popup windows in Vim. This feature is
experimental, and has some limitations as it is not possible to enter a popup window in Vim (unlike
floating windows in Neovim). Entering a popup is emulated by initially disabling keyboard mappings
for the popup window, and only enabling them when it been marked as "entered", either by running the
`:GitMessenger` command a second time, or with `g:git_messenger_always_into_popup` set to `v:true`.

#### `g:git_messenger_vimpopup_win_opts` (Default `{}`)

Options passed to `popup_create()` on opening a popup window in Vim. This is useful when you want to
override some window options. See `:help popup-usage`.

The following example will add a border to the window in the default style.

```vim
let g:git_messenger_vimpopup_win_opts = { 'border': [] }
```

### Popup Window Highlight

This plugin uses color definitions from your colorscheme for highlighting stuffs in popup window by
Expand Down
2 changes: 2 additions & 0 deletions autoload/gitmessenger.vim
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ let g:git_messenger_max_popup_width = get(g:, 'git_messenger_max_popup_width', v
let g:git_messenger_date_format = get(g:, 'git_messenger_date_format', '%c')
let g:git_messenger_conceal_word_diff_marker = get(g:, 'git_messenger_conceal_word_diff_marker', 1)
let g:git_messenger_floating_win_opts = get(g:, 'git_messenger_floating_win_opts', {})
let g:git_messenger_vimpopup_enabled = get(g:, 'git_messenger_vimpopup_enabled', v:false)
let g:git_messenger_vimpopup_win_opts = get(g:, 'git_messenger_vimpopup_win_opts', {})
let g:git_messenger_popup_content_margins = get(g:, 'git_messenger_popup_content_margins', v:true)

" All popup instances keyed by opener's bufnr to manage lifetime of popups
Expand Down
45 changes: 35 additions & 10 deletions autoload/gitmessenger/blame.vim
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,22 @@ function! s:blame__forward() dict abort
endfunction
let s:blame.forward = funcref('s:blame__forward')

function! s:blame__yank_hash() dict abort
" Note: v:register is blackhole here when vim-cutlass plugin used
" TODO: investigate further, it should be possible to use v:register
let register = '"'
if has('clipboard')
if stridx(&clipboard, 'unnamedplus') != -1
let register = '+'
elseif stridx(&clipboard, 'unnamed') != -1
let register = '*'
endif
endif
call setreg(register, self.state.commit)
echo 'git-messenger: yanked commit hash ' . self.state.commit
endfunction
let s:blame.yank_hash = funcref('s:blame__yank_hash')

function! s:blame__open_popup() dict abort
if has_key(self, 'popup') && has_key(self.popup, 'bufnr')
" Already popup is open. It means that now older commit is showing up.
Expand All @@ -80,6 +96,7 @@ function! s:blame__open_popup() dict abort
\ 'q': [{-> execute('close', '')}, 'Close popup window'],
\ 'o': [funcref(self.back, [], self), 'Back to older commit'],
\ 'O': [funcref(self.forward, [], self), 'Forward to newer commit'],
\ 'c': [funcref(self.yank_hash, [], self), 'Yank/copy current commit hash'],
\ 'd': [funcref(self.reveal_diff, [v:false, v:false], self), "Toggle current file's diffs"],
\ 'D': [funcref(self.reveal_diff, [v:true, v:false], self), 'Toggle all diffs'],
\ 'r': [funcref(self.reveal_diff, [v:false, v:true], self), "Toggle current file's word diffs"],
Expand Down Expand Up @@ -192,18 +209,26 @@ function! s:blame__reveal_diff(include_all, word_diff) dict abort
endif

" Remove diff hunks from popup
let saved = getpos('.')
try
keepjumps execute 1
let diff_pattern = g:git_messenger_popup_content_margins ? '^ diff --git ' : '^diff --git '
let diff_offset = g:git_messenger_popup_content_margins ? 2 : 3
let diff_start = search(diff_pattern, 'ncW')
if diff_start > 1
let diff_pattern = g:git_messenger_popup_content_margins ? '^ diff --git ' : '^diff --git '
if has_key(self, 'popup') && has_key(self.popup, 'type') && self.popup.type ==# 'popup'
let diff_offset = g:git_messenger_popup_content_margins ? 1 : 2
let diff_start = match(self.state.contents, diff_pattern)
if diff_start > 0
let self.state.contents = self.state.contents[ : diff_start-diff_offset]
endif
finally
keepjumps call setpos('.', saved)
endtry
else
let diff_offset = g:git_messenger_popup_content_margins ? 2 : 3
let saved = getpos('.')
try
keepjumps execute 1
let diff_start = search(diff_pattern, 'ncW')
if diff_start > 1
let self.state.contents = self.state.contents[ : diff_start-diff_offset]
endif
finally
keepjumps call setpos('.', saved)
endtry
endif

if next_diff ==# 'none'
let self.state.diff = next_diff
Expand Down
185 changes: 180 additions & 5 deletions autoload/gitmessenger/popup.vim
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,15 @@ function! s:popup__close() dict abort
return
endif

let winnr = self.get_winnr()
if winnr > 0
" Without this 'noautocmd', the BufWipeout event will be triggered and
" this function will be called again.
noautocmd execute winnr . 'wincmd c'
if self.type ==# 'popup'
call popup_close(self.win_id)
else
let winnr = self.get_winnr()
if winnr > 0
" Without this 'noautocmd', the BufWipeout event will be triggered and
" this function will be called again.
noautocmd execute winnr . 'wincmd c'
endif
endif

unlet self.bufnr
Expand Down Expand Up @@ -48,6 +52,9 @@ endfunction
let s:popup.set_buf_var = funcref('s:popup__set_buf_var')

function! s:popup__scroll(map) dict abort
if self.type ==# 'popup'
return
endif
let winnr = self.get_winnr()
if winnr == 0
return
Expand All @@ -60,6 +67,10 @@ endfunction
let s:popup.scroll = funcref('s:popup__scroll')

function! s:popup__into() dict abort
if self.type ==# 'popup'
let self.entered = v:true
return
endif
let winnr = self.get_winnr()
if winnr == 0
return
Expand Down Expand Up @@ -150,10 +161,151 @@ function! s:popup__get_opener_winnr() dict abort
endfunction
let s:popup.get_opener_winnr = funcref('s:popup__get_opener_winnr')

function! s:popup__vimpopup_keymaps() dict abort
" TODO: allow customisation via config var once happy with dict key names
return {
\ 'scroll_down_1': ["\<c-e>", "\<c-n>", "\<Down>"],
\ 'scroll_up_1': ["\<c-y>", "\<c-p>", "\<Up>"],
\ 'scroll_down_page': ["\<c-f>", "\<PageDown>"],
\ 'scroll_up_page': ["\<c-b>", "\<PageUp>"],
\ 'scroll_down_half': ["\<c-d>"],
\ 'scroll_up_half': ["\<c-u>"],
\ }
endfunction
let s:popup.vimpopup_keymaps = funcref('s:popup__vimpopup_keymaps')

function! s:popup__vimpopup_win_filter(win_id, key) dict abort
" if popup not marked as entered, do not handle any keys
if !self.entered
return 0
endif
" Note: default q handler assumes we are in the popup window, but in Vim we
" cannot enter the popup window, so we override the handling here for now
let keymaps = self.vimpopup_keymaps()
if a:key ==# 'q'
call self.close()
elseif a:key ==# '?'
call self.echo_help()
elseif has_key(self.opts, 'mappings') && has_key(self.opts.mappings, a:key)
call self.opts.mappings[a:key][0]()
elseif index(keymaps['scroll_down_1'], a:key) >= 0
call win_execute(a:win_id, "normal! \<c-e>")
elseif index(keymaps['scroll_up_1'], a:key) >= 0
call win_execute(a:win_id, "normal! \<c-y>")
elseif index(keymaps['scroll_down_page'], a:key) >= 0
call win_execute(a:win_id, "normal! \<c-f>")
elseif index(keymaps['scroll_up_page'], a:key) >= 0
call win_execute(a:win_id, "normal! \<c-b>")
elseif index(keymaps['scroll_down_half'], a:key) >= 0
call win_execute(a:win_id, "normal! \<c-d>")
elseif index(keymaps['scroll_up_half'], a:key) >= 0
call win_execute(a:win_id, "normal! \<c-u>")
elseif a:key ==? "\<ScrollWheelUp>"
let pos = getmousepos()
if pos.winid == a:win_id
call win_execute(a:win_id, "normal! 3\<c-y>")
else
return 0
endif
elseif a:key ==? "\<ScrollWheelDown>"
let pos = getmousepos()
if pos.winid == a:win_id
call win_execute(a:win_id, "normal! 3\<c-e>")
else
return 0
endif
else
return 0
endif
return 1
endfunction
let s:popup.vimpopup_win_filter = funcref('s:popup__vimpopup_win_filter')

function! s:popup__vimpopup_win_opts(width, height) dict abort
" Note: calculations here are not the same as for Neovim floating window as
" Vim popup positioning relative to the editor window is slightly different,
" but the end result is that the popup is in same position in Vim as Neovim
if self.opened_at[0] + a:height <= &lines
let vert = 'top'
let row = self.opened_at[0] + 1
else
let vert = 'bot'
let row = self.opened_at[0] - 1
endif

if self.opened_at[1] + a:width <= &columns
let hor = 'left'
let col = self.opened_at[1]
else
let hor = 'right'
let col = self.opened_at[1]
endif

" Note: scrollbar disabled as seems buggy, even in Vim 9.1, scrollbar does
" not reliably appear when content does not fit, which means scroll is not
" always enabled when needed, so handle scroll in filter function instead.
" This now works the same as Neovim, no scrollbar, but mouse scroll works.
return extend({
\ 'filter': self.vimpopup_win_filter,
\ 'callback': self.vimpopup_win_callback,
\ },
\ extend({
\ 'line': row,
\ 'col': col,
\ 'pos': vert . hor,
\ 'filtermode': 'n',
\ 'minwidth': a:width,
\ 'maxwidth': a:width,
\ 'minheight': a:height,
\ 'maxheight': a:height,
\ 'scrollbar': v:false,
\ 'highlight': 'gitmessengerPopupNormal'
\ },
\ g:git_messenger_vimpopup_win_opts), 'error')
endfunction
let s:popup.vimpopup_win_opts = funcref('s:popup__vimpopup_win_opts')

function! s:popup__vimpopup_win_callback(win_id, result) dict abort
" Hacky custom cleanup for vimpopup, necessary as buffer never entered
silent! unlet b:__gitmessenger_popup
silent! autocmd! plugin-git-messenger-close * <buffer>
silent! autocmd! plugin-git-messenger-buf-enter
endfunction
let s:popup.vimpopup_win_callback = funcref('s:popup__vimpopup_win_callback')

function! s:popup__open() dict abort
let self.opened_at = s:get_global_pos()
let self.opener_bufnr = bufnr('%')
let self.opener_winid = win_getid()

if g:git_messenger_vimpopup_enabled && has('popupwin')
let self.type = 'popup'
let [width, height] = self.window_size()
let win_id = popup_create('', self.vimpopup_win_opts(width, height))
" Note: all local options are automatically set for new popup buffers
" in Vim so we only need to override a few, see :help popup-buffer
call win_execute(win_id, 'setlocal nomodified nofoldenable nomodeline conceallevel=2')
call popup_settext(win_id, self.contents)
call win_execute(win_id, 'setlocal nomodified nomodifiable')
if has_key(self.opts, 'filetype')
" Note: setbufvar() seems necessary to trigger Filetype autocmds
call setbufvar(winbufnr(win_id), '&filetype', self.opts.filetype)
endif
if has_key(self.opts, 'enter') && self.opts.enter
let self.entered = v:true
else
let self.entered = v:false
endif
" Allow multiple invocations of :GitMessenger command to toggle popup
" See gitmessenger#popup#close_current_popup() and gitmessenger#new()
let b:__gitmessenger_popup = self " local to opener, removed by callback
" Also ensure popup closed and callback called when leaving opener
autocmd BufWipeout,BufLeave <buffer> ++once silent! call b:__gitmessenger_popup.close()
let self.bufnr = winbufnr(win_id)
let self.win_id = win_id
return
endif

let self.type = s:floating_window_available ? 'floating' : 'preview'

let [width, height] = self.window_size()
Expand Down Expand Up @@ -228,6 +380,17 @@ endfunction
let s:popup.open = funcref('s:popup__open')

function! s:popup__update() dict abort

if self.type ==# 'popup'
let [width, height] = self.window_size()
let win_id = self.win_id
call popup_setoptions(self.win_id, self.vimpopup_win_opts(width, height))
call win_execute(win_id, 'setlocal modifiable')
call popup_settext(win_id, self.contents)
call win_execute(win_id, 'setlocal nomodified nomodifiable')
return
endif

" Note: `:noautocmd` to prevent BufLeave autocmd event (#13)
" It should be ok because the cursor position is finally back to the first
" position.
Expand Down Expand Up @@ -291,6 +454,15 @@ function! s:popup__echo_help() dict abort
call sort(maps, 'i')
let maps += ['?']

" When using Vim popup only one echo command output is shown in cmdline
if self.type ==# 'popup'
let lines = map(maps, {_, map ->
\ map . ' : ' . ( map ==# '?' ? 'Show this help' : self.opts.mappings[map][1] )
\ })
echo join(lines, "\n")
return
endif

for map in maps
if map ==# '?'
let desc = 'Show this help'
Expand Down Expand Up @@ -333,6 +505,9 @@ function! gitmessenger#popup#close_current_popup() abort
if !exists('b:__gitmessenger_popup')
return 0
endif
if b:__gitmessenger_popup.type ==# 'popup' && !b:__gitmessenger_popup.entered
return 0
endif
call b:__gitmessenger_popup.close()
" TODO?: Back to opened_at pos by setpos()
return 1
Expand Down
21 changes: 21 additions & 0 deletions doc/git-messenger.txt
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,7 @@ COMMANDS *git-messenger-commands*
| D | Toggle all unified diff hunks of the commit |
| r | Toggle word diff hunks only in current file of the commit |
| R | Toggle all word diff hunks of the commit |
| c | Yank/copy the current commit hash to |v:register| |
| ? | Show mappings help |

*:GitMessengerClose*
Expand Down Expand Up @@ -297,6 +298,26 @@ Setting |v:false| to this variable removes all the margins. Removing margins
might be useful when you enable borders of popup window with
|g:git_messenger_floating_win_opts|.

*g:git_messenger_vimpopup_enabled* (Default: |v:false|)

When this value is set to |v:true|, enables the use of popup windows in Vim.
This feature is experimental, and has some limitations as it is not possible
to enter a popup window in Vim (unlike floating windows in Neovim). Entering a
popup is emulated by initially disabling keyboard mappings for the popup
window, and only enabling them when it been marked as "entered", either by
running the |:GitMessenger| command a second time, or with
|g:git_messenger_always_into_popup| set to |v:true|.

*g:git_messenger_vimpopup_win_opts* (Default |{}|)

Options passed to `popup_create()` on opening a popup window in Vim. This is
useful when you want to override some window options. See |popup-usage|.

The following example will add a border to the window in the default style.
>
let g:git_messenger_vimpopup_win_opts = { 'border': [] }
<

==============================================================================
HIGHLIGHTS *git-messenger-highlights*

Expand Down
6 changes: 5 additions & 1 deletion syntax/gitmessengerpopup.vim
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,11 @@ hi def link gitmessengerHeader Identifier
hi def link gitmessengerHash Comment
hi def link gitmessengerHistory Constant
hi def link gitmessengerEmail gitmessengerPopupNormal
hi def link gitmessengerPopupNormal NormalFloat
if has('nvim')
hi def link gitmessengerPopupNormal NormalFloat
else
hi def link gitmessengerPopupNormal Pmenu
endif

hi def link diffOldFile diffFile
hi def link diffNewFile diffFile
Expand Down
Loading