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