1

Regenerate nvim config

This commit is contained in:
2024-06-02 03:29:20 +02:00
parent 75eea0c030
commit ef2e28883d
5576 changed files with 604886 additions and 503 deletions

View File

@ -0,0 +1,10 @@
coverage:
status:
project:
default:
target: 0%
patch:
default:
target: 0%
comment: false

View File

@ -0,0 +1,3 @@
[run]
plugins = covimerage
data_file = .coverage_covimerage

View File

@ -0,0 +1,90 @@
name: CI
on: [push, pull_request]
jobs:
test:
name: Unit tests
strategy:
matrix:
os: [macos-latest, ubuntu-latest]
neovim: [true, false]
version: [stable, nightly]
# Exclude Neovim nightly since Neovim's release workflow is fairly unstable: https://github.com/neovim/neovim/actions/workflows/release.yml
exclude:
- neovim: true
version: nightly
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@v2
- name: Set Git user for running Git commands in unit tests
run: |
git config --global user.email "users@noreply.github.com"
git config --global user.name "github-action"
- name: Fetch Git history for unit tests
run: git fetch --no-tags --prune --unshallow
- name: Checkout themis.vim
uses: actions/checkout@v2
with:
repository: thinca/vim-themis
path: vim-themis
- uses: rhysd/action-setup-vim@v1
id: vim
with:
version: ${{ matrix.version }}
neovim: ${{ matrix.neovim }}
- name: Run unit tests
env:
THEMIS_VIM: ${{ steps.vim.outputs.executable }}
THEMIS_PROFILE: profile.txt
run: ./vim-themis/bin/themis ./test/all.vimspec
- uses: actions/setup-python@v1
- name: Report coverage
run: |
# https://github.com/Vimjas/covimerage/issues/95
pip install 'click<8.0.0'
pip install covimerage
covimerage write_coverage profile.txt
coverage report
coverage xml
- name: Upload coverage to codecov
uses: codecov/codecov-action@v1
with:
token: ${{ secrets.CODECOV_TOKEN }}
test-windows:
name: Unit tests on Windows
strategy:
matrix:
neovim: [true, false]
runs-on: windows-latest
steps:
- uses: actions/checkout@v2
- name: Set Git user for running Git commands in unit tests
run: |
git config --global user.email "users@noreply.github.com"
git config --global user.name "github-action"
- name: Fetch Git history for unit tests
run: git fetch --no-tags --prune --unshallow
- name: Checkout themis.vim
uses: actions/checkout@v2
with:
repository: thinca/vim-themis
path: vim-themis
- uses: rhysd/action-setup-vim@v1
id: vim
with:
version: stable
neovim: ${{ matrix.neovim }}
- name: Run unit tests
env:
THEMIS_VIM: ${{ steps.vim.outputs.executable }}
run: ./vim-themis/bin/themis ./test/all.vimspec
vint:
name: Run vint
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: actions/setup-python@v1
- run: pip install vim-vint
- run: vint --warning --verbose --enable-neovim ./autoload ./plugin

View File

@ -0,0 +1,5 @@
/vim-themis
/htmlcov
/coverage.xml
/.coverage_covimerage
/doc/tags

View File

@ -0,0 +1,20 @@
The MIT License (MIT)
Copyright (c) 2013 rhysd
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
of the Software, and to permit persons to whom the Software is furnished to do so,
subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR
PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR
THE USE OR OTHER DEALINGS IN THE SOFTWARE.

View File

