diff --git a/README.md b/README.md index 9fc5775..1070c0b 100644 --- a/README.md +++ b/README.md @@ -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 @@ -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 diff --git a/autoload/gitmessenger.vim b/autoload/gitmessenger.vim index 83c920c..750fcd2 100644 --- a/autoload/gitmessenger.vim +++ b/autoload/gitmessenger.vim @@ -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 diff --git a/autoload/gitmessenger/blame.vim b/autoload/gitmessenger/blame.vim index d42e7bc..ea90393 100644 --- a/autoload/gitmessenger/blame.vim +++ b/autoload/gitmessenger/blame.vim @@ -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. @@ -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"], @@ -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 diff --git a/autoload/gitmessenger/popup.vim b/autoload/gitmessenger/popup.vim index ed0d8ed..51896bd 100644 --- a/autoload/gitmessenger/popup.vim +++ b/autoload/gitmessenger/popup.vim @@ -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 @@ -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 @@ -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 @@ -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': ["\", "\", "\"], + \ 'scroll_up_1': ["\", "\", "\"], + \ 'scroll_down_page': ["\", "\"], + \ 'scroll_up_page': ["\", "\"], + \ 'scroll_down_half': ["\"], + \ 'scroll_up_half': ["\"], + \ } +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! \") + elseif index(keymaps['scroll_up_1'], a:key) >= 0 + call win_execute(a:win_id, "normal! \") + elseif index(keymaps['scroll_down_page'], a:key) >= 0 + call win_execute(a:win_id, "normal! \") + elseif index(keymaps['scroll_up_page'], a:key) >= 0 + call win_execute(a:win_id, "normal! \") + elseif index(keymaps['scroll_down_half'], a:key) >= 0 + call win_execute(a:win_id, "normal! \") + elseif index(keymaps['scroll_up_half'], a:key) >= 0 + call win_execute(a:win_id, "normal! \") + elseif a:key ==? "\" + let pos = getmousepos() + if pos.winid == a:win_id + call win_execute(a:win_id, "normal! 3\") + else + return 0 + endif + elseif a:key ==? "\" + let pos = getmousepos() + if pos.winid == a:win_id + call win_execute(a:win_id, "normal! 3\") + 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 * + 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 ++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() @@ -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. @@ -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' @@ -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 diff --git a/doc/git-messenger.txt b/doc/git-messenger.txt index 6ce1632..9c3e5a4 100644 --- a/doc/git-messenger.txt +++ b/doc/git-messenger.txt @@ -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* @@ -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* diff --git a/syntax/gitmessengerpopup.vim b/syntax/gitmessengerpopup.vim index 10f8c0f..b2b15b5 100644 --- a/syntax/gitmessengerpopup.vim +++ b/syntax/gitmessengerpopup.vim @@ -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 diff --git a/test/all.vimspec b/test/all.vimspec index 426e9c2..698b355 100644 --- a/test/all.vimspec +++ b/test/all.vimspec @@ -242,6 +242,33 @@ Describe git-messenger.vim Assert True(found, 'Got line: ' . string(getline(2))) End + It yanks current commit on c + GitMessenger + Assert IsNotNone(GetPopup()) + + GitMessenger + Assert Exists('b:__gitmessenger_popup') + + call setreg(v:register, 'foo') + Assert Equal(getreg(v:register), 'foo') + + normal c + + let lines = getline(1, '$') + let commit = lines[2] + let hash = matchstr(commit, '^ Commit: \+\zs[[:xdigit:]]\{7,}$') + Assert Equal(getreg(v:register), hash) + + " Confirm also works after moving to a different commit + normal o + normal c + + let lines = getline(1, '$') + let commit = lines[2] + let hash = matchstr(commit, '^ Commit: \+\zs[[:xdigit:]]\{7,}$') + Assert Equal(getreg(v:register), hash) + End + It does not cause #23 again " 1. Open the same buffer with multiple window " 2. Move cursror to the second window which opens the same