@ -0,0 +1,357 @@
git-messenger.vim
=================
[![Build Status][ci-badge]][ci]
[![codecov][codecov-badge]][codecov]
[git-messenger.vim][repo] is a Vim/Neovim plugin to reveal the hidden message from Git under the
cursor quickly. It shows the history of commits under the cursor in popup window.
This plugin shows the message of the last commit in a 'popup window'. If the last commit is not
convenient, you can explore older commits in the popup window. Additionally you can also check diff
of the commit.
The popup window is implemented in
- Floating window on Neovim (0.4 or later)
- Preview window on Vim (8 or later) or Neovim (0.3 or earlier)
The floating window is definitely recommended since it can shows the information near the cursor.
I wrote [a Japanese blogpost for this plugin](https://rhysd.hatenablog.com/entry/2019/03/10/230119).
## Why?
When you're modifying unfamiliar codes, you would sometimes wonder 'why was this line added?' or
'why this value was chosen?' in the source code. The answer sometimes lays in a commit message,
especially in message of the last commit which modifies the line.
## Screenshot
#### Show popup window with Neovim v0.4.0-dev
<img alt="main screenshot" src="https://github.com/rhysd/ss/blob/master/git-messenger.vim/demo.gif?raw=true" width=763 height=556 />
#### Exploring older commits
<img alt="history screenshot" src="https://github.com/rhysd/ss/blob/master/git-messenger.vim/history.gif?raw=true" width=510 height=252 />
#### Exploring diff of the commit (you may be also interested in `g:git_messenger_include_diff`)
<img alt="diff screenshot" src="https://github.com/rhysd/ss/blob/master/git-messenger.vim/diff.gif?raw=true" width=742 height=492 />
#### Switching unified diffs and word diffs
<img alt="word diff screenshot" src="https://github.com/rhysd/ss/blob/master/git-messenger.vim/worddiff.gif?raw=true" width=661 height=492 />
## Installation
Please ensure the following requirement before installing this plugin.
- Git v1.8.5 or later (for `-C` option of `git` command)
If you use any package manager, please follow its instruction.
With [vim-plug](https://github.com/junegunn/vim-plug):
```vim
Plug 'rhysd/git-messenger.vim'
```
With [dein.vim](https://github.com/Shougo/dein.vim):
```vim
call dein#add('rhysd/git-messenger.vim', {
\ 'lazy' : 1,
\ 'on_cmd' : 'GitMessenger',
\ 'on_map' : '<Plug>(git-messenger)',
\ })
```
With [minpac](https://github.com/k-takata/minpac):
```vim
call minpac#add('rhysd/git-messenger.vim')
```
if you use Vim's builtin packager, please follow the instruction at `:help pack-add`.
To enable a floating window support, you need to install Neovim 0.4 or later.
Please follow [the official instruction][nvim-install].
To check if Neovim's floating window feature is available, try `:checkhealth`.
## Usage
Only one mapping (or one command) provides all features of this plugin. Briefly, move cursor to
the position and run `:GitMessenger` or `<Leader>gm`. If you see an error message, please try
[health check](#health-check).
### Commands
```
:GitMessenger
```
Behavior of this command is depending on the situation. You can do every operations only with this
mapping.
- Normally, it opens a popup window with the last commit message
- When a popup window is already open, it moves cursor into the window
- When a cursor is within a popup window, it closes the window
It opens a popup window with the last commit message which modified the line at cursor. The popup
window shows following contents:
- **History:** `History: {page number}` In popup window, `o`/`O` navigates to previous/next commit.
- **Commit:** `Commit: {hash}` The commit hash
- **Author:** `Author: {name}<{email}>` Author name and mail address of the commit
- **Committer:** `Committer: {name}<{email}>` Committer name and mail address of the commit when
committer is different from author
- **Date:** `Date: {date}` Author date of the commit in system format
- When a committer date and an author date are different (e.g. the commit was created again with
`git commit --amend`), both **Author Date:** and **Committer Date:** are shown.
- **Summary:** First line after `Date:` header line is a summary of commit
- **Body:** After summary, commit body is put (if the commit has body)
The popup window will be automatically closed when you move the cursor so you don't need to close
it manually.
Running this command while a popup window is open, it moves the cursor into the window.
This behavior is useful when the commit message is too long and window cannot show the whole content.
By moving the cursor into the popup window, you can see the rest of contents by scrolling it.
You can also see the older commits.
Following mappings are defined within popup window.
| Mapping | Description |
|---------|--------------------------------------------------------------|
| `q` | Close the popup window |
| `o` | **o**lder. Back to older commit at the line |
| `O` | Opposite to `o`. Forward to newer commit at the line |
| `d` | Toggle unified diff hunks only in current file of the commit |
| `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 |
| `?` | Show mappings help |
### Mappings
```
<Plug>(git-messenger)
```
The same as running `:GitMessenger` command.
By default, this plugin defines following mapping.
```vim
nmap <Leader>gm <Plug>(git-messenger)
```
If you don't like the default mapping, set `g:git_messenger_no_default_mappings` to `v:true` in
your `.vimrc` or `init.vim` and map the `<Plug>` mapping to your favorite key sequence.
For example:
```vim
nmap <C-w>m <Plug>(git-messenger)
```
Some other additional `<Plug>` mappings. Please read [`:help git-messenger`][doc].
### Variables
Some global variables are available to configure the behavior of this plugin.
#### `g:git_messenger_close_on_cursor_moved` (Default: `v:true`)
When this value is set to `v:false`, a popup window is no longer closed automatically when moving a
cursor after the window is shown up.
#### `g:git_messenger_include_diff` (Default: `"none"`)
One of `"none"`, `"current"`, `"all"`.
When this value is not set to `"none"`, a popup window includes diff hunks of the commit at showing
up. `"current"` includes diff hunks of only current file in the commit. `"all"` includes all diff
hunks in the commit.
Please note that typing `d`/`D` or `r`/`R` in popup window toggle showing diff
hunks even if this value is set to `"none"`.
#### `g:git_messenger_git_command` (Default: `"git"`)
`git` command to retrieve commit messages. If your `git` executable is not in `$PATH` directories,
please specify the path to the executable.
#### `g:git_messenger_no_default_mappings` (Default: `v:false`)
When this value is set to `v:true`, it does not define any key mappings. `<Plug>` mappings are still
defined since they don't make any conflicts with existing mappings.
#### `g:git_messenger_into_popup_after_show` (Default: `v:true`)
When this value is set to `v:false`, running `:GitMessenger` or `<plug>(git-messenger)` again after
showing a popup does not move the cursor in the window.
#### `g:git_messenger_always_into_popup` (Default: `v:false`)
When this value is set to `v:true`, the cursor goes into a popup window when running `:GitMessenger`
or `<Plug>(git-messenger)`.
#### `g:git_messenger_extra_blame_args` (Default: `""`)
When this variable is set the contents will be appended to the git blame
command. Use it to add options (like -w).
#### `g:git_messenger_preview_mods` (Default: `""`)
This variable is effective only when opening preview window (on Neovim (0.3.0 or earlier) or Vim).
Command modifiers for opening preview window. The value will be passed as prefix of `:pedit` command.
For example, setting `"botright"` to the variable opens a preview window at bottom of the current
window. Please see `:help <mods>` for more details.
#### `g:git_messenger_max_popup_height` (Default: `v:null`)
Max lines of popup window in an integer value. Setting `v:null` means no limit.
#### `g:git_messenger_max_popup_width` (Default: `v:null`)
Max characters of popup window in an integer value. Setting `v:null` means no limit.
#### `g:git_messenger_date_format` (Default: `"%c"`)
String value to format dates in popup window. Please see `:help strftime()` to know the details of
the format.
```vim
" Example: '2019 May 26 03:27:43'
let g:git_messenger_date_format = "%Y %b %d %X"
```
#### `g:git_messenger_conceal_word_diff_marker` (Default: `v:true`)
When this value is set to `v:true`, markers for word diffs like `[-`, `-]`, `{+`, `+}` are concealed.
Set `v:false` when you don't want to hide them.
Note: Word diff is enabled by typing "r" in a popup window.
#### `g:git_messenger_floating_win_opts` (Default `{}`)
Options passed to `nvim_open_win()` on opening a popup window. This is useful when you want to
override some window options.
The following example will add single border line to the window.
```vim
let g:git_messenger_floating_win_opts = { 'border': 'single' }
```
#### `g:git_messenger_popup_content_margins` (Default: `v:true`)
Setting `v:true` means adding margins in popup window. Blank lines at the top and bottom of popup
content are inserted. And every line is indented with one whitespace character.
Setting `v:false` to this variable removes all the margins.
### Popup Window Highlight
This plugin uses color definitions from your colorscheme for highlighting stuffs in popup window by
default. This is done by linking highlight groups in [syntax/gitmessengerpopup.vim](syntax/gitmessengerpopup.vim)
on `gitmessengerpopup` filetype. Highlights for diff are common with normal `diff` filetype syntax
highlighting.
If the groups don't fit, please rearrange the highlight with `:hi link`. For example:
```vim
" Normal color in popup window with 'CursorLine'
hi link gitmessengerPopupNormal CursorLine
" Header such as 'Commit:', 'Author:' with 'Statement' highlight group
hi link gitmessengerHeader Statement
" Commit hash at 'Commit:' header with 'Special' highlight group
hi link gitmessengerHash Special
" History number at 'History:' header with 'Title' highlight group
hi link gitmessengerHistory Title
```
For another example, if you want to define colors directly, defining the colors with `:hi` works
fine as follows.
```vim
hi gitmessengerPopupNormal term=None guifg=#eeeeee guibg=#333333 ctermfg=255 ctermbg=234
hi gitmessengerHeader term=None guifg=#88b8f6 ctermfg=111
hi gitmessengerHash term=None guifg=#f0eaaa ctermfg=229
hi gitmessengerHistory term=None guifg=#fd8489 ctermfg=210
```
Note: If your colorscheme does not allocate proper color for `NormalFloat`, you may need to set
proper color to `gitmessengerPopupNormal`.
Note: `gitmessengerPopupNormal` is only available on Neovim since `winhighlight` option is used.
On Vim, simply `Normal` highlight group is used for normal color.
### Configuration for Popup Window
Filetype `gitmessengerpopup` is set in the popup window. Please hook `FileType` event to do some
local setup within a popup window.
For example:
```vim
function! s:setup_git_messenger_popup() abort
" Your favorite configuration here
" For example, set go back/forward history to <C-o>/<C-i>
nmap <buffer><C-o> o
nmap <buffer><C-i> O
endfunction
autocmd FileType gitmessengerpopup call <SID>setup_git_messenger_popup()
```
Recent Neovim supports adding border lines to floating windows. `git_messenger_floating_win_opts`
is available to set the options. And `g:git_messenger_popup_content_margins` can remove margins
within popup content if you feel margins are unnecessary.
```vim
let g:git_messenger_floating_win_opts = { 'border': 'single' }
let g:git_messenger_popup_content_margins = v:false
```
<img alt="popup with border" src="https://github.com/rhysd/ss/blob/master/git-messenger.vim/border_popup.png?raw=true" width=684 height=203 />
### Health Check
This plugin supports a health checker on Neovim. When you see some error, please run `:checkhealth`
to check your environment is ready for use of this plugin.
On Vim, please install [vim-healthcheck](https://github.com/rhysd/vim-healthcheck) and run
`:CheckHealth`. It's a plugin to run `:checkhealth` on Vim.
### Known Issues
- On Windows, `git` command installed via MSYS does not work. Please use [Git for Windows][git-win]
for now. This issue is tracked at [#57](https://github.com/rhysd/git-messenger.vim/issues/57).
## License
Distributed under [the MIT License](LICENSE)
[repo]: https://github.com/rhysd/git-messenger.vim
[ci-badge]: https://github.com/rhysd/git-messenger.vim/workflows/CI/badge.svg?branch=master&event=push
[ci]: https://github.com/rhysd/git-messenger.vim/actions?query=workflow%3ACI+branch%3Amaster
[codecov]: https://codecov.io/gh/rhysd/git-messenger.vim
[codecov-badge]: https://codecov.io/gh/rhysd/git-messenger.vim/branch/master/graph/badge.svg
[doc]: ./doc/git-messenger.txt
[nvim-install]: https://github.com/neovim/neovim/wiki/Installing-Neovim
[git-win]: https://gitforwindows.org/

View File

@ -0,0 +1,161 @@
" Global variables
let g:git_messenger_close_on_cursor_moved = get(g:, 'git_messenger_close_on_cursor_moved', v:true)
let g:git_messenger_git_command = get(g:, 'git_messenger_git_command', 'git')
let g:git_messenger_into_popup_after_show = get(g:, 'git_messenger_into_popup_after_show', v:true)
let g:git_messenger_always_into_popup = get(g:, 'git_messenger_always_into_popup', v:false)
let g:git_messenger_preview_mods = get(g:, 'git_messenger_preview_mods', '')
let g:git_messenger_extra_blame_args = get(g:, 'git_messenger_extra_blame_args', '')
let g:git_messenger_include_diff = get(g:, 'git_messenger_include_diff', 'none')
let g:git_messenger_max_popup_height = get(g:, 'git_messenger_max_popup_height', v:null)
let g:git_messenger_max_popup_width = get(g:, 'git_messenger_max_popup_width', v:null)
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_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
let s:all_popups = {}
function! s:on_cursor_moved() abort
let bufnr = bufnr('%')
if !has_key(s:all_popups, bufnr)
autocmd! plugin-git-messenger-close * <buffer>
return
endif
if s:all_popups[bufnr].cursor_moved()
autocmd! plugin-git-messenger-close * <buffer>
call gitmessenger#close_popup(bufnr)
endif
endfunction
function! s:on_buf_enter(bufnr) abort
let popup = s:popup_for(a:bufnr)
if popup is v:null
autocmd! plugin-git-messenger-buf-enter
return
endif
let b = bufnr('%')
" When entering/exiting popup window, do nothing
if popup.bufnr == b
return
endif
" This triggers s:on_close()
call popup.close()
if empty(s:all_popups)
autocmd! plugin-git-messenger-buf-enter
endif
endfunction
function! s:on_open(blame) dict abort
if !has_key(a:blame.popup, 'bufnr')
" For some reason, popup was already closed
unlet! s:all_popups[a:blame.popup.opener_bufnr]
return
endif
let opener_bufnr = a:blame.popup.opener_bufnr
let s:all_popups[opener_bufnr] = a:blame.popup
if g:git_messenger_close_on_cursor_moved
augroup plugin-git-messenger-close
autocmd CursorMoved,CursorMovedI,InsertEnter <buffer> call <SID>on_cursor_moved()
augroup END
endif
augroup plugin-git-messenger-buf-enter
execute 'autocmd BufEnter,WinEnter * call <SID>on_buf_enter(' . opener_bufnr . ')'
augroup END
endfunction
function! s:on_close(popup) dict abort
unlet! s:all_popups[a:popup.opener_bufnr]
endfunction
function! s:on_error(errmsg) abort
echohl ErrorMsg
" Avoid ^@
for line in split(a:errmsg, '\r\=\n')
echomsg line
endfor
echohl None
endfunction
" file: string
" line: number
" bufnr: number
" opts?: {}
function! gitmessenger#new(file, line, bufnr, ...) abort
" When cursor is in popup window, close the window
if gitmessenger#popup#close_current_popup()
return
endif
" Just after opening a popup window, move cursor into the window
if g:git_messenger_into_popup_after_show && has_key(s:all_popups, a:bufnr)
let p = s:all_popups[a:bufnr]
if has_key(p, 'bufnr')
call p.into()
return
endif
endif
let opts = get(a:, 1, {})
let opts.pos = getpos('.')
" Close previous popup
if has_key(s:all_popups, a:bufnr)
call s:all_popups[a:bufnr].close()
endif
let blame = gitmessenger#blame#new(a:file, a:line, {
\ 'did_open': funcref('s:on_open', [], opts),
\ 'did_close': funcref('s:on_close', [], opts),
\ 'on_error': funcref('s:on_error'),
\ 'enter_popup': g:git_messenger_always_into_popup,
\ })
if blame isnot v:null
call blame.start()
endif
endfunction
function! s:popup_for(bufnr) abort
if !has_key(s:all_popups, a:bufnr)
return v:null
endif
let popup = s:all_popups[a:bufnr]
if !has_key(popup, 'bufnr')
" Here should be unreachable
unlet! s:all_popups[a:bufnr]
return v:null
endif
return popup
endfunction
function! gitmessenger#close_popup(bufnr) abort
if gitmessenger#popup#close_current_popup()
return
endif
let p = s:popup_for(a:bufnr)
if p isnot v:null
call p.close()
endif
endfunction
function! gitmessenger#scroll(bufnr, map) abort
let p = s:popup_for(a:bufnr)
if p isnot v:null
call p.scroll(a:map)
endif
endfunction
function! gitmessenger#into_popup(bufnr) abort
let p = s:popup_for(a:bufnr)
if p isnot v:null
call p.into()
endif
endfunction

View File

@ -0,0 +1,502 @@
let s:blame = {}
function! s:git_cmd_failure(git) abort
return printf(
\ 'git-messenger: %s: `%s %s` exited with non-zero status %d',
\ join(a:git.stderr, ' '),
\ a:git.cmd,
\ join(a:git.args, ' '),
\ a:git.exit_status
\ )
endfunction
function! s:blame__error(msg) dict abort
if has_key(self.opts, 'on_error')
call self.opts.on_error(a:msg)
else
throw a:msg
endif
endfunction
let s:blame.error = funcref('s:blame__error')
function! s:blame__render() dict abort
let self.popup.contents = self.state.contents
if self.state.prev_diff !=# self.state.diff
call self.popup.set_buf_var('__gitmessenger_diff', self.state.diff)
let prev_is_word = self.state.prev_diff =~# '\.word$'
let is_word = self.state.diff =~# '\.word$'
if self.state.diff !=# 'none' && prev_is_word != is_word
call self.popup.set_buf_var('&syntax', 'gitmessengerpopup')
endif
endif
call self.popup.update()
endfunction
let s:blame.render = funcref('s:blame__render')
function! s:blame__back() dict abort
if self.state.back()
call self.render()
return
endif
if self.prev_commit ==# '' || self.oldest_commit =~# '^0\+$'
echo 'git-messenger: No older commit found'
return
endif
" Reset current state
call self.state.set_diff('none')
let args = ['--no-pager', 'blame', self.prev_commit, '-L', self.line . ',+1', '--porcelain'] + split(g:git_messenger_extra_blame_args, ' ') + ['--', self.blame_file]
call self.spawn_git(args, 's:blame__after_blame')
endfunction
let s:blame.back = funcref('s:blame__back')
function! s:blame__forward() dict abort
if self.state.forward()
call self.render()
elseif self.state.commit !=# ''
echo 'git-messenger: ' . self.state.commit . ' is the latest commit'
else
echo 'git-messenger: The latest commit'
endif
endfunction
let s:blame.forward = funcref('s:blame__forward')
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.
" Save the contents to history and show the contents in current
" popup.
call self.state.push()
call self.state.save()
call self.render()
return
endif
let opts = {
\ 'filetype': 'gitmessengerpopup',
\ 'mappings': {
\ 'q': [{-> execute('close', '')}, 'Close popup window'],
\ 'o': [funcref(self.back, [], self), 'Back to older commit'],
\ 'O': [funcref(self.forward, [], self), 'Forward to newer commit'],
\ '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"],
\ 'R': [funcref(self.reveal_diff, [v:true, v:true], self), 'Toggle all word diffs'],
\ },
\ }
if has_key(self.opts, 'did_close')
let opts.did_close = self.opts.did_close
endif
if has_key(self.opts, 'enter_popup')
let opts.enter = self.opts.enter_popup
endif
call self.state.push()
let self.popup = gitmessenger#popup#new(self.state.contents, opts)
call self.popup.open()
if has_key(self.opts, 'did_open')
call self.opts.did_open(self)
endif
endfunction
let s:blame.open_popup = funcref('s:blame__open_popup')
function! s:blame__append_lines(lines) dict abort
if !g:git_messenger_popup_content_margins
let self.state.contents += ['']
endif
let lines = a:lines
if lines[-1] ==# ''
" Strip last newline
let lines = lines[:-2]
endif
let skip_first_nl = v:true
for line in lines
if skip_first_nl && line ==# ''
continue
else
let skip_first_nl = v:false
endif
if g:git_messenger_popup_content_margins
if line ==# ''
let self.state.contents += ['']
else
let self.state.contents += [' ' . line]
endif
else
let self.state.contents += [line]
endif
endfor
if g:git_messenger_popup_content_margins && self.state.contents[-1] !~# '^\s*$'
let self.state.contents += ['']
endif
endfunction
let s:blame.append_lines = funcref('s:blame__append_lines')
function! s:blame__after_diff(next_diff, git) dict abort
let self.failed = a:git.exit_status != 0
if self.failed
call self.error(s:git_cmd_failure(a:git))
return
endif
let popup_open = has_key(self, 'popup')
if a:git.stdout == [] || a:git.stdout == [''] ||
\ (popup_open && !has_key(self.popup, 'bufnr'))
return
endif
" When getting diff with `git show --pretty=format:%b`, it may contain
" commit body. By removing line until 'diff --git ...' line, the body is
" removed (#35)
while a:git.stdout !=# [] && stridx(a:git.stdout[0], 'diff --git ') !=# 0
let a:git.stdout = a:git.stdout[1:]
endwhile
call self.append_lines(a:git.stdout)
call self.state.set_diff(a:next_diff)
if popup_open
call self.render()
else
" Note: When g:git_messenger_include_diff is not 'none' and popup is
" being opened for line which is not committed yet.
" In the case, commit hash is 0000000000000000 and `git log` is not
" available. So `git diff` is used instead and this callback is
" called.
call self.open_popup()
endif
endfunction
function! s:blame__reveal_diff(include_all, word_diff) dict abort
if a:include_all
let next_diff = 'all'
else
let next_diff = 'current'
endif
if a:word_diff
let next_diff .= '.word'
endif
if self.state.diff ==# next_diff
" Toggle diff
let next_diff = 'none'
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 self.state.contents = self.state.contents[ : diff_start-diff_offset]
endif
finally
keepjumps call setpos('.', saved)
endtry
if next_diff ==# 'none'
let self.state.diff = next_diff
call self.render()
return
endif
let hash = self.state.commit
if hash ==# ''
call self.error('Not a valid commit hash: ' . hash)
return
endif
if hash !~# '^0\+$'
" `git diff hash^..hash` is not available since hash^ is invalid when
" it is an initial commit.
let args = ['--no-pager', 'show', '--no-color', '--pretty=format:%b', hash]
else
" When the line is not committed yet, show diff against HEAD (#26)
let args = ['--no-pager', 'diff', '--no-color', 'HEAD']
endif
if a:word_diff
let args += ['--word-diff=plain']
endif
if !a:include_all
let args += ['--', self.state.diff_file_to]
let prev = self.state.diff_file_from
if prev !=# '' && prev != self.state.diff_file_to
" Note: When file was renamed, both file name before rename and file
" name after rename are necessary to show correct diff.
" If only file name after rename is specified, it shows diff as if
" the file was added at the commit not considering rename.
let args += [prev]
endif
endif
call self.spawn_git(args, funcref('s:blame__after_diff', [next_diff], self))
endfunction
let s:blame.reveal_diff = funcref('s:blame__reveal_diff')
function! s:blame__after_log(git) dict abort
let self.failed = a:git.exit_status != 0
if self.failed
call self.error(s:git_cmd_failure(a:git))
return
endif
if a:git.stdout != [] && a:git.stdout != ['']
call self.append_lines(a:git.stdout)
endif
call self.open_popup()
endfunction
function! s:blame__after_blame(git) dict abort
let self.failed = a:git.exit_status != 0
if self.failed
if a:git.stderr[0] =~# 'has only \d\+ lines\='
echo 'git-messenger: ' . get(self, 'oldest_commit', 'It') . ' is the oldest commit'
return
endif
call self.error(s:git_cmd_failure(a:git))
return
endif
" Parse `blame --porcelain` output
" Note: Output less than 11 lines are invalid. At least followings should
" be included:
" header, author, author-email, author-time, author-tz, committer-email,
" committer-time, committer-tz, summary, filename
let stdout = a:git.stdout
if len(stdout) < 11
" Note: '\n' is not "\n", it's intentional
call self.error('Unexpected `git blame` output: ' . join(stdout, '\n'))
return
endif
" Blame header
" {hash} {line number of original} {line number of final} {line offset in lines group}
"
" Please see 'THE PORCELAIN FORMAT' section of `man git-blame` for more
" details
let hash = matchstr(stdout[0], '^[[:xdigit:]]\+')
if has_key(self, 'oldest_commit') && self.oldest_commit ==# hash
echo 'git-messenger: ' . hash . ' is the oldest commit'
return
endif
let not_committed_yet = hash =~# '^0\+$'
let author = matchstr(stdout[1], '^author \zs.\+')
let author_email = matchstr(stdout[2], '^author-mail \zs\S\+')
let committer = matchstr(stdout[5], '^committer \zs.\+')
let headers = [
\ ['History', '#' . self.state.history_no()],
\ ['Commit', hash],
\ ['Author', author . ' ' . author_email],
\ ]
if author !=# committer
let committer_email = matchstr(stdout[6], '^committer-mail \zs\S\+')
let headers += [['Committer', committer . ' ' . committer_email]]
endif
if exists('*strftime')
let author_time = matchstr(stdout[3], '^author-time \zs\d\+')
let committer_time = matchstr(stdout[7], '^committer-time \zs\d\+')
if author_time ==# committer_time
let headers += [['Date', strftime(g:git_messenger_date_format, str2nr(author_time))]]
else
let headers += [['Author Date', strftime(g:git_messenger_date_format, str2nr(author_time))]]
let headers += [['Committer Date', strftime(g:git_messenger_date_format, str2nr(committer_time))]]
endif
endif
let header_width = 0
for [key, _] in headers
let len = len(key)
if len > header_width
let header_width = len
endif
endfor
let self.state.contents = []
if g:git_messenger_popup_content_margins
let self.state.contents += ['']
endif
let margin = g:git_messenger_popup_content_margins ? ' ' : ''
for [key, value] in headers
let pad = repeat(' ', header_width - len(key))
let line = printf('%s%s: %s%s', margin, key, pad, value)
let self.state.contents += [line]
endfor
if not_committed_yet
let summary = 'This line is not committed yet'
else
let summary = matchstr(stdout[9], '^summary \zs.*')
endif
let self.state.contents += ['', margin . summary]
if g:git_messenger_popup_content_margins
let self.state.contents += ['']
endif
" Reset the state
let self.prev_commit = ''
let self.blame_file = ''
" Diff target file is fallback to blame target file
let self.state.diff_file_to = self.blame_file
" Parse 'previous', 'boundary' and 'filename'
for line in stdout[10:]
" At final of output, the current line prefixed with tab is put
if line[0] ==# "\t"
break
endif
" previous {hash} {next blame file path}
"
" where {next blame file path} is a relative path from root directory of
" the repository.
let m = matchlist(line, '^previous \([[:xdigit:]]\+\) \(.\+\)$')
if m != []
let self.prev_commit = m[1]
let self.blame_file = m[2]
continue
endif
" filename {file path from root dir}
"
" where {file path} is a target file of the current commit.
" The file name may be different from current editing file because
" it might be renamed.
let filename = matchstr(line, '^filename \zs.\+$')
if filename !=# ''
let self.state.diff_file_to = filename
continue
endif
" boundary
" Boudary commit. It means current commit is the oldest.
" Nothing to do
endfor
" diff_file_from is the same as blame_file at this moment, but stored in
" another variable since it should be stored in history.
let self.state.diff_file_from = self.blame_file
let self.oldest_commit = hash
let self.state.commit = hash
" Check hash is 0000000000000000000000 it means that the line is not committed yet
if hash =~# '^0\+$'
if g:git_messenger_include_diff ==? 'none'
call self.open_popup()
return
endif
" Note: To show diffs which are not committed yet, `git log` is not
" available. Use `git diff` instead.
let next_diff = 'all'
let args = ['--no-pager', 'diff', 'HEAD']
if g:git_messenger_include_diff ==? 'current'
let next_diff = 'current'
let args += [self.blame_file]
endif
call self.spawn_git(args, funcref('s:blame__after_diff', [next_diff], self))
return
endif
let args = ['--no-pager', 'log', '--no-color', '-n', '1', '--pretty=format:%b']
if g:git_messenger_include_diff !=? 'none'
if g:git_messenger_include_diff ==? 'current'
call self.state.set_diff('current')
else
call self.state.set_diff('all')
endif
let args += ['-p', '-m']
endif
let args += [hash]
if g:git_messenger_include_diff ==? 'current'
let args += ['--', self.state.diff_file_to]
let prev = self.state.diff_file_from
if prev !=# '' && prev != self.state.diff_file_to
" Note: When file was renamed, both file name before rename and file
" name after rename are necessary to show correct diff.
" If only file name after rename is specified, it shows diff as if
" the file was added at the commit not considering rename.
let args += [prev]
endif
endif
call self.spawn_git(args, 's:blame__after_log')
endfunction
function! s:blame__spawn_git(args, callback) dict abort
let git = gitmessenger#git#new(g:git_messenger_git_command, self.git_root)
let CB = a:callback
if type(CB) == v:t_string
let CB = funcref(CB, [], self)
endif
try
call git.spawn(a:args, CB)
catch /^git-messenger: /
call self.error(v:exception)
endtry
endfunction
let s:blame.spawn_git = funcref('s:blame__spawn_git')
function! s:blame__start() dict abort
call self.spawn_git(
\ ['--no-pager', 'blame', self.blame_file, '-L', self.line . ',+1', '--porcelain'] + split(g:git_messenger_extra_blame_args, ' '),
\ 's:blame__after_blame')
endfunction
let s:blame.start = funcref('s:blame__start')
" interface Blame {
" state: BlameHistory;
" line: number;
" git_root: string;
" blame_file: string;
" prev_commit?: string;
" oldest_commit?: string;
" opts: {
" did_open: (b: Blame) => void;
" did_close: (p: Popup) => void;
" on_error: (errmsg: string) => void;
" enter_popup: boolean;
" };
" }
"
" blame_file:
" File path given to `git blame`. This can be relative to root of repo.
" Note: This does not need to be put in BlameHistory state because it is
" used by only `git blame`.
function! gitmessenger#blame#new(file, line, opts) abort
let file = resolve(a:file)
let b = deepcopy(s:blame)
let b.state = gitmessenger#history#new(file)
let b.line = a:line
let b.blame_file = file
let b.opts = a:opts
let dir = fnamemodify(file, ':p:h')
let b.git_root = gitmessenger#git#root_dir(dir)
" Validations
if b.git_root ==# ''
call b.error("git-messenger: Directory '" . dir . "' is not inside a Git repository")
return v:null
endif
return b
endfunction

View File

@ -0,0 +1,174 @@
let s:SEP = has('win32') ? '\' : '/'
function! s:find_dotgit(from) abort
let dir = finddir('.git', a:from . ';')
let file = findfile('.git', a:from . ';')
if dir ==# '' && file ==# ''
return ''
endif
" When .git directory is below the current working directory, finddir()
" returns a relative path. So ensuring an absolute path here.
let dir = dir ==# '' ? '' : fnamemodify(dir, ':p')
" When .git exists in current directory, findfile() returns relative path
" '.git' though finddir() returns an absolute path '/path/to/.git' (#49).
" Since path length will be compared, they must be both abusolute path.
let file = file ==# '' ? '' : fnamemodify(file, ':p')
" Choose larger (deeper) path (#48). When worktree directory is put in its
" main repository, the .git directory which is near to `from` should be
" chosen.
" When `dir` or `file` is empty, the other is chosen so we don't need to
" care about empty string here.
let dotgit = len(dir) > len(file) ? dir : file
" Inside .git directory is outside repository
" This check must be done before chopping last path separator otherwise it
" matches to directory like /path/to/.github/ (#70)
if stridx(a:from, dotgit) == 0
return ''
endif
if dotgit[-1:] ==# s:SEP
" [:-2] chops last path separator (/path/to/.git/ => /path/to/.git)
let dotgit = dotgit[:-2]
endif
return dotgit
endfunction
" Params:
" path: string
" base path to find .git in ancestor directories
" Returns:
" string
" empty string means root directory was not found
function! gitmessenger#git#root_dir(from) abort
let from = fnameescape(fnamemodify(a:from, ':p'))
if from[-1:] ==# s:SEP
" [:-2] chops last path separator
let from = from[:-2]
endif
let dotgit = s:find_dotgit(from)
if dotgit ==# ''
return ''
endif
" /path/to/.git => /path/to
return fnamemodify(dotgit, ':h')
endfunction
let s:git = {}
if has('nvim')
function! s:on_output_nvim(job, data, event) dict abort
if a:data == ['']
return
endif
" Output from Git might contain \r for example when the commit
" author's text editor uses \r\n for newlines. But Neovim reads output
" from the process line by line based on \n. The trailing \r remains
" when \r\n is used for newlines. This removes the trailing \r (#75).
call map(a:data, 'v:val[len(v:val)-1] ==# "\r" ? v:val[:-2] : v:val')
let self[a:event][-1] .= a:data[0]
call extend(self[a:event], a:data[1:])
endfunction
function! s:on_exit_nvim(job, code, event) dict abort
let self.exit_status = a:code
call self.on_exit(self)
endfunction
else
function! s:git__finalize_vim(ch) dict abort
if has_key(self, 'finalized') && self.finalized
return
endif
" Note:
" Workaround for Vim's exit_cb behavior. When the callback is called,
" sometimes channel for stdout and/or stderr is not closed yet. So
" their status may be 'open'. As workaround for the behavior, we do
" polling to check the channel statuses with 1 msec interval until the
" statuses are set to 'close'. (#16)
let out_opt = {'part': 'out'}
let err_opt = {'part': 'err'}
while 1
let out_status = ch_status(a:ch, out_opt)
let err_status = ch_status(a:ch, err_opt)
if out_status !=# 'open' && out_status !=# 'buffered' &&
\ err_status !=# 'open' && err_status !=# 'buffered'
let self.finalized = v:true
call self.on_exit(self)
return
endif
sleep 1m
endwhile
endfunction
let s:git.finalize_vim = funcref('s:git__finalize_vim')
function! s:on_output_vim(event, ch, msg) dict abort
call extend(self[a:event], split(a:msg, '\r\=\n', 1))
endfunction
function! s:on_exit_vim(ch, code) dict abort
let self.exit_status = a:code
call self.finalize_vim(a:ch)
endfunction
endif
" Params:
" args: string[]
" on_exit: (git: Git) => void
" Returns:
" Job ID of the spawned process
function! s:git__spawn(args, on_exit) dict abort
let cmdline = [self.cmd, '-C', self.dir] + a:args
if has('nvim')
let self.stdout = ['']
let self.stderr = ['']
let job_id = jobstart(cmdline, {
\ 'cwd': self.dir,
\ 'on_stdout' : funcref('s:on_output_nvim', [], self),
\ 'on_stderr' : funcref('s:on_output_nvim', [], self),
\ 'on_exit' : funcref('s:on_exit_nvim', [], self),
\ })
if job_id == 0
throw 'git-messenger: Invalid arguments: ' . string(a:args)
elseif job_id == -1
throw 'git-messenger: Command does not exist: ' . self.cmd
endif
else
let self.stdout = []
let self.stderr = []
let job_id = job_start(cmdline, {
\ 'cwd': self.dir,
\ 'out_cb' : funcref('s:on_output_vim', ['stdout'], self),
\ 'err_cb' : funcref('s:on_output_vim', ['stderr'], self),
\ 'exit_cb' : funcref('s:on_exit_vim', [], self),
\ })
endif
let self.job_id = job_id
let self.on_exit = a:on_exit
let self.args = a:args
return job_id
endfunction
let s:git.spawn = funcref('s:git__spawn')
" Creates new Git instance. Git instance represents one-shot Git command
" asynchronous execution.
"
" Params:
" cmd: string
" 'git' command to run Git
" dir: string
" Directory path to run Git
" Returns:
" Git object
function! gitmessenger#git#new(cmd, dir) abort
let g = deepcopy(s:git)
let g.cmd = a:cmd
let g.dir = a:dir
return g
endfunction

View File

@ -0,0 +1,132 @@
" Note: Index 0 means the latest entry of history
" interface BlameState {
" commit: string;
" contents: string[];
" diff_file_to: string;
" diff_file_from: string;
" diff: 'none' | 'all' | 'current';
" }
"
" interface BlameHistory extends BlameState {
" _index: number;
" _history: BlameState[];
" }
"
" History of chain of `git blame` with contents.
"
" contents:
" Lines of contents of popup
" diff_file_to:
" File path for diff. It represents the file path after the commit.
" When the file was renamed while the commit, it is different from 'diff_file_from'
" diff_file_from:
" File path for diff. It represents the file path before the commit.
" When the file was renamed while the commit, it is different from 'diff_file_to'
" diff:
" Diff type. Please see document for g:git_messenger_include_diff
" commit:
" Commit hash of the commit
" _index:
" Index of history which indicates current state. 0 means the latest history
" _history:
" History of chain of blame entries. Latter is older.
let s:history = { '_index': 0, '_history': [] }
" Create new empty history entry as the latest
function! s:history__push() dict abort
" Note: copy() is necessary because the contents may be updated later
" for diff
" Note: 'commit' is a special key which will be never changed. This field
" will be used for checking invariant state on saving the state
let self._history += [{ 'commit': self.commit }]
let self._index = len(self._history) - 1
endfunction
let s:history.push = funcref('s:history__push')
" Save current state to current history entry
function! s:history__save() dict abort
if self._index > len(self._history)
throw printf('FATAL: Invariant error on saving history. Index %d is out of range. Length of history is %d', self._index, len(self._history))
endif
let e = self._history[self._index]
if self.commit !=# e.commit
throw printf('FATAL: Invariant error on saving history. Current commit hash %s is different from commit hash in history %s', self.commit, e.commit)
endif
let e.diff = self.diff
let e.contents = copy(self.contents)
let e.diff_file_to = self.diff_file_to
let e.commit = self.commit
let e.diff_file_from = self.diff_file_from
endfunction
let s:history.save = funcref('s:history__save')
" Load specific history entry as current state
function! s:history__load(index) dict abort
let e = self._history[a:index]
" Note: copy() is necessary because the contents may be updated later
" for diff. Without copy(), it modifies array in self.history directly
" but that's not intended.
let self.contents = copy(e.contents)
call self.set_diff(e.diff)
let self.commit = e.commit
let self.diff_file_to = e.diff_file_to
let self.diff_file_from = e.diff_file_from
let self._index = a:index
endfunction
let s:history._load = funcref('s:history__load')
function! s:history__history_no() dict abort
return len(self._history)
endfunction
let s:history.history_no = funcref('s:history__history_no')
function! s:history__set_diff(diff) dict abort
let self.prev_diff = self.diff
let self.diff = a:diff
endfunction
let s:history.set_diff = funcref('s:history__set_diff')
" Go back to older. Load older history entry to current history.
" Returns boolean which is true when older entry was found.
function! s:history__back() dict abort
let next_index = self._index + 1
call self.save()
if len(self._history) <= next_index
return v:false
endif
call self._load(next_index)
return v:true
endfunction
let s:history.back = funcref('s:history__back')
" Go forward to newer. Load newer history entry to current state.
" Returns boolean which is true when newer entry was found.
function! s:history__forward() dict abort
" Note: Index 0 is the latest entry
let next_index = self._index - 1
if next_index < 0
return v:false
endif
call self.save()
call self._load(next_index)
return v:true
endfunction
let s:history.forward = funcref('s:history__forward')
function! gitmessenger#history#new(filepath) abort
let h = deepcopy(s:history)
let h.contents = []
let h.diff_file_to = a:filepath
let h.diff_file_from = a:filepath
let h.diff = 'none'
let h.prev_diff = ''
let h.commit = ''
return h
endfunction

View File

@ -0,0 +1,339 @@
let s:popup = {}
let s:floating_window_available = has('nvim') && exists('*nvim_win_set_config')
function! s:get_global_pos() abort
let pos = win_screenpos('.')
return [pos[0] + winline() - 1, pos[1] + wincol() - 1]
endfunction
function! s:popup__close() dict abort
if !has_key(self, 'bufnr')
" Already closed
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'
endif
unlet self.bufnr
unlet self.win_id
if has_key(self.opts, 'did_close')
call self.opts.did_close(self)
endif
endfunction
let s:popup.close = funcref('s:popup__close')
function! s:popup__get_winnr() dict abort
if !has_key(self, 'bufnr')
return 0
endif
" Note: bufwinnr() is not available here because there may be multiple
" windows which open the buffer. This situation happens when enter <C-w>v
" in popup window. It opens a new normal window with the popup's buffer.
return win_id2win(self.win_id)
endfunction
let s:popup.get_winnr = funcref('s:popup__get_winnr')
function! s:popup__set_buf_var(name, value) dict abort
if has_key(self, 'bufnr')
call setbufvar(self.bufnr, a:name, a:value)
endif
endfunction
let s:popup.set_buf_var = funcref('s:popup__set_buf_var')
function! s:popup__scroll(map) dict abort
let winnr = self.get_winnr()
if winnr == 0
return
endif
execute winnr . 'wincmd w'
sandbox let input = eval('"\<'.a:map.'>"')
execute 'normal!' input
wincmd p
endfunction
let s:popup.scroll = funcref('s:popup__scroll')
function! s:popup__into() dict abort
let winnr = self.get_winnr()
if winnr == 0
return
endif
execute winnr . 'wincmd w'
endfunction
let s:popup.into = funcref('s:popup__into')
function! s:popup__window_size() dict abort
let margin = g:git_messenger_popup_content_margins ? 1 : 0
let has_max_width = type(g:git_messenger_max_popup_width) == v:t_number
if has_max_width
" ` - 1` for considering right margin
let max_width = g:git_messenger_max_popup_width - margin
endif
let width = 0
let height = 0
for line in self.contents
let lw = strdisplaywidth(line)
if lw > width
if has_max_width && lw > max_width
let height += lw / max_width + 1
let width = max_width
continue
endif
let width = lw
endif
let height += 1
endfor
let width += margin " right margin
if type(g:git_messenger_max_popup_height) == v:t_number && height > g:git_messenger_max_popup_height
let height = g:git_messenger_max_popup_height
endif
return [width, height]
endfunction
let s:popup.window_size = funcref('s:popup__window_size')
function! s:popup__floating_win_opts(width, height) dict abort
let border = has_key(g:git_messenger_floating_win_opts, 'border')
\ && index(
\ ['single', 'double', 'rounded', 'solid'], g:git_messenger_floating_win_opts['border']
\ ) != -1 ? 2 : 0
" &lines - 1 because it is not allowed to overlay a floating window on a status line.
" Bottom line of a floating window must be less than line of command line. (#80)
if self.opened_at[0] + a:height + border <= &lines - 1
let vert = 'N'
let row = self.opened_at[0]
else
let vert = 'S'
let row = self.opened_at[0] - 1 - border
endif
if self.opened_at[1] + a:width + border <= &columns
let hor = 'W'
let col = self.opened_at[1] - 1
else
let hor = 'E'
let col = self.opened_at[1] - border
endif
return extend({
\ 'relative': 'editor',
\ 'anchor': vert . hor,
\ 'row': row,
\ 'col': col,
\ 'width': a:width,
\ 'height': a:height,
\ 'style': 'minimal',
\ },
\ g:git_messenger_floating_win_opts)
endfunction
let s:popup.floating_win_opts = funcref('s:popup__floating_win_opts')
function! s:popup__get_opener_winnr() dict abort
let winnr = win_id2win(self.opener_winid)
if winnr != 0
return winnr
endif
let winnr = bufwinnr(self.opener_bufnr)
if winnr > 0
return winnr
endif
return 0
endfunction
let s:popup.get_opener_winnr = funcref('s:popup__get_opener_winnr')
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()
let self.type = s:floating_window_available ? 'floating' : 'preview'
let [width, height] = self.window_size()
" Open window
if self.type ==# 'floating'
let opts = self.floating_win_opts(width, height)
let win_id = nvim_open_win(self.opener_bufnr, v:true, opts)
else
let curr_pos = getpos('.')
let mods = 'noswapfile'
if g:git_messenger_preview_mods !=# ''
let mods .= ' ' . g:git_messenger_preview_mods
endif
" :pedit! is not available since it refreshes the file buffer (#39)
execute mods 'new'
set previewwindow
call setpos('.', curr_pos)
wincmd P
execute height . 'wincmd _'
let win_id = win_getid()
endif
" Setup content
enew!
let popup_bufnr = bufnr('%')
" Note: Set conceallevel for hiding word diff markers
setlocal
\ buftype=nofile bufhidden=wipe nomodified nobuflisted noswapfile nonumber
\ nocursorline wrap nonumber norelativenumber signcolumn=no nofoldenable
\ nospell nolist nomodeline conceallevel=2
call setline(1, self.contents)
setlocal nomodified nomodifiable
" Setup highlights
if has('nvim')
setlocal winhighlight=Normal:gitmessengerPopupNormal
endif
if has_key(self.opts, 'mappings')
for m in keys(self.opts.mappings)
execute printf('nnoremap <buffer><silent><nowait>%s :<C-u>call b:__gitmessenger_popup.opts.mappings["%s"][0]()<CR>', m, m)
endfor
nnoremap <buffer><silent><nowait>? :<C-u>call b:__gitmessenger_popup.echo_help()<CR>
endif
if has_key(self.opts, 'filetype')
let &l:filetype = self.opts.filetype
endif
" Ensure to close popup
let b:__gitmessenger_popup = self
execute 'autocmd BufWipeout,BufLeave <buffer> call getbufvar(' . popup_bufnr . ', "__gitmessenger_popup").close()'
if has_key(self.opts, 'enter') && !self.opts.enter
noautocmd wincmd p
if self.type !=# 'floating'
" Opening a preview window may move global position of the cursor.
" `opened_at` is used for checking if the popup window should be
" closed on `CursorMoved` event. If the position is not updated
" here, the event wrongly will refer the position before opening
" the preview window.
let self.opened_at = s:get_global_pos()
endif
endif
let self.bufnr = popup_bufnr
let self.win_id = win_id
endfunction
let s:popup.open = funcref('s:popup__open')
function! s:popup__update() dict abort
" Note: `:noautocmd` to prevent BufLeave autocmd event (#13)
" It should be ok because the cursor position is finally back to the first
" position.
let prev_winnr = winnr()
let popup_winnr = self.get_winnr()
if popup_winnr == 0
return
endif
let opener_winnr = self.get_opener_winnr()
if opener_winnr == 0
return
endif
if opener_winnr != prev_winnr
noautocmd execute opener_winnr . 'wincmd w'
endif
try
let [width, height] = self.window_size()
" Window must be configured in opener buffer since the window position
" is relative to cursor
if self.type ==# 'floating'
let id = win_getid(popup_winnr)
if id == 0
return
endif
let opts = self.floating_win_opts(width, height)
call nvim_win_set_config(id, opts)
endif
noautocmd execute popup_winnr . 'wincmd w'
if self.type ==# 'preview'
execute height . 'wincmd _'
endif
setlocal modifiable
silent %delete _
call setline(1, self.contents)
setlocal nomodified nomodifiable
finally
if winnr() != prev_winnr
noautocmd execute prev_winnr . 'wincmd w'
endif
endtry
endfunction
let s:popup.update = funcref('s:popup__update')
" Returns if the cursor moved since this popup window had opened
function! s:popup__cursor_moved() dict abort
return s:get_global_pos() != self.opened_at
endfunction
let s:popup.cursor_moved = funcref('s:popup__cursor_moved')
function! s:popup__echo_help() dict abort
if has_key(self.opts, 'mappings')
let maps = keys(self.opts.mappings)
call sort(maps, 'i')
let maps += ['?']
for map in maps
if map ==# '?'
let desc = 'Show this help'
else
let desc = self.opts.mappings[map][1]
endif
echohl Identifier | echo ' ' . map
echohl Comment | echon ' : '
echohl None | echon desc
endfor
endif
endfunction
let s:popup.echo_help = funcref('s:popup__echo_help')
" contents: string[] // lines of contents
" opts: {
" floating?: boolean;
" bufnr?: number;
" cursor?: [number, number]; // (line, col)
" filetype?: string;
" did_close?: (pupup: Popup) => void;
" mappings?: {
" [keyseq: string]: [() => void, string];
" };
" enter?: boolean
" }
function! gitmessenger#popup#new(contents, opts) abort
let p = deepcopy(s:popup)
let opts = { 'floating': v:true }
call extend(opts, a:opts)
let p.opts = opts
let p.contents = a:contents
return p
endfunction
" When current window is popup, close the window.
" Returns true when popup window was closed
function! gitmessenger#popup#close_current_popup() abort
if !exists('b:__gitmessenger_popup')
return 0
endif
call b:__gitmessenger_popup.close()
" TODO?: Back to opened_at pos by setpos()
return 1
endfunction

View File

@ -0,0 +1,80 @@
function! s:check_job() abort
if !has('nvim') && !has('job')
call health#report_error('Not supported since +job feature is not enabled')
else
call health#report_ok('+job is available to execute Git command')
endif
endfunction
function! s:check_floating_window() abort
if !has('nvim')
return
endif
if !exists('*nvim_win_set_config')
call health#report_warn(
\ 'Neovim 0.3.0 or earlier does not support floating window feature. Preview window is used instead',
\ 'Please install Neovim 0.4.0 or later')
return
endif
" XXX: Temporary
try
noautocmd let win_id = nvim_open_win(bufnr('%'), v:false, {
\ 'relative': 'editor',
\ 'row': 0,
\ 'col': 0,
\ 'width': 2,
\ 'height': 2,
\ })
noautocmd call nvim_win_close(win_id, v:true)
catch /^Vim\%((\a\+)\)\=:E118/
call health#report_error(
\ 'Your Neovim is too old',
\ [
\ 'Please update Neovim to 0.4.0 or later',
\ 'If the version does not fix the error, please make an issue at https://github.com/rhysd/git-messenger.vim',
\ ])
return
endtry
call health#report_ok('Floating window is available for popup window')
endfunction
function! s:check_git_binary() abort
let cmd = get(g:, 'git_messenger_git_command', 'git')
if !executable(cmd)
call health#report_error('`' . cmd . '` command is not found. Please set proper command to g:git_messenger_git_command')
return
endif
let output = substitute(system(cmd . ' -C . --version'), '\r\=\n', '', 'g')
if v:shell_error
call health#report_error('Git command `' . cmd . '` is broken (v1.8.5 or later is required): ' . output)
return
endif
call health#report_ok('Git command `' . cmd . '` is available: ' . output)
endfunction
function! s:check_vim_version() abort
if has('nvim')
return
endif
if v:version < 800
call health#report_error(
\ 'Your Vim version is too old: ' . v:version,
\ 'Please install Vim 8.0 or later')
return
endif
call health#report_ok('Vim version is fine: ' . v:version)
endfunction
function! health#gitmessenger#check() abort
call s:check_job()
call s:check_git_binary()
call s:check_floating_window()
call s:check_vim_version()
endfunction

View File

@ -0,0 +1,424 @@
*git-messenger.txt* Reveal the hidden message by git under the cursor quickly
Author : rhysd <https://rhysd.github.io>
CONTENTS *git-messenger-contents*
Introduction |git-messenger-introduction|
Screenshot |git-messenger-screenshot|
Install |git-messenger-install|
Commands |git-messenger-commands|
Mappings |git-messenger-mappings|
Variables |git-messenger-variables|
Highlights |git-messenger-highlights|
Popup Window Config |git-messenger-popup-window-config|
Health Check |git-messenger-health-check|
Known Issues |git-messenger-known-issues|
Repository |git-messenger-repository-page|
License |git-messenger-license|
==============================================================================
INTRODUCTION *git-messenger-introduction*
*git-messenger.vim* is a Vim/Neovim plugin to reveal the hidden message under
the cursor quickly. It shows the last commit message under the cursor in popup
window.
When you modifying unfamiliar codes, you would sometimes wonder 'why was this
line added?' or 'why this value was chosen?' in the source code. The answer
sometimes lays in a commit message, especially in message of the last commit
which modifies the line.
This plugin shows the message of the last commit in a 'popup window'. The
popup window is implemented in
- Floating window on Neovim 0.4 or later
- Preview window on Vim 8+ or Neovim 0.3 or earlier
The floating window is definitely recommended since it can shows the
information near the cursor.
If you see some error message, please try |git-messenger-health-check|.
==============================================================================
SCREENSHOT *git-messenger-screenshot*
1. Show popup window with Neovim v0.4.0-dev
https://github.com/rhysd/ss/blob/master/git-messenger.vim/demo.gif
2. Exploring older commits
https://github.com/rhysd/ss/blob/master/git-messenger.vim/history.gif
3. Exploring diff of the commit
https://github.com/rhysd/ss/blob/master/git-messenger.vim/diff.gif
You may be also interested in |g:git_messenger_include_diff|
==============================================================================
INSTALL *git-messenger-install*
Please ensure the following requirement before installing this plugin.
- Git v1.8.5 (for -C option of "git" command)
If you use any package manager, please follow its instruction.
With vim-plug:
>
Plug 'rhysd/git-messenger.vim'
<
With dein.vim:
>
call dein#add('rhysd/git-messenger.vim', {
\ 'lazy' : 1,
\ 'on_cmd' : 'GitMessenger',
\ 'on_map' : '<Plug>(git-messenger-',
\ })
<
With minpac:
>
call minpac#add('rhysd/git-messenger.vim')
<
If you're using Vim's builtin packager, please follow instruction at |pack-add|.
vim-plug: https://github.com/junegunn/vim-plug
dein.vim: https://github.com/Shougo/dein.vim
minpac: https://github.com/k-takata/minpac
To enable a floating window support, you need to install Neovim 0.4 or later.
Please follow the official instruction.
https://github.com/neovim/neovim/wiki/Installing-Neovim
==============================================================================
COMMANDS *git-messenger-commands*
*:GitMessenger*
It opens a popup window with the last commit message which modified the line
at cursor. The popup window shows following contents:
- History: Number of history. In popup window, h/l navigates to previous or
next commit.
- Commit: The commit hash
- Author: Author name and mail address of the commit
- Committer: Committer name and mail address of the commit when
committer is different from author
- Date: Author date of the commit in system format
- When a committer date and an author date are different, both
'Author Date' and 'Committer Date' are shown
- Summary: First line after a date header line is a summary of commit
- Body: After summary, commit body is put (if the commit has body)
The popup window will be automatically closed when you move the cursor so
you don't need to close it manually.
Running command again after the popup window shows up moves the cursor into
the window. This behavior is useful when the commit message is too long and
window cannot show the whole content. By moving the cursor into the popup
window, you can see the rest of contents by scrolling it. This behavior can
be suppressed with |g:git_messenger_into_popup_after_show|.
Following mappings are defined within popup window.
| Mapping | Description |
| ------- | ------------------------------------------------------------ |
| q | Close the popup window |
| o | "o"lder. Back to older commit at the line |
| O | Opposite to older. Forward to newer commit at the line |
| d | Toggle unified diff hunks only in current file of the commit |
| 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 |
| ? | Show mappings help |
*:GitMessengerClose*
Though a popup window is automatically closed by default, it closes the
popup window explicitly. It is useful when you set
|g:git_messenger_close_on_cursor_moved| to |v:false|.
==============================================================================
MAPPINGS *git-messenger-mappings*
Some <Plug> mappings are available to operate a popup window. They can be
mapped to your favorite key sequences. For example:
*<Plug>(git-messenger)*
The same as running |:GitMessenger| command.
*<Plug>(git-messenger-close)*
The same as running |:GitMessengerClose| command.
*<Plug>(git-messenger-into-popup)*
Moves the cursor into the popup window. It's useful when you want to scroll
the content and close the window.
*<Plug>(git-messenger-scroll-down-1)*
Scroll down the popup window by 1 line directly.
*<Plug>(git-messenger-scroll-up-1)*
Scroll up the popup window by 1 line directly.
*<Plug>(git-messenger-scroll-down-page)*
Scroll down the popup window by 1 page directly.
*<Plug>(git-messenger-scroll-up-page)*
Scroll up the popup window by 1 page directly.
*<Plug>(git-messenger-scroll-down-half)*
Scroll down the popup window by half page directly.
*<Plug>(git-messenger-scroll-up-half)*
Scroll up the popup window by half page directly.
==============================================================================
VARIABLES *git-messenger-variables*
Some global variables are available to configure the behavior of this plugin.
*g:git_messenger_close_on_cursor_moved* (Default: |v:true|)
When this value is set to |v:false|, a popup window is no longer closed
automatically when moving a cursor after the window is shown up.
*g:git_messenger_include_diff* (Default: "none")
When this value is set to |v:true|, a popup window includes diff hunks of the
commit by default.
Note: Typing "d" or "r" in popup window reveals diff hunks even if this
value is set to |v:false|.
One of "none", "current", "all".
When this value is NOT set to "none", a popup window includes diff hunks of
the commit at showing up. "current" includes diff hunks of only current file
in the commit. "all" includes all diff hunks in the commit.
Note: Typing d/D or r/R mappings in popup window reveal diff hunks even if
this value is set to "none".
*g:git_messenger_git_command* (Default: "git")
"git" command to retrieve commit messages. If your "git" executable is not
in "$PATH" directories, please specify the path to the executable.
*g:git_messenger_no_default_mappings* (Default: |v:false|)
When this value is set to |v:true|, it does not define any key mappings.
<Plug> mappings are still defined since they don't make any conflicts with
existing mappings.
*g:git_messenger_into_popup_after_show* (Default: |v:true|)
When this value is set to |v:false|, run |:GitMessenger| or
|<plug>(git-messenger)| again after showing a popup does not move the cursor
in the window.
*g:git_messenger_always_into_popup* (Default: |v:false|)
When this value is set to |v:true|, the cursor goes into a popup window when
running |:GitMessenger| or |<Plug>(git-messenger)|.
*g:git_messenger_extra_blame_args* (Default: "")
When this variable is set the contents will be appended to the git blame
command. Use it to add options (like -w).
*g:git_messenger_preview_mods* (Default: "")
Command modifiers for opening preview window. The value will be passed as
prefix of |:pedit| command.
For example, setting "botright" to the variable opens a preview window at
bottom of the current window. Please see |<mods>| for more details.
NOTE: This variable is effective only when opening preview window (on Neovim
(0.3.0 or earlier) or Vim).
*g:git_messenger_max_popup_height* (Default: |v:null|)
Max lines of popup window in an integer value. Setting |v:null| means no
limit.
*g:git_messenger_max_popup_width* (Default: |v:null|)
Max characters of popup window in an integer value. Setting |v:null| means no
limit.
*g:git_messenger_date_format* (Default: "%c")
String value to format dates in popup window. Please see |strftime()| to know
the details of the format.
>
" Example: '2019 May 26 03:27:43'
let g:git_messenger_date_format = "%Y %b %d %X"
<
*g:git_messenger_conceal_word_diff_marker* (Default: |v:true|)
When this value is set to |v:true|, markers for word diffs like "[-", "-]",
"{+", "+}" are concealed. Set |v:false| when you don't want to hide them.
Note: Word diff is enabled by typing "r" in a popup window.
*g:git_messenger_floating_win_opts* (Default: {})
Options passed to |nvim_open_win| on opening a popup window. This is useful
when you want to override some window options.
The following example will add single border line to the window.
>
let g:git_messenger_floating_win_opts = { 'border': 'single' }
<
*g:git_messenger_popup_content_margins* (Default: |v:true|)
Setting |v:true| means adding margins in popup window. Blank lines at the top
and bottom of popup content are inserted. And every line is indented with one
whitespace character.
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|.
==============================================================================
HIGHLIGHTS *git-messenger-highlights*
|git-messenger.vim| uses color definitions from your colorscheme for
highlighting stuffs in popup window by default. This is done by linking
highlight groups on "gitmessengerpopup" filetype.
Highlights for diff are common with normal |diff| filetype syntax highlighting.
If the groups don't fit, please rearrange the highlight with |:hi-link|.
For example:
>
" Normal color in popup window with 'CursorLine'
hi link gitmessengerPopupNormal CursorLine
" Header such as 'Commit:', 'Author:' with 'Statement' highlight group
hi link gitmessengerHeader Statement
" Commit hash at 'Commit:' header with 'Special' highlight group
hi link gitmessengerHash Special
" History number at 'History:' header with 'Title' highlight group
hi link gitmessengerHistory Title
" Email address at 'Author:' header with 'Comment' highlight group
hi link gitmessengerEmail Comment
<
For another example, if you want to define colors directly, defining the
colors with |:hi| works fine as follows.
>
hi gitmessengerPopupNormal term=None guifg=#eeeeee guibg=#333333 ctermfg=255 ctermbg=234
hi gitmessengerHeader term=None guifg=#88b8f6 ctermfg=111
hi gitmessengerHash term=None guifg=#f0eaaa ctermfg=229
hi gitmessengerHistory term=None guifg=#fd8489 ctermfg=210
hi gitmessengerEmail term=None guifg=#999999 ctermfg=245
<
Note: If your colorscheme does not allocate proper color for |hl-NormalFloat|,
you may need to set proper color to "gitmessengerPopupNormal". For example, if
your colorscheme use the same color for both normal background color and popup
background color, linking "gitmessengerPopupNormal" to different background
color like |hl-CursorLine| would be better.
Note: "gitmessengerPopupNormal" is only available on Neovim since
'winhighlight' option is used. On Vim, simply |hl-Normal| is used for normal
color.
==============================================================================
CONFIGURATION IN POPUP WINDOW *git-messenger-popup-window-config*
Filetype *gitmessengerpopup* is set in the popup window. Please hook |FileType|
event to do some local setup within a popup window.
For example:
>
function! s:setup_gitmessengerpopup() abort
" For example, set go back/forward history to <C-o>/<C-i>
nmap <buffer><C-o> o
nmap <buffer><C-i> O
endfunction
autocmd FileType gitmessengerpopup call <SID>setup_gitmessengerpopup()
<
==============================================================================
HEALTH CHECK *git-messenger-health-check*
|git-messenger.vim| supports a health checker on Neovim. When you see some
error, please run `:checkhealth` to check your environment is ready for use
of this plugin.
>
:checkhealth
<
On Vim, please install vim-healthcheck and run `:CheckHealth`. It's a plugin to
run `:checkhealth` on Vim.
>
:CheckHealth
<
https://github.com/rhysd/vim-healthcheck
==============================================================================
KNOWN ISSUES *git-messenger-known-issues*
1.
On Windows, `git` command installed via MSYS does not work. Please use
Git for Windows for now. This issue is tracked at
https://github.com/rhysd/git-messenger.vim/issues/57
==============================================================================
REPOSITORY *git-messenger-repository-page*
|git-messenger.vim| is developed at GitHub. Please make a new issue or PR to
join development.
https://github.com/rhysd/git-messenger.vim
==============================================================================
LICENSE *git-messenger-license*
|git-messenger.vim| is distributed under The MIT license.
Copyright (c) 2019 rhysd
Permission is hereby granted, free of charge, to any person obtaining
a copy of this software and associated documentation files (the
"Software"), to deal in the Software without restriction, including
without limitation the rights to use, copy, modify, merge, publish,
distribute, sublicense, and/or sell copies of the Software, and to
permit persons to whom the Software is furnished to do so, subject to
the following conditions:
The above copyright notice and this permission notice shall be
included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
==============================================================================
vim:tw=78:ts=8:ft=help:norl:et:fen:fdl=0:

View File

@ -0,0 +1,24 @@
if exists('g:loaded_git_messenger')
finish
endif
let g:loaded_git_messenger = 1
" Global variables used in plugin/gitmessenger.vim
let g:git_messenger_no_default_mappings = get(g:, 'git_messenger_no_default_mappings', v:false)
command! -nargs=0 -bar GitMessenger call gitmessenger#new(expand('%:p'), line('.'), bufnr('%'))
command! -nargs=0 -bar GitMessengerClose call gitmessenger#close_popup(bufnr('%'))
nnoremap <silent><Plug>(git-messenger) :<C-u>GitMessenger<CR>
nnoremap <silent><Plug>(git-messenger-close) :<C-u>call gitmessenger#close_popup(bufnr('%'))<CR>
nnoremap <silent><Plug>(git-messenger-into-popup) :<C-u>call gitmessenger#into_popup(bufnr('%'))<CR>
nnoremap <silent><Plug>(git-messenger-scroll-down-1) :<C-u>call gitmessenger#scroll(bufnr('%'), 'C-e')<CR>
nnoremap <silent><Plug>(git-messenger-scroll-up-1) :<C-u>call gitmessenger#scroll(bufnr('%'), 'C-y')<CR>
nnoremap <silent><Plug>(git-messenger-scroll-down-page) :<C-u>call gitmessenger#scroll(bufnr('%'), 'C-f')<CR>
nnoremap <silent><Plug>(git-messenger-scroll-up-page) :<C-u>call gitmessenger#scroll(bufnr('%'), 'C-b')<CR>
nnoremap <silent><Plug>(git-messenger-scroll-down-half) :<C-u>call gitmessenger#scroll(bufnr('%'), 'C-d')<CR>
nnoremap <silent><Plug>(git-messenger-scroll-up-half) :<C-u>call gitmessenger#scroll(bufnr('%'), 'C-u')<CR>
if !g:git_messenger_no_default_mappings
nmap <Leader>gm <Plug>(git-messenger)
endif

View File

@ -0,0 +1,50 @@
if exists('b:current_syntax')
finish
endif
syn match gitmessengerHeader '^ \=\%(History\|Commit\|\%(Author \|Committer \)\=Date\|Author\|Committer\):' display
syn match gitmessengerHash '\%(^ \=Commit: \+\)\@<=[[:xdigit:]]\+' display
syn match gitmessengerHistory '\%(^ \=History: \+\)\@<=#\d\+' display
syn match gitmessengerEmail '\%(^ \=\%(Author\|Committer\): \+.*\)\@<=<.\+>' display
" Diff included in popup
" There are two types of diff format; 'none' 'current', 'all', 'current.word', 'all.word'.
" 'current.word' and 'all.word' are for word diff. And 'current' and 'all' are " for unified diff.
" Define different highlights for unified diffs and word diffs.
" b:__gitmessenger_diff is set by Blame.render() in blame.vim.
if get(b:, '__gitmessenger_diff', '') =~# '\.word$'
if has('conceal') && get(g:, 'git_messenger_conceal_word_diff_marker', v:true)
syn region diffWordsRemoved matchgroup=Conceal start=/\[-/ end=/-]/ concealends oneline
syn region diffWordsAdded matchgroup=Conceal start=/{+/ end=/+}/ concealends oneline
else
syn region diffWordsRemoved start=/\[-/ end=/-]/ oneline
syn region diffWordsAdded start=/{+/ end=/+}/ oneline
endif
else
syn match diffRemoved "^ \=-.*" display
syn match diffAdded "^ \=+.*" display
endif
syn match diffFile "^ \=diff --git .*" display
syn match diffOldFile "^ \=--- a\>.*" display
syn match diffNewFile "^ \=+++ b\>.*" display
syn match diffIndexLine "^ \=index \x\{7,}\.\.\x\{7,}.*" display
syn match diffLine "^ \=@@ .*" display
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
hi def link diffOldFile diffFile
hi def link diffNewFile diffFile
hi def link diffIndexLine PreProc
hi def link diffFile Type
hi def link diffRemoved Special
hi def link diffAdded Identifier
hi def link diffWordsRemoved diffRemoved
hi def link diffWordsAdded diffAdded
hi def link diffLine Statement
let b:current_syntax = 'gitmessengerpopup'

View File

@ -0,0 +1,70 @@
call themis#option('exclude', 'test/README.md')
call themis#option('exclude', 'test/Guardfile')
if $THEMIS_PROFILE !=# ''
execute 'profile' 'start' $THEMIS_PROFILE
profile! file ./autoload/*
profile! file ./plugin/*
profile! file ./syntax/*
endif
let s:helper = themis#helper('assert')
call themis#helper('command').with(s:helper)
function! GetPopupNoWait() abort
for b in range(1, bufnr('$'))
let p = getbufvar(b, '__gitmessenger_popup', v:null)
if p isnot v:null
return p
endif
endfor
return v:null
endfunction
function! GetPopup() abort
return CallWithTimeout(1, funcref('GetPopupNoWait'))
endfunction
function! CallWithTimeout(timeout, func) abort
let total = 0
while v:true
let v = a:func()
if v isnot v:null
return v
endif
sleep 100m
let total += 0.1
if total >= a:timeout
return v:null
endif
endwhile
endfunction
function! WaitUntil(func, ...) abort
let timeout = get(a:, 1, 1) " 1sec by default
let total = 0
while !a:func()
sleep 100m
let total += 0.1
if total >= timeout
" Note: v:true/v:false are not supported by themis.vim
" https://github.com/thinca/vim-themis/pull/56
return 0
endif
endwhile
return 1
endfunction
function! WaitWhile(func, ...) abort
return call('WaitUntil', [{-> !a:func()}] + a:000)
endfunction
function! Git(dir, ...) abort
let cmd = join(['git', '-C', shellescape(a:dir)] + a:000, ' ')
let out = system(cmd)
if v:shell_error
throw '`' . cmd . '` failed: ' . out
endif
return out
endfunction
let g:ON_TRAVIS = $TRAVIS !=# ''

View File

@ -0,0 +1,21 @@
def run(target)
target = target.to_s
success = if target.endwith? 'nvim.vimspec'
system({ 'THEMIS_VIM' => 'nvim' }, '../vim-themis/bin/themis', target)
else
system('../vim-themis/bin/themis', target)
end
puts(success ? 'OK' : 'NG')
end
guard :shell do
watch /^.+\.vim$/ do |m|
puts "Changed #{m[0]}. Run all tests."
run './test/'
end
watch /^.+\.vimspec$/ do |m|
puts "Changed #{m[0]}"
run m[0]
end
end

View File

@ -0,0 +1,49 @@
### How to Run tests
Setup:
```
$ cd /path/to/git-messenger.vim
$ git clone https://github.com/thinca/vim-themis.git
```
Run tests on `nvim`:
```
$ THEMIS_VIM=nvim ./vim-themis/bin/themis test/all.vimspec
```
Run tests on `vim`:
```
$ ./vim-themis/bin/themis test/all.vimspec
```
### How to run guard
Install [guard][] and [guard-shell][] as prerequisites.
```
$ guard -G test/Guardfile
```
It watches your file changes and runs tests automatically.
### How to take coverage
Set `$THEMIS_PROFILE` to take profiler log.
```
$ THEMIS_PROFILE=profile.txt ./vim-themis/bin/themis test/all.vimspec
```
It generates `profile.txt`. And run [covimerage][] to make a coverage file for `coverage` command.
```
$ covimerage write_coverage profile.txt
$ coverage report
```
[guard]: https://github.com/guard/guard
[guard-shell]: https://github.com/guard/guard-shell
[covimerage]: https://github.com/Vimjas/covimerage

File diff suppressed because it is too large Load Diff