1
0
mirror of https://github.com/SpaceVim/SpaceVim.git synced 2025-04-13 22:59:15 +08:00

fix(gina): use bundle gina & fix conceal setting

close https://github.com/SpaceVim/SpaceVim/issues/4660
This commit is contained in:
wsdjeg 2022-05-19 09:10:49 +08:00
parent 62a15d02a4
commit 1ff70c5873
214 changed files with 30980 additions and 1 deletions

View File

@ -62,7 +62,7 @@ function! SpaceVim#layers#git#plugins() abort
\ ]
call add(plugins, ['airblade/vim-gitgutter', { 'merged' : 0}])
if s:git_plugin ==# 'gina'
call add(plugins, ['lambdalisue/gina.vim', { 'on_cmd' : 'Gina'}])
call add(plugins, [g:_spacevim_root_dir . 'bundle/gina.vim', { 'merged' : 0}])
elseif s:git_plugin ==# 'fugitive'
call add(plugins, ['tpope/vim-fugitive', { 'merged' : 0}])
call add(plugins, ['tpope/vim-dispatch', { 'merged' : 0}])
@ -89,6 +89,11 @@ function! SpaceVim#layers#git#config() abort
call SpaceVim#mapping#space#def('nnoremap', ['g', 'b'], 'Gina blame', 'view-git-blame', 1)
call SpaceVim#mapping#space#def('nnoremap', ['g', 'V'], 'Gina log %', 'git-log-of-current-file', 1)
call SpaceVim#mapping#space#def('nnoremap', ['g', 'v'], 'Gina log', 'git-log-of-current-repo', 1)
augroup spacevim_git_layer_gina
autocmd!
autocmd FileType gina-log setlocal concealcursor=nvic
autocmd FileType gina-log setlocal conceallevel=3
augroup END
elseif s:git_plugin ==# 'fugitive'
call SpaceVim#mapping#space#def('nnoremap', ['g', 's'], 'Git', 'git-status', 1)
call SpaceVim#mapping#space#def('nnoremap', ['g', 'S'], 'Git add %', 'stage-current-file', 1)

View File

@ -0,0 +1,52 @@
name: neovim
on:
push:
branches:
- master
pull_request:
branches:
- master
jobs:
build:
strategy:
fail-fast: false
matrix:
os:
- macos-latest
- windows-latest
- ubuntu-latest
version:
- nightly
- stable
- v0.4.3
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@v2
- name: Install Neovim
uses: rhysd/action-setup-vim@v1
id: vim
with:
neovim: true
version: ${{ matrix.version }}
- name: Download test runner
uses: actions/checkout@v2
with:
repository: thinca/vim-themis
path: vim-themis
- name: Prepare environment
run: |
git config --global user.name "github-action"
git config --global user.email "github-action@example.com"
- name: Run tests
env:
CI: 1
THEMIS_VIM: ${{ steps.vim.outputs.executable }}
# XXX:
# Overwrite %TMP% to point a correct temp directory.
# Note that %TMP% only affects value of 'tempname()' in Windows.
# https://github.community/t5/GitHub-Actions/TEMP-is-broken-on-Windows/m-p/30432#M427
TMP: 'C:\Users\runneradmin\AppData\Local\Temp'
run: |
./vim-themis/bin/themis

View File

@ -0,0 +1,22 @@
name: reviewdog
on:
push:
branches:
- master
pull_request:
branches:
- master
jobs:
vimlint:
name: runner / vint
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: vint
uses: reviewdog/action-vint@v1
with:
github_token: ${{ secrets.github_token }}
level: error
reporter: github-pr-review

View File

@ -0,0 +1,52 @@
name: vim
on:
push:
branches:
- master
pull_request:
branches:
- master
jobs:
build:
strategy:
fail-fast: false
matrix:
os:
- macos-latest
- windows-latest
- ubuntu-latest
version:
- nightly
- v8.2.0235
- v8.1.2424
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@v2
- name: Install Vim
uses: rhysd/action-setup-vim@v1
id: vim
with:
neovim: false
version: ${{ matrix.version }}
- name: Download test runner
uses: actions/checkout@v2
with:
repository: thinca/vim-themis
path: vim-themis
- name: Prepare environment
run: |
git config --global user.name "github-action"
git config --global user.email "github-action@example.com"
- name: Run tests
env:
CI: 1
THEMIS_VIM: ${{ steps.vim.outputs.executable }}
# XXX:
# Overwrite %TMP% to point a correct temp directory.
# Note that %TMP% only affects value of 'tempname()' in Windows.
# https://github.community/t5/GitHub-Actions/TEMP-is-broken-on-Windows/m-p/30432#M427
TMP: 'C:\Users\runneradmin\AppData\Local\Temp'
run: |
./vim-themis/bin/themis

5
bundle/gina.vim/.gitignore vendored Normal file
View File

@ -0,0 +1,5 @@
doc/tags
release
coverage.xml
.coverage.covimerage
.profile

20
bundle/gina.vim/LICENSE Normal file
View File

@ -0,0 +1,20 @@
Copyright 2016 Alisue, hashnote.net
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.

95
bundle/gina.vim/README.md Normal file
View File

@ -0,0 +1,95 @@
# 👣 gina.vim
![Version 1.0](https://img.shields.io/badge/version-1.0-yellow.svg)
![Support Vim 8.1 or above](https://img.shields.io/badge/support-Vim%208.1%20or%20above-yellowgreen.svg)
![Support Neovim 0.4 or above](https://img.shields.io/badge/support-Neovim%200.4%20or%20above-yellowgreen.svg)
![Support Git 2.25 or above](https://img.shields.io/badge/support-Git%202.25%20or%20above-green.svg)
[![Powered by vital.vim](https://img.shields.io/badge/powered%20by-vital.vim-80273f.svg)](https://github.com/vim-jp/vital.vim)
[![MIT License](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE)
[![Doc](https://img.shields.io/badge/doc-%3Ah%20gina-orange.svg)](doc/gina.txt)
[![Doc (dev)](https://img.shields.io/badge/doc-%3Ah%20gina--develop-orange.svg)](doc/gina-develop.txt)
[![reviewdog](https://github.com/lambdalisue/gina.vim/workflows/reviewdog/badge.svg)](https://github.com/lambdalisue/gina.vim/actions?query=workflow%3Areviewdog)
[![vim](https://github.com/lambdalisue/gina.vim/workflows/vim/badge.svg)](https://github.com/lambdalisue/gina.vim/actions?query=workflow%3Avim)
[![neovim](https://github.com/lambdalisue/gina.vim/workflows/neovim/badge.svg)](https://github.com/lambdalisue/gina.vim/actions?query=workflow%3Aneovim)
gina.vim (gina) is a plugin to asynchronously control git repositories.
## Presentation
[![You've been Super Viman. After this talk, you could say you are Super Viman 2 -- Life with gina.vim](https://img.youtube.com/vi/zkANQ9l7YDM/0.jpg)](https://www.youtube.com/watch?v=zkANQ9l7YDM)
I've talked about what the gina.vim is in [VimConf2017](http://vimconf.vim-jp.org/2017/) ([Slide](https://lambdalisue.github.io/vimconf2017/assets/player/KeynoteDHTMLPlayer.html)). Check it out if you would like to feel what the gina.vim is.
## Usage
The following is a schematic image of general working-flow with gina.
```
┌─────┬──────────┐
│ │ │
#DIRTY# │ ▼
▲ │ :Gina status │ << : stage
│ │ │ │ >> : unstage
│ │ │ │ -- : toggle
:write │ #STAGED# │ == : discard
▲ │ │ │ pp : patch
│ ├──────────┤ │ dd : diff
│ │ ▼
#CLEAN# │ :Gina commit │ ! : switch --amend
│ │ │ │ :w : save cache
│ ▼ │ │ :q : commit changes (confirm)
└────────────────┘ │ :wq : commit changes (immediate)
```
So basically user would
1. Edit contents in a git repository
2. Stage changes with `:Gina status`
3. Commit changes with `:Gina commit`
See `:h gina-usage` for advance usage. Gina provides a lot more features.
## Pros.
- A git detection is fast and accurate
- It does not require `git` process so incredibly fast
- Used in [lambdalisue/vim-gita][], for several years
- Commands are asynchronously performed
- Users don't have to wait `:Gina push` (`git push`)
- Asynchronous feature in Neovim is great. `:Gina log` (`git log`) on **Linux** repository won't freeze Neovim
- Single command. Users do not need to remember tons of commands
- `:Gina {command}` will execute a gina command or a git raw command asynchronously
- `:Gina! {command}` will execute a git raw command asynchronously
- `:Gina!! {command}` will execute a git raw command in a shell (mainly for `:Gina!! add -p` or `:Gina!! rebase -i`)
- Action based. Users do not need to remember tons of mappings
- `?` to see the help
- `a` to select an action to perform (complete with `<Tab>`)
- `.` to repeat previous action
- All action can map to an actual keymap
- Author tried to follow Vim's flavor
- No mapping for `ee` or whatever which conflicts with Vim's native mappings (like vim-gita does...)
- Customizable
- Users can define action aliases and mappings
- Users can define default options and aliases of command
- More
- Tested on all major platforms
- Powered by [vim-jp/vital.vim][], mean that the things are unit tested
- Gina add some behaviour test as well
[lambdalisue/vim-gita]: https://github.com/lambdalisue/vim-gita
[vim-jp/vital.vim]: https://github.com/vim-jp/vital.vim
## Contribution
Any contribution including documentations are welcome.
Contributers should install [thinca/vim-themis][] to run tests before sending a PR if they applied some modification to the code.
PRs which does not pass tests won't be accepted.
[thinca/vim-themis]: https://github.com/thinca/vim-themis
## License
The code in gina.vim follows MIT license texted in [LICENSE](./LICENSE).
Contributors need to agree that any modifications sent in this repository follow the license.

View File

@ -0,0 +1,27 @@
let s:Config = vital#gina#import('Config')
let s:Path = vital#gina#import('System.Filepath')
function! gina#config(sfile, options) abort
return s:Config.define(s:translate(a:sfile), a:options)
endfunction
" Private --------------------------------------------------------------------
function! s:translate(sfile) abort
let path = s:Path.unixpath(a:sfile)
let name = matchstr(path, 'autoload/\zs\%(gina\.vim\|gina/.*\)$')
let name = substitute(name, '\.vim$', '', '')
let name = substitute(name, '/', '#', 'g')
let name = substitute(name, '\%(^#\|#$\)', '', 'g')
return 'g:' . name
endfunction
" Default variable -----------------------------------------------------------
call gina#config(expand('<sfile>'), {
\ 'test': 0,
\ 'debug': -1,
\ 'develop': 1,
\ 'complete_threshold': 30,
\})

View File

@ -0,0 +1,116 @@
scriptencoding utf-8
let s:Action = vital#gina#import('Action')
let s:Action.name = 'gina'
let s:Highlight = vital#gina#import('Vim.Highlight')
function! gina#action#get(...) abort
return call(s:Action.get, a:000, s:Action)
endfunction
function! gina#action#attach(...) abort
return call(s:Action.attach, a:000, s:Action)
endfunction
function! gina#action#include(scheme) abort
let binder = s:Action.get()
if binder is# v:null
" TODO: raise an exception
return
endif
let scheme = substitute(a:scheme, '-', '_', 'g')
try
return call(
\ printf('gina#action#%s#define', scheme),
\ [binder]
\)
catch /^Vim\%((\a\+)\)\=:E117: [^:]\+: gina#action#[^#]\+#define/
call gina#core#console#debug(v:exception)
call gina#core#console#debug(v:throwpoint)
endtry
throw gina#core#revelator#error(printf(
\ 'No action script "gina/action/%s.vim" is found',
\ a:scheme,
\))
endfunction
function! gina#action#alias(...) abort
let binder = s:Action.get()
if binder is# v:null
" TODO: raise an exception
return
endif
return gina#core#revelator#call(binder.alias, a:000, binder)
endfunction
function! gina#action#shorten(action_scheme, ...) abort
let excludes = get(a:000, 0, [])
let binder = s:Action.get()
if binder is# v:null
" TODO: raise an exception
return
endif
let action_scheme = substitute(a:action_scheme, '-', '_', 'g')
let names = filter(
\ keys(binder.actions),
\ 'v:val =~# ''^'' . action_scheme . '':'''
\)
for name in filter(names, 'index(excludes, v:val) == -1')
call binder.alias(matchstr(name, '^' . action_scheme . ':\zs.*'), name)
endfor
endfunction
function! gina#action#call(...) abort
let binder = s:Action.get()
if binder is# v:null
" TODO: raise an exception
return
endif
return gina#core#revelator#call(binder.call, a:000, binder)
endfunction
function! gina#action#candidates(...) abort
let binder = s:Action.get()
if binder is# v:null
" TODO: raise an exception
return
endif
return gina#core#revelator#call(binder._get_candidates, a:000, binder)
endfunction
" Config ---------------------------------------------------------------------
call gina#config(expand('<sfile>'), {
\ 'mark_sign_text': '|',
\})
" Highlight ------------------------------------------------------------------
function! s:define_highlihghts() abort
silent let bg = s:Highlight.get('SignColumn')
silent let fg = s:Highlight.get('Title')
call s:Highlight.set({
\ 'name': 'GinaActionMarkSelected',
\ 'attrs': {
\ 'cterm': 'bold',
\ 'ctermfg': get(fg.attrs, 'ctermfg', '1'),
\ 'ctermbg': get(bg.attrs, 'ctermbg', 'NONE'),
\ 'gui': 'bold',
\ 'guifg': get(fg.attrs, 'guifg', '#ff0000'),
\ 'guibg': get(bg.attrs, 'guibg', 'NONE'),
\ }
\}, {
\ 'default': 1,
\})
highlight link VitalActionMarkSelectedLine Search
highlight link VitalActionMarkSelected GinaActionMarkSelected
let s:Action.mark_sign_text = g:gina#action#mark_sign_text
endfunction
augroup gina_action_internal
autocmd! *
autocmd ColorScheme * call s:define_highlihghts()
augroup END
call s:define_highlihghts()

View File

@ -0,0 +1,154 @@
function! gina#action#blame#define(binder) abort
if gina#core#buffer#param('%', 'scheme') ==# 'blame'
call a:binder.define('blame:echo', function('s:on_echo'), {
\ 'description': 'Echo a chunk info',
\ 'mapping_mode': 'n',
\ 'requirements': [
\ 'summary',
\ 'author',
\ 'author_time',
\ 'author_tz',
\ 'revision',
\ ],
\ 'options': {},
\ 'use_marks': 0,
\ 'clear_marks': 0,
\})
call a:binder.define('blame:open', function('s:on_open'), {
\ 'description': 'Blame a content on a commit of a chunk',
\ 'mapping_mode': 'n',
\ 'requirements': ['rev', 'path'],
\ 'options': {},
\ 'use_marks': 0,
\ 'clear_marks': 0,
\})
call a:binder.define('blame:back', function('s:on_back'), {
\ 'description': 'Back to a navigational previous blame',
\ 'mapping_mode': 'n',
\ 'requirements': [],
\ 'options': {},
\ 'use_marks': 0,
\ 'clear_marks': 0,
\})
endif
endfunction
" Private --------------------------------------------------------------------
function! s:on_echo(candidates, options) abort dict
if empty(a:candidates)
return
endif
let chunk = a:candidates[0]
let timestamper = gina#core#timestamper#new({
\ 'months': g:gina#action#blame#timestamp_months,
\ 'format1': g:gina#action#blame#timestamp_format1,
\ 'format2': g:gina#action#blame#timestamp_format2,
\})
let timestamp = timestamper.format(
\ chunk.author_time,
\ chunk.author_tz,
\)
redraw | echo printf(
\ '%s: %s authored on %s [%s]',
\ chunk.summary,
\ chunk.author,
\ timestamp,
\ chunk.revision,
\)
endfunction
function! s:on_open(candidates, options) abort dict
if empty(a:candidates)
return
endif
let options = extend({
\ 'opener': '',
\}, a:options)
let args = gina#core#meta#get_or_fail('args')
let chunk = a:candidates[0]
if !empty(args.params.rev) && chunk.rev =~# '^' . args.params.rev
if !has_key(chunk, 'previous')
throw gina#core#revelator#info(printf(
\ 'No related parent commit exists and "%s" is already shown',
\ chunk.rev,
\))
endif
if !gina#core#console#confirm(printf(
\ 'A related parent commit "%s" exist. Do you want to move on?',
\ chunk.previous,
\), 'y')
throw gina#core#revelator#info('Cancel')
endif
let rev = matchstr(chunk.previous, '^\S\+')
let path = matchstr(chunk.previous, '^\S\+\s\zs.*')
let line = gina#core#tracker#track(
\ gina#core#get_or_fail(),
\ chunk.path,
\ line('.'),
\ {
\ 'lhs': chunk.rev,
\ 'rhs': rev,
\ }
\)
else
let rev = chunk.rev
let path = chunk.path
let line = gina#util#get(chunk, 'line')
endif
call s:add_history()
let treeish = gina#core#treeish#build(rev, path)
execute printf(
\ '%s Gina blame %s %s %s',
\ options.mods,
\ gina#util#shellescape(options.opener, '--opener='),
\ gina#util#shellescape(line, '--line='),
\ gina#util#shellescape(treeish),
\)
endfunction
function! s:on_back(candidates, options) abort dict
let options = extend({
\ 'opener': '',
\}, a:options)
let history = s:pop_history()
let treeish = gina#core#treeish#build(history.rev, history.path)
execute printf(
\ '%s Gina blame %s %s %s',
\ options.mods,
\ gina#util#shellescape(options.opener, '--opener='),
\ gina#util#shellescape(history.line, '--line='),
\ gina#util#shellescape(treeish),
\)
endfunction
" History --------------------------------------------------------------------
function! s:add_history() abort
if gina#core#buffer#param('%', 'scheme') !=# 'blame'
return
endif
let w:gina_blame_history = get(w:, 'gina_blame_history', [])
let args = gina#core#meta#get_or_fail('args')
call add(w:gina_blame_history, {
\ 'rev': empty(args.params.rev) ? ':0' : args.params.rev,
\ 'path': args.params.path,
\ 'line': line('.'),
\})
endfunction
function! s:pop_history() abort
let w:gina_blame_history = get(w:, 'gina_blame_history', [])
if empty(w:gina_blame_history)
throw gina#core#revelator#info('No navigational history is found')
endif
return remove(w:gina_blame_history, -1)
endfunction
" Config ---------------------------------------------------------------------
call gina#config(expand('<sfile>'), {
\ 'timestamp_months': 3,
\ 'timestamp_format1': '%d %b',
\ 'timestamp_format2': '%d %b, %Y',
\})

View File

@ -0,0 +1,175 @@
function! gina#action#branch#define(binder) abort
call a:binder.define('branch:refresh', function('s:on_refresh'), {
\ 'description': 'Refresh remote branches',
\ 'mapping_mode': 'n',
\ 'requirements': [],
\ 'options': {},
\ 'use_marks': 0,
\ 'clear_marks': 0,
\})
call a:binder.define('branch:new', function('s:on_new'), {
\ 'description': 'Create a new branch',
\ 'mapping_mode': 'n',
\ 'requirements': ['branch', 'remote'],
\ 'options': {},
\})
call a:binder.define('branch:move', function('s:on_move'), {
\ 'description': 'Rename a branch',
\ 'mapping_mode': 'nv',
\ 'requirements': ['branch', 'remote'],
\ 'options': {},
\})
call a:binder.define('branch:move:force', function('s:on_move'), {
\ 'hidden': 1,
\ 'description': 'Rename a branch',
\ 'mapping_mode': 'nv',
\ 'requirements': ['branch', 'remote'],
\ 'options': { 'force': 1 },
\})
call a:binder.define('branch:delete', function('s:on_delete'), {
\ 'description': 'Delete a branch',
\ 'mapping_mode': 'nv',
\ 'requirements': ['branch', 'remote'],
\ 'options': {},
\})
call a:binder.define('branch:delete:force', function('s:on_delete'), {
\ 'hidden': 1,
\ 'description': 'Delete a branch',
\ 'mapping_mode': 'nv',
\ 'requirements': ['branch', 'remote'],
\ 'options': { 'force': 1 },
\})
call a:binder.define('branch:set-upstream-to', function('s:on_set_upstream_to'), {
\ 'description': 'Set an upstream of a branch',
\ 'mapping_mode': 'nv',
\ 'requirements': ['branch', 'remote'],
\ 'options': {},
\})
call a:binder.define('branch:unset-upstream', function('s:on_unset_upstream'), {
\ 'description': 'Unset an upstream of a branch',
\ 'mapping_mode': 'nv',
\ 'requirements': ['branch', 'remote'],
\ 'options': {},
\})
endfunction
" Private --------------------------------------------------------------------
function! s:on_refresh(candidates, options) abort
let options = extend({}, a:options)
execute printf(
\ '%s Gina remote update --prune',
\ options.mods,
\)
endfunction
function! s:on_new(candidates, options) abort
if empty(a:candidates)
return
endif
let options = extend({}, a:options)
for candidate in a:candidates
let name = gina#core#console#ask_or_cancel(
\ 'Name: ', '',
\)
let from = gina#core#console#ask_or_cancel(
\ 'From: ', candidate.rev,
\ function('gina#complete#commit#branch')
\)
execute printf(
\ '%s Gina checkout -b %s %s',
\ options.mods,
\ gina#util#shellescape(name),
\ gina#util#shellescape(from),
\)
endfor
endfunction
function! s:on_move(candidates, options) abort
let candidates = filter(copy(a:candidates), 'empty(v:val.remote)')
if empty(candidates)
return
endif
let options = extend({
\ 'force': 0,
\}, a:options)
for candidate in candidates
let name = gina#core#console#ask_or_cancel(
\ 'Rename: ',
\ candidate.branch,
\)
execute printf(
\ '%s Gina branch --move %s %s %s',
\ options.mods,
\ options.force ? '--force' : '',
\ gina#util#shellescape(candidate.branch),
\ gina#util#shellescape(name),
\)
endfor
endfunction
function! s:on_delete(candidates, options) abort
if empty(a:candidates)
return
endif
let options = extend({
\ 'force': 0,
\}, a:options)
for candidate in a:candidates
let is_remote = !empty(candidate.remote)
if is_remote
execute printf(
\ '%s Gina push --delete %s %s %s',
\ options.mods,
\ options.force ? '--force' : '',
\ gina#util#shellescape(candidate.remote),
\ gina#util#shellescape(candidate.branch),
\)
else
execute printf(
\ '%s Gina branch --delete %s %s',
\ options.mods,
\ options.force ? '--force' : '',
\ gina#util#shellescape(candidate.branch),
\)
endif
endfor
endfunction
function! s:on_set_upstream_to(candidates, options) abort
let candidates = filter(copy(a:candidates), 'empty(v:val.remote)')
if empty(candidates)
return
endif
let options = extend({}, a:options)
for candidate in candidates
let upstream = gina#core#console#ask_or_cancel(
\ 'Upstream: ', candidate.branch,
\ function('gina#complete#commit#remote_branch'),
\)
let upstream = substitute(
\ upstream, printf('^%s/', candidate.remote), '', ''
\)
execute printf(
\ '%s Gina branch --set-upstream-to=%s %s',
\ options.mods,
\ gina#util#shellescape(upstream),
\ gina#util#shellescape(candidate.branch),
\)
endfor
endfunction
function! s:on_unset_upstream(candidates, options) abort
let candidates = filter(copy(a:candidates), 'empty(v:val.remote)')
if empty(candidates)
return
endif
let options = extend({}, a:options)
for candidate in candidates
execute printf(
\ '%s Gina branch --unset-upstream %s',
\ options.mods,
\ gina#util#shellescape(candidate.branch),
\)
endfor
endfunction

View File

@ -0,0 +1,60 @@
function! gina#action#browse#define(binder) abort
call a:binder.define('browse', function('s:on_browse'), {
\ 'description': 'Open a remote url in a system browser',
\ 'mapping_mode': 'n',
\ 'requirements': [],
\ 'options': {},
\ 'use_marks': 0,
\ 'clear_marks': 0,
\})
call a:binder.define('browse:exact', function('s:on_browse'), {
\ 'hidden': 1,
\ 'description': 'Open a remote url in a system browser',
\ 'mapping_mode': 'n',
\ 'requirements': [],
\ 'options': { 'exact': 1 },
\ 'use_marks': 0,
\ 'clear_marks': 0,
\})
call a:binder.define('browse:yank', function('s:on_browse'), {
\ 'description': 'Copy a remote url',
\ 'mapping_mode': 'n',
\ 'requirements': [],
\ 'options': { 'yank': 1 },
\ 'use_marks': 0,
\ 'clear_marks': 0,
\})
call a:binder.define('browse:yank:exact', function('s:on_browse'), {
\ 'hidden': 1,
\ 'description': 'Copy a remote url',
\ 'mapping_mode': 'n',
\ 'requirements': [],
\ 'options': { 'yank': 1, 'exact': 1 },
\ 'use_marks': 0,
\ 'clear_marks': 0,
\})
endfunction
" Private --------------------------------------------------------------------
function! s:on_browse(candidates, options) abort
if empty(a:candidates)
return
endif
let options = extend({
\ 'exact': 0,
\ 'yank': 0,
\}, a:options)
let candidate = a:candidates[0]
let treeish = gina#core#treeish#build(
\ gina#util#get(candidate, 'rev'),
\ gina#util#get(candidate, 'path', v:null),
\)
execute printf(
\ '%s Gina browse %s %s %s',
\ options.mods,
\ options.exact ? '--exact' : '',
\ options.yank ? '--yank' : '',
\ gina#util#shellescape(treeish),
\)
endfunction

View File

@ -0,0 +1,110 @@
function! gina#action#changes#define(binder) abort
" changes:of
let params = {
\ 'description': 'Show changes of a commit',
\ 'mapping_mode': 'nv',
\ 'requirements': ['rev'],
\}
call a:binder.define('changes:of', function('s:on_changes'), extend({
\ 'options': {},
\}, params))
call a:binder.define('changes:of:split', function('s:on_changes'), extend({
\ 'options': {'opener': 'new'},
\}, params))
call a:binder.define('changes:of:vsplit', function('s:on_changes'), extend({
\ 'options': {'opener': 'vnew'},
\}, params))
call a:binder.define('changes:of:tab', function('s:on_changes'), extend({
\ 'options': {'opener': 'tabedit'},
\}, params))
call a:binder.define('changes:of:preview', function('s:on_changes'), extend({
\ 'options': {'opener': 'pedit'},
\}, params))
" changes:between
let params = {
\ 'description': 'Show changes between a commit and a HEAD',
\ 'mapping_mode': 'nv',
\ 'requirements': ['rev'],
\}
call a:binder.define('changes:between', function('s:on_changes'), extend({
\ 'options': { 'format': '%s..' },
\}, params))
call a:binder.define('changes:between:split', function('s:on_changes'), extend({
\ 'options': { 'format': '%s..', 'opener': 'new' },
\}, params))
call a:binder.define('changes:between:vsplit', function('s:on_changes'), extend({
\ 'options': { 'format': '%s..', 'opener': 'vnew' },
\}, params))
call a:binder.define('changes:between:tab', function('s:on_changes'), extend({
\ 'options': { 'format': '%s..', 'opener': 'tabedit' },
\}, params))
call a:binder.define('changes:between:preview', function('s:on_changes'), extend({
\ 'options': { 'format': '%s..', 'opener': 'pedit' },
\}, params))
" changes:from
let params = {
\ 'description': 'Show changes from a common ancestor of a commit and a HEAD',
\ 'mapping_mode': 'nv',
\ 'requirements': ['rev'],
\}
call a:binder.define('changes:from', function('s:on_changes'), extend({
\ 'options': { 'format': '%s...' },
\}, params))
call a:binder.define('changes:from:split', function('s:on_changes'), extend({
\ 'options': { 'format': '%s...', 'opener': 'new' },
\}, params))
call a:binder.define('changes:from:vsplit', function('s:on_changes'), extend({
\ 'options': { 'format': '%s...', 'opener': 'vnew' },
\}, params))
call a:binder.define('changes:from:tab', function('s:on_changes'), extend({
\ 'options': { 'format': '%s...', 'opener': 'tabedit' },
\}, params))
call a:binder.define('changes:from:preview', function('s:on_changes'), extend({
\ 'options': { 'format': '%s...', 'opener': 'pedit' },
\}, params))
call a:binder.alias('changes:of:above', 'leftabove changes:of:split')
call a:binder.alias('changes:of:below', 'belowright changes:of:split')
call a:binder.alias('changes:of:left', 'leftabove changes:of:vsplit')
call a:binder.alias('changes:of:right', 'belowright changes:of:vsplit')
call a:binder.alias('changes:of:top', 'topleft changes:of:split')
call a:binder.alias('changes:of:bottom', 'botright changes:of:split')
call a:binder.alias('changes:of:leftest', 'topleft changes:of:vsplit')
call a:binder.alias('changes:of:rightest', 'botright changes:of:vsplit')
call a:binder.alias('changes:between:above', 'leftabove changes:between:split')
call a:binder.alias('changes:between:below', 'belowright changes:between:split')
call a:binder.alias('changes:between:left', 'leftabove changes:between:vsplit')
call a:binder.alias('changes:between:right', 'belowright changes:between:vsplit')
call a:binder.alias('changes:between:top', 'topleft changes:between:split')
call a:binder.alias('changes:between:bottom', 'botright changes:between:split')
call a:binder.alias('changes:between:leftest', 'topleft changes:between:vsplit')
call a:binder.alias('changes:between:rightest', 'botright changes:between:vsplit')
call a:binder.alias('changes:from:above', 'leftabove changes:from:split')
call a:binder.alias('changes:from:below', 'belowright changes:from:split')
call a:binder.alias('changes:from:left', 'leftabove changes:from:vsplit')
call a:binder.alias('changes:from:right', 'belowright changes:from:vsplit')
call a:binder.alias('changes:from:top', 'topleft changes:from:split')
call a:binder.alias('changes:from:bottom', 'botright changes:from:split')
call a:binder.alias('changes:from:leftest', 'topleft changes:from:vsplit')
call a:binder.alias('changes:from:rightest', 'botright changes:from:vsplit')
endfunction
" Private --------------------------------------------------------------------
function! s:on_changes(candidates, options) abort
if empty(a:candidates)
return
endif
let options = extend({
\ 'opener': '',
\ 'format': '%s',
\}, a:options)
for candidate in a:candidates
execute printf(
\ '%s Gina changes %s %s -- %s',
\ options.mods,
\ gina#util#shellescape(options.opener, '--opener='),
\ gina#util#shellescape(printf(options.format, candidate.rev)),
\ gina#util#shellescape(gina#util#get(candidate, 'residual')),
\)
endfor
endfunction

View File

@ -0,0 +1,53 @@
function! gina#action#chaperon#define(binder) abort
let params = {
\ 'description': 'Open three buffers to solve conflict',
\ 'mapping_mode': 'nv',
\ 'requirements': ['path', 'sign'],
\}
call a:binder.define('chaperon', function('s:on_chaperon'), extend({
\ 'options': {},
\}, params))
call a:binder.define('chaperon:split', function('s:on_chaperon'), extend({
\ 'hidden': 1,
\ 'options': {'opener': 'new'},
\}, params))
call a:binder.define('chaperon:vsplit', function('s:on_chaperon'), extend({
\ 'hidden': 1,
\ 'options': {'opener': 'vnew'},
\}, params))
call a:binder.define('chaperon:tab', function('s:on_chaperon'), extend({
\ 'hidden': 1,
\ 'options': {'opener': 'tabedit'},
\}, params))
" Alias
call a:binder.alias('chaperon:above', 'leftabove chaperon:split')
call a:binder.alias('chaperon:below', 'belowright chaperon:split')
call a:binder.alias('chaperon:left', 'leftabove chaperon:vsplit')
call a:binder.alias('chaperon:right', 'belowright chaperon:vsplit')
call a:binder.alias('chaperon:top', 'topleft chaperon:split')
call a:binder.alias('chaperon:bottom', 'botright chaperon:split')
call a:binder.alias('chaperon:leftest', 'topleft chaperon:vsplit')
call a:binder.alias('chaperon:rightest', 'botright chaperon:vsplit')
endfunction
" Private --------------------------------------------------------------------
function! s:on_chaperon(candidates, options) abort
let candidates = filter(copy(a:candidates), 'v:val.sign ==# ''UU''')
if empty(candidates)
return
endif
let options = extend({
\ 'opener': '',
\}, a:options)
for candidate in candidates
execute printf(
\ '%s Gina chaperon %s %s %s %s',
\ options.mods,
\ gina#util#shellescape(options.opener, '--opener='),
\ gina#util#shellescape(get(candidate, 'line'), '--line='),
\ gina#util#shellescape(get(candidate, 'col'), '--col='),
\ gina#util#shellescape(candidate.path),
\)
endfor
endfunction

View File

@ -0,0 +1,331 @@
function! gina#action#commit#define(binder) abort
" checkout
call a:binder.define('commit:checkout', function('s:on_checkout'), {
\ 'description': 'Checkout a commit',
\ 'mapping_mode': 'nv',
\ 'requirements': ['rev'],
\ 'options': {},
\})
call a:binder.define('commit:checkout:track', function('s:on_checkout_track'), {
\ 'description': 'Checkout a commit with a tracking branch',
\ 'mapping_mode': 'nv',
\ 'requirements': ['rev'],
\ 'options': {},
\})
" reset
call a:binder.define('commit:reset', function('s:on_reset'), {
\ 'description': 'Reset a HEAD to a commit',
\ 'mapping_mode': 'nv',
\ 'requirements': ['rev'],
\ 'options': {},
\})
call a:binder.define('commit:reset:soft', function('s:on_reset'), {
\ 'hidden': 1,
\ 'description': 'Reset a HEAD to a commit',
\ 'mapping_mode': 'nv',
\ 'requirements': ['rev'],
\ 'options': {'mode': 'soft'},
\})
call a:binder.define('commit:reset:hard', function('s:on_reset'), {
\ 'hidden': 1,
\ 'description': 'Reset a HEAD to a commit',
\ 'mapping_mode': 'nv',
\ 'requirements': ['rev'],
\ 'options': {'mode': 'hard'},
\})
call a:binder.define('commit:reset:merge', function('s:on_reset'), {
\ 'hidden': 1,
\ 'description': 'Reset a HEAD to a commit',
\ 'mapping_mode': 'nv',
\ 'requirements': ['rev'],
\ 'options': {'mode': 'merge'},
\})
call a:binder.define('commit:reset:keep', function('s:on_reset'), {
\ 'hidden': 1,
\ 'description': 'Reset a current HEAD to a commit',
\ 'mapping_mode': 'nv',
\ 'requirements': ['rev'],
\ 'options': {'mode': 'keep'},
\})
" merge
call a:binder.define('commit:merge', function('s:on_merge'), {
\ 'description': 'Merge a commit into a HEAD',
\ 'mapping_mode': 'nv',
\ 'requirements': ['rev'],
\ 'options': {},
\})
call a:binder.define('commit:merge:ff-only', function('s:on_merge'), {
\ 'hidden': 1,
\ 'description': 'Merge a commit into a HEAD',
\ 'mapping_mode': 'nv',
\ 'requirements': ['rev'],
\ 'options': { 'ff-only': 1 },
\})
call a:binder.define('commit:merge:no-ff', function('s:on_merge'), {
\ 'hidden': 1,
\ 'description': 'Merge a commit into a HEAD',
\ 'mapping_mode': 'nv',
\ 'requirements': ['rev'],
\ 'options': { 'no-ff': 1 },
\})
call a:binder.define('commit:merge:squash', function('s:on_merge'), {
\ 'hidden': 1,
\ 'description': 'Merge a commit into a HEAD',
\ 'mapping_mode': 'nv',
\ 'requirements': ['rev'],
\ 'options': { 'squash': 1 },
\})
" rebase
call a:binder.define('commit:rebase', function('s:on_rebase'), {
\ 'description': 'Rebase a HEAD from a commit',
\ 'mapping_mode': 'nv',
\ 'requirements': ['rev'],
\ 'options': {},
\})
call a:binder.define('commit:rebase:merge', function('s:on_rebase'), {
\ 'hidden': 1,
\ 'description': 'Rebase a HEAD by merging a commit',
\ 'mapping_mode': 'nv',
\ 'requirements': ['rev'],
\ 'options': { 'merge': 1 },
\})
" revert
call a:binder.define('commit:revert', function('s:on_revert'), {
\ 'description': 'Revert a commit',
\ 'mapping_mode': 'nv',
\ 'requirements': ['rev'],
\ 'options': {},
\})
call a:binder.define('commit:revert:1', function('s:on_revert'), {
\ 'hidden': 1,
\ 'description': 'Revert a commit',
\ 'mapping_mode': 'nv',
\ 'requirements': ['rev'],
\ 'options': { 'mainline': '1' },
\})
call a:binder.define('commit:revert:2', function('s:on_revert'), {
\ 'hidden': 1,
\ 'description': 'Revert a commit',
\ 'mapping_mode': 'nv',
\ 'requirements': ['rev'],
\ 'options': { 'mainline': '2' },
\})
" cherry-pick
call a:binder.define('commit:cherry-pick', function('s:on_cherry_pick'), {
\ 'description': 'Apply changes of a commit',
\ 'mapping_mode': 'nv',
\ 'requirements': ['rev'],
\ 'options': {},
\})
call a:binder.define('commit:cherry-pick:1', function('s:on_cherry_pick'), {
\ 'hidden': 1,
\ 'description': 'Apply changes of a commit',
\ 'mapping_mode': 'nv',
\ 'requirements': ['rev'],
\ 'options': { 'mainline': '1' },
\})
call a:binder.define('commit:cherry-pick:2', function('s:on_cherry_pick'), {
\ 'hidden': 1,
\ 'description': 'Apply changes of a commit',
\ 'mapping_mode': 'nv',
\ 'requirements': ['rev'],
\ 'options': { 'mainline': '2' },
\})
call a:binder.define('commit:tag:lightweight', function('s:on_tag'), {
\ 'description': 'Create a lightweight tag',
\ 'mapping_mode': 'nv',
\ 'requirements': ['rev'],
\ 'options': {},
\})
call a:binder.define('commit:tag:annotate', function('s:on_tag'), {
\ 'description': 'Create an unsigned, annotated tag',
\ 'mapping_mode': 'nv',
\ 'requirements': ['rev'],
\ 'options': {'annotate': 1},
\})
call a:binder.define('commit:tag:sign', function('s:on_tag'), {
\ 'description': 'Create a GPG-signed tag',
\ 'mapping_mode': 'nv',
\ 'requirements': ['rev'],
\ 'options': {'sign': 1},
\})
call a:binder.define('commit:tag:lightweight:force', function('s:on_tag'), {
\ 'hidden': 1,
\ 'description': 'Create a lightweight tag',
\ 'mapping_mode': 'nv',
\ 'requirements': ['rev'],
\ 'options': {'force': 1},
\})
call a:binder.define('commit:tag:annotate:force', function('s:on_tag'), {
\ 'hidden': 1,
\ 'description': 'Create an unsigned, annotated tag',
\ 'mapping_mode': 'nv',
\ 'requirements': ['rev'],
\ 'options': {'annotate': 1, 'force': 1},
\})
call a:binder.define('commit:tag:sign:force', function('s:on_tag'), {
\ 'hidden': 1,
\ 'description': 'Create a GPG-signed tag',
\ 'mapping_mode': 'nv',
\ 'requirements': ['rev'],
\ 'options': {'sign': 1, 'force': 1},
\})
" Alias
call a:binder.alias('commit:tag', 'commit:tag:annotate')
endfunction
" Private --------------------------------------------------------------------
function! s:on_checkout(candidates, options) abort
if empty(a:candidates)
return
endif
let options = extend({}, a:options)
for candidate in a:candidates
execute printf(
\ '%s Gina checkout %s',
\ options.mods,
\ gina#util#shellescape(candidate.rev),
\)
endfor
endfunction
function! s:on_checkout_track(candidates, options) abort
if empty(a:candidates)
return
endif
let options = extend({}, a:options)
for candidate in a:candidates
if !has_key(candidate, 'branch')
let branch = gina#core#console#ask_or_cancel(
\ 'A tracking branch name: ',
\ printf('%s-tracking', candidate.rev),
\)
else
let branch = candidate.branch
endif
execute printf(
\ '%s Gina checkout -b %s %s',
\ options.mods,
\ gina#util#shellescape(branch),
\ gina#util#shellescape(candidate.rev),
\)
endfor
endfunction
function! s:on_reset(candidates, options) abort
if empty(a:candidates)
return
endif
let options = extend({
\ 'mode': '',
\}, a:options)
let mode = empty(options.mode)
\ ? ''
\ : '--' . options.mode
for candidate in a:candidates
execute printf(
\ '%s Gina reset %s %s',
\ options.mods,
\ mode,
\ gina#util#shellescape(candidate.rev),
\)
endfor
endfunction
function! s:on_merge(candidates, options) abort
if empty(a:candidates)
return
endif
let options = extend({
\ 'no-ff': 0,
\ 'ff-only': 0,
\ 'squash': 0,
\}, a:options)
for candidate in a:candidates
execute printf(
\ '%s Gina merge --no-edit %s %s %s -- %s',
\ options.mods,
\ options['no-ff'] ? '--no-ff' : '',
\ options['ff-only'] ? '--ff-only' : '',
\ options.squash ? '--squash' : '',
\ gina#util#shellescape(candidate.rev),
\)
endfor
endfunction
function! s:on_rebase(candidates, options) abort
if empty(a:candidates)
return
endif
let options = extend({
\ 'merge': 0,
\}, a:options)
for candidate in a:candidates
execute printf(
\ '%s Gina rebase %s -- %s',
\ options.mods,
\ options.merge ? '--merge' : '',
\ gina#util#shellescape(candidate.rev),
\)
endfor
endfunction
function! s:on_revert(candidates, options) abort
if empty(a:candidates)
return
endif
let options = extend({
\ 'mainline': '',
\}, a:options)
for candidate in a:candidates
execute printf(
\ '%s Gina revert %s %s',
\ options.mods,
\ empty(options.mainline) ? '' : printf('--mainline %s', options.mainline),
\ gina#util#shellescape(candidate.rev),
\)
endfor
endfunction
function! s:on_cherry_pick(candidates, options) abort
if empty(a:candidates)
return
endif
let options = extend({
\ 'mainline': '',
\}, a:options)
for candidate in a:candidates
execute printf(
\ '%s Gina cherry-pick %s %s',
\ options.mods,
\ empty(options.mainline) ? '' : printf('--mainline %s', options.mainline),
\ gina#util#shellescape(candidate.rev),
\)
endfor
endfunction
function! s:on_tag(candidates, options) abort
if empty(a:candidates)
return
endif
let options = extend({
\ 'annotate': 0,
\ 'sign': 0,
\ 'force': 0,
\}, a:options)
for candidate in a:candidates
let name = gina#core#console#ask_or_cancel(
\ 'Name: ', '',
\)
execute printf(
\ '%s Gina tag %s %s %s %s %s',
\ options.mods,
\ options.annotate ? '--annotate' : '',
\ options.sign ? '--sign' : '',
\ options.force ? '--force' : '',
\ gina#util#shellescape(name),
\ gina#util#shellescape(candidate.rev),
\)
endfor
endfunction

View File

@ -0,0 +1,58 @@
function! gina#action#compare#define(binder) abort
let params = {
\ 'description': 'Open two buffers to compare differences',
\ 'mapping_mode': 'nv',
\ 'requirements': ['path'],
\}
call a:binder.define('compare', function('s:on_compare'), extend({
\ 'options': {},
\}, params))
call a:binder.define('compare:split', function('s:on_compare'), extend({
\ 'hidden': 1,
\ 'options': {'opener': 'new'},
\}, params))
call a:binder.define('compare:vsplit', function('s:on_compare'), extend({
\ 'hidden': 1,
\ 'options': {'opener': 'vnew'},
\}, params))
call a:binder.define('compare:tab', function('s:on_compare'), extend({
\ 'hidden': 1,
\ 'options': {'opener': 'tabedit'},
\}, params))
" Alias
call a:binder.alias('compare:above', 'leftabove compare:split')
call a:binder.alias('compare:below', 'belowright compare:split')
call a:binder.alias('compare:left', 'leftabove compare:vsplit')
call a:binder.alias('compare:right', 'belowright compare:vsplit')
call a:binder.alias('compare:top', 'topleft compare:split')
call a:binder.alias('compare:bottom', 'botright compare:split')
call a:binder.alias('compare:leftest', 'topleft compare:vsplit')
call a:binder.alias('compare:rightest', 'botright compare:vsplit')
endfunction
" Private --------------------------------------------------------------------
function! s:on_compare(candidates, options) abort
if empty(a:candidates)
return
endif
let options = extend({
\ 'opener': '',
\}, a:options)
for candidate in a:candidates
let cached = gina#util#get(candidate, 'sign', '!!') !~# '^\%(??\|!!\|.\w\)$'
let treeish = gina#core#treeish#build(
\ gina#util#get(candidate, 'rev'),
\ candidate.path,
\)
execute printf(
\ '%s Gina compare %s %s %s %s %s',
\ options.mods,
\ cached ? '--cached' : '',
\ gina#util#shellescape(options.opener, '--opener='),
\ gina#util#shellescape(get(candidate, 'line'), '--line='),
\ gina#util#shellescape(get(candidate, 'col'), '--col='),
\ gina#util#shellescape(treeish),
\)
endfor
endfunction

View File

@ -0,0 +1,63 @@
function! gina#action#diff#define(binder) abort
let params = {
\ 'description': 'Open an unified-diff',
\ 'mapping_mode': 'nv',
\ 'requirements': [],
\}
call a:binder.define('diff', function('s:on_diff'), extend({
\ 'options': {},
\}, params))
call a:binder.define('diff:split', function('s:on_diff'), extend({
\ 'hidden': 1,
\ 'options': {'opener': 'new'},
\}, params))
call a:binder.define('diff:vsplit', function('s:on_diff'), extend({
\ 'hidden': 1,
\ 'options': {'opener': 'vnew'},
\}, params))
call a:binder.define('diff:tab', function('s:on_diff'), extend({
\ 'hidden': 1,
\ 'options': {'opener': 'tabedit'},
\}, params))
call a:binder.define('diff:preview', function('s:on_diff'), extend({
\ 'hidden': 1,
\ 'options': {'opener': 'pedit'},
\}, params))
" Alias
call a:binder.alias('diff:above', 'leftabove diff:split')
call a:binder.alias('diff:below', 'belowright diff:split')
call a:binder.alias('diff:left', 'leftabove diff:vsplit')
call a:binder.alias('diff:right', 'belowright diff:vsplit')
call a:binder.alias('diff:top', 'topleft diff:split')
call a:binder.alias('diff:bottom', 'botright diff:split')
call a:binder.alias('diff:leftest', 'topleft diff:vsplit')
call a:binder.alias('diff:rightest', 'botright diff:vsplit')
call a:binder.alias('diff:preview:top', 'topleft diff:preview')
call a:binder.alias('diff:preview:bottom', 'botright diff:preview')
endfunction
" Private --------------------------------------------------------------------
function! s:on_diff(candidates, options) abort
if empty(a:candidates)
return
endif
let options = extend({
\ 'opener': '',
\}, a:options)
for candidate in a:candidates
let cached = get(candidate, 'sign', '!!') !~# '^\%(??\|!!\|.\w\)$'
let treeish = gina#core#treeish#build(
\ gina#util#get(candidate, 'rev'),
\ gina#util#get(candidate, 'path', v:null),
\)
execute printf(
\ '%s Gina diff %s %s %s -- %s',
\ options.mods,
\ cached ? '--cached' : '',
\ gina#util#shellescape(options.opener, '--opener='),
\ gina#util#shellescape(treeish),
\ gina#util#shellescape(gina#util#get(candidate, 'residual')),
\)
endfor
endfunction

View File

@ -0,0 +1,58 @@
function! gina#action#edit#define(binder) abort
let params = {
\ 'description': 'Edit a content in a working tree',
\ 'mapping_mode': 'nv',
\ 'requirements': ['path'],
\}
call a:binder.define('edit', function('s:on_edit'), extend({
\ 'options': {},
\}, params))
call a:binder.define('edit:split', function('s:on_edit'), extend({
\ 'hidden': 1,
\ 'options': {'opener': 'new'},
\}, params))
call a:binder.define('edit:vsplit', function('s:on_edit'), extend({
\ 'hidden': 1,
\ 'options': {'opener': 'vnew'},
\}, params))
call a:binder.define('edit:tab', function('s:on_edit'), extend({
\ 'hidden': 1,
\ 'options': {'opener': 'tabedit'},
\}, params))
call a:binder.define('edit:preview', function('s:on_edit'), extend({
\ 'hidden': 1,
\ 'options': {'opener': 'pedit'},
\}, params))
" Alias
call a:binder.alias('edit:above', 'leftabove edit:split')
call a:binder.alias('edit:below', 'belowright edit:split')
call a:binder.alias('edit:left', 'leftabove edit:vsplit')
call a:binder.alias('edit:right', 'belowright edit:vsplit')
call a:binder.alias('edit:top', 'topleft edit:split')
call a:binder.alias('edit:bottom', 'botright edit:split')
call a:binder.alias('edit:leftest', 'topleft edit:vsplit')
call a:binder.alias('edit:rightest', 'botright edit:vsplit')
call a:binder.alias('edit:preview:top', 'topleft edit:preview')
call a:binder.alias('edit:preview:bottom', 'botright edit:preview')
endfunction
" Private --------------------------------------------------------------------
function! s:on_edit(candidates, options) abort
if empty(a:candidates)
return
endif
let options = extend({
\ 'opener': '',
\}, a:options)
for candidate in a:candidates
execute printf(
\ '%s Gina edit %s %s %s %s',
\ options.mods,
\ gina#util#shellescape(options.opener, '--opener='),
\ gina#util#shellescape(get(candidate, 'line'), '--line='),
\ gina#util#shellescape(get(candidate, 'col'), '--col='),
\ gina#util#shellescape(candidate.path),
\)
endfor
endfunction

View File

@ -0,0 +1,49 @@
let s:Path = vital#gina#import('System.Filepath')
let s:String = vital#gina#import('Data.String')
function! gina#action#export#define(binder) abort
call a:binder.define('export:quickfix', function('s:on_quickfix'), {
\ 'description': 'Create a new quickfix list with selected candidates',
\ 'mapping_mode': 'nv',
\ 'requirements': ['path', 'word'],
\ 'options': {},
\})
call a:binder.define('export:quickfix:add', function('s:on_quickfix'), {
\ 'hidden': 1,
\ 'description': 'Add selected candidates to an existing quickfix list',
\ 'mapping_mode': 'nv',
\ 'requirements': ['path', 'word'],
\ 'options': { 'action': 'a' },
\})
call a:binder.define('export:quickfix:replace', function('s:on_quickfix'), {
\ 'hidden': 1,
\ 'description': 'Replace an existing quickfix list with selected candidates',
\ 'mapping_mode': 'nv',
\ 'requirements': ['path', 'word'],
\ 'options': { 'action': 'r' },
\})
endfunction
" Private --------------------------------------------------------------------
function! s:on_quickfix(candidates, options) abort dict
let options = extend({
\ 'action': ' ',
\}, a:options)
let git = gina#core#get_or_fail()
call setqflist(
\ map(copy(a:candidates), 's:to_quickfix(git, v:val)'),
\ options.action,
\)
endfunction
function! s:to_quickfix(git, candidate) abort
let abspath = gina#core#repo#abspath(a:git, a:candidate.path)
return {
\ 'filename': s:Path.realpath(abspath),
\ 'lnum': get(a:candidate, 'line', 1),
\ 'col': get(a:candidate, 'col', 1),
\ 'text': s:String.remove_ansi_sequences(a:candidate.word),
\}
endfunction

View File

@ -0,0 +1,338 @@
let s:File = vital#gina#import('System.File')
let s:Path = vital#gina#import('System.Filepath')
function! gina#action#index#define(binder) abort
call a:binder.define('index:add', function('s:on_add'), {
\ 'hidden': 1,
\ 'description': 'Add changes to an index',
\ 'mapping_mode': 'nv',
\ 'requirements': ['path'],
\ 'options': {},
\})
call a:binder.define('index:add:force', function('s:on_add'), {
\ 'hidden': 1,
\ 'description': 'Add changes to an index',
\ 'mapping_mode': 'nv',
\ 'requirements': ['path'],
\ 'options': { 'force': 1 },
\})
call a:binder.define('index:add:intent-to-add', function('s:on_add'), {
\ 'hidden': 1,
\ 'description': 'Intent to add changes to an index',
\ 'mapping_mode': 'nv',
\ 'requirements': ['path'],
\ 'options': { 'intent-to-add': 1 },
\})
call a:binder.define('index:rm', function('s:on_rm'), {
\ 'hidden': 1,
\ 'description': 'Remove files from a working tree and from an index',
\ 'mapping_mode': 'nv',
\ 'requirements': ['path'],
\ 'options': {},
\})
call a:binder.define('index:rm:force', function('s:on_rm'), {
\ 'hidden': 1,
\ 'description': 'Remove files from a working tree and from an index',
\ 'mapping_mode': 'nv',
\ 'requirements': ['path'],
\ 'options': { 'force': 1 },
\})
call a:binder.define('index:rm:cached', function('s:on_rm'), {
\ 'hidden': 1,
\ 'description': 'Remove files from an index but a working tree',
\ 'mapping_mode': 'nv',
\ 'requirements': ['path'],
\ 'options': { 'cached': 1 },
\})
call a:binder.define('index:reset', function('s:on_reset'), {
\ 'hidden': 1,
\ 'description': 'Reset changes on an index',
\ 'mapping_mode': 'nv',
\ 'requirements': ['path'],
\ 'options': {},
\})
call a:binder.define('index:stage', function('s:on_stage'), {
\ 'description': 'Stage changes to an index',
\ 'mapping_mode': 'nv',
\ 'requirements': ['path', 'sign'],
\ 'options': {},
\})
call a:binder.define('index:stage:force', function('s:on_stage'), {
\ 'hidden': 1,
\ 'description': 'Stage changes to an index',
\ 'mapping_mode': 'nv',
\ 'requirements': ['path', 'sign'],
\ 'options': { 'force': 1 },
\})
call a:binder.define('index:unstage', function('s:on_unstage'), {
\ 'description': 'Unstage changes from an index',
\ 'mapping_mode': 'nv',
\ 'requirements': ['path', 'sign'],
\ 'options': {},
\})
call a:binder.define('index:toggle', function('s:on_toggle'), {
\ 'description': 'Toggle stage/unstage of changes in an index',
\ 'mapping_mode': 'nv',
\ 'requirements': ['path', 'sign'],
\ 'options': {},
\})
call a:binder.define('index:checkout', function('s:on_checkout'), {
\ 'description': 'Checkout a content from an index',
\ 'mapping_mode': 'nv',
\ 'requirements': ['path'],
\ 'options': {},
\})
call a:binder.define('index:checkout:force', function('s:on_checkout'), {
\ 'hidden': 1,
\ 'description': 'Checkout a content from an index',
\ 'mapping_mode': 'nv',
\ 'requirements': ['path'],
\ 'options': { 'force': 1 },
\})
call a:binder.define('index:checkout:HEAD', function('s:on_checkout'), {
\ 'hidden': 1,
\ 'description': 'Checkout a content from a HEAD',
\ 'mapping_mode': 'nv',
\ 'requirements': ['path'],
\ 'options': { 'rev': 'HEAD' },
\})
call a:binder.define('index:checkout:HEAD:force', function('s:on_checkout'), {
\ 'hidden': 1,
\ 'description': 'Checkout a content from a HEAD',
\ 'mapping_mode': 'nv',
\ 'requirements': ['path'],
\ 'options': { 'rev': 'HEAD', 'force': 1 },
\})
call a:binder.define('index:checkout:origin', function('s:on_checkout'), {
\ 'hidden': 1,
\ 'description': 'Checkout a content from an origin/HEAD',
\ 'mapping_mode': 'nv',
\ 'requirements': ['path'],
\ 'options': { 'rev': 'origin/HEAD' },
\})
call a:binder.define('index:checkout:origin:force', function('s:on_checkout'), {
\ 'hidden': 1,
\ 'description': 'Checkout a content from an origin/HEAD',
\ 'mapping_mode': 'nv',
\ 'requirements': ['path'],
\ 'options': { 'rev': 'origin/HEAD', 'force': 1 },
\})
call a:binder.define('index:checkout:ours', function('s:on_checkout'), {
\ 'hidden': 1,
\ 'description': 'Checkout a content from local (ours) during merge',
\ 'mapping_mode': 'nv',
\ 'requirements': ['path'],
\ 'options': { 'ours': 1 },
\})
call a:binder.define('index:checkout:theirs', function('s:on_checkout'), {
\ 'hidden': 1,
\ 'description': 'Checkout a content from remote (theirs) during merge',
\ 'mapping_mode': 'nv',
\ 'requirements': ['path'],
\ 'options': { 'theirs': 1 },
\})
call a:binder.define('index:discard', function('s:on_discard'), {
\ 'description': 'Discard changes on a working tree',
\ 'mapping_mode': 'nv',
\ 'requirements': ['path', 'sign'],
\ 'options': {},
\})
call a:binder.define('index:discard:force', function('s:on_discard'), {
\ 'hidden': 1,
\ 'description': 'Discard changes on a working tree',
\ 'mapping_mode': 'nv',
\ 'requirements': ['path', 'sign'],
\ 'options': { 'force': 1 },
\})
endfunction
function! s:on_add(candidates, options) abort
if empty(a:candidates)
return
endif
let options = extend({
\ 'force': 0,
\ 'intent-to-add': 0,
\}, a:options)
let pathlist = map(copy(a:candidates), 'v:val.path')
execute printf(
\ '%s Gina add --ignore-errors %s %s -- %s',
\ options.mods,
\ options.force ? '--force' : '',
\ options['intent-to-add'] ? '--intent-to-add' : '',
\ gina#util#shellescape(pathlist),
\)
endfunction
function! s:on_rm(candidates, options) abort
if empty(a:candidates)
return
endif
let options = extend({
\ 'cached': 0,
\ 'force': 0,
\}, a:options)
let pathlist = map(copy(a:candidates), 'v:val.path')
execute printf(
\ '%s Gina rm --quiet --ignore-unmatch %s %s -- %s',
\ options.mods,
\ options.force ? '--force' : '',
\ options.cached ? '--cached' : '',
\ gina#util#shellescape(pathlist),
\)
endfunction
function! s:on_reset(candidates, options) abort
if empty(a:candidates)
return
endif
let options = extend({}, a:options)
let pathlist = map(copy(a:candidates), 'v:val.path')
execute printf(
\ '%s Gina reset --quiet -- %s',
\ options.mods,
\ gina#util#shellescape(pathlist),
\)
endfunction
function! s:on_stage(candidates, options) abort dict
if empty(a:candidates)
return
endif
let options = extend({
\ 'force': 0,
\}, a:options)
let rm_candidates = []
let add_candidates = []
for candidate in a:candidates
if candidate.sign =~# '^.D$'
call add(rm_candidates, candidate)
elseif candidate.sign !~# '^. $'
call add(add_candidates, candidate)
endif
endfor
if options.force
call self.call(options.mods . 'index:add:force', add_candidates)
if !empty(rm_candidates)
call gina#process#wait()
endif
call self.call(options.mods . 'index:rm:force', rm_candidates)
else
call self.call(options.mods . 'index:add', add_candidates)
if !empty(rm_candidates)
call gina#process#wait()
endif
call self.call(options.mods . 'index:rm', rm_candidates)
endif
endfunction
function! s:on_unstage(candidates, options) abort dict
let options = extend({}, a:options)
call self.call(options.mods . 'index:reset', a:candidates)
endfunction
function! s:on_toggle(candidates, options) abort dict
if empty(a:candidates)
return
endif
let options = extend({}, a:options)
let stage_candidates = []
let unstage_candidates = []
for candidate in a:candidates
if candidate.sign =~# '^\%(??\|!!\|.\w\)$'
call add(stage_candidates, candidate)
elseif candidate.sign =~# '^\w.$'
call add(unstage_candidates, candidate)
endif
endfor
call self.call(options.mods . 'index:stage', stage_candidates)
if !empty(unstage_candidates)
call gina#process#wait()
endif
call self.call(options.mods . 'index:unstage', unstage_candidates)
endfunction
function! s:on_checkout(candidates, options) abort
if empty(a:candidates)
return
endif
let options = extend({
\ 'force': 0,
\ 'ours': 0,
\ 'theirs': 0,
\ 'rev': '',
\}, a:options)
let pathlist = map(copy(a:candidates), 'v:val.path')
execute printf(
\ '%s Gina! checkout --quiet %s %s %s %s -- %s',
\ options.mods,
\ options.force ? '--force' : '',
\ options.ours ? '--ours' : '',
\ options.theirs ? '--theirs' : '',
\ gina#util#shellescape(options.rev),
\ gina#util#shellescape(pathlist),
\)
endfunction
function! s:on_discard(candidates, options) abort dict
if empty(a:candidates)
return
endif
let git = gina#core#get_or_fail()
let options = extend({
\ 'force': 0,
\}, a:options)
let delete_candidates = []
let checkout_candidates = []
for candidate in a:candidates
if candidate.sign =~# '^\%(??\|!!\)$'
call add(delete_candidates, candidate)
else
call add(checkout_candidates, candidate)
endif
endfor
if !options.force
call gina#core#console#warn(join([
\ 'A discard action will discard all local changes on the working ',
\ 'tree and the operation is irreversible, mean that you have no ',
\ 'chance to revert the operation.',
\], "\n"))
call gina#core#console#info(
\ 'This operation will be performed to the following candidates:'
\)
for candidate in extend(copy(delete_candidates), checkout_candidates)
call gina#core#console#echo('- ' . s:Path.realpath(candidate.path))
endfor
if !gina#core#console#confirm('Are you sure to discard the changes?', 'n')
return
endif
endif
" delete untracked files
for candidate in delete_candidates
let abspath = s:Path.realpath(gina#core#repo#abspath(git, candidate.path))
if isdirectory(abspath)
if g:gina#action#index#discard_directories
call s:File.rmdir(abspath, 'r')
else
call gina#core#console#info(printf(
\ '"%s" is directory. While g:gina#action#index#discard_directories is not set, it is not removed.',
\ candidate.path,
\))
endif
elseif filewritable(abspath)
call delete(abspath)
endif
endfor
call self.call(options.mods . 'index:checkout:HEAD:force', checkout_candidates)
if !empty(delete_candidates) && empty(checkout_candidates)
call gina#core#emitter#emit('modified:delay')
endif
endfunction
" Config ---------------------------------------------------------------------
call gina#config(expand('<sfile>'), {
\ 'discard_directories': 0,
\})

View File

@ -0,0 +1,55 @@
function! gina#action#ls#define(binder) abort
let params = {
\ 'description': 'List files/directories of the repository on a particular commit',
\ 'mapping_mode': 'nv',
\ 'requirements': ['rev'],
\}
call a:binder.define('ls', function('s:on_ls'), extend({
\ 'options': {},
\}, params))
call a:binder.define('ls:split', function('s:on_ls'), extend({
\ 'hidden': 1,
\ 'options': {'opener': 'new'},
\}, params))
call a:binder.define('ls:vsplit', function('s:on_ls'), extend({
\ 'hidden': 1,
\ 'options': {'opener': 'vnew'},
\}, params))
call a:binder.define('ls:tab', function('s:on_ls'), extend({
\ 'hidden': 1,
\ 'options': {'opener': 'tabedit'},
\}, params))
call a:binder.define('ls:preview', function('s:on_ls'), extend({
\ 'hidden': 1,
\ 'options': {'opener': 'pedit'},
\}, params))
" Alias
call a:binder.alias('ls:above', 'leftabove ls:split')
call a:binder.alias('ls:below', 'belowright ls:split')
call a:binder.alias('ls:left', 'leftabove ls:vsplit')
call a:binder.alias('ls:right', 'belowright ls:vsplit')
call a:binder.alias('ls:top', 'topleft ls:split')
call a:binder.alias('ls:bottom', 'botright ls:split')
call a:binder.alias('ls:leftest', 'topleft ls:vsplit')
call a:binder.alias('ls:rightest', 'botright ls:vsplit')
endfunction
" Private --------------------------------------------------------------------
function! s:on_ls(candidates, options) abort
if empty(a:candidates)
return
endif
let options = extend({
\ 'opener': '',
\}, a:options)
for candidate in a:candidates
execute printf(
\ '%s Gina ls %s %s',
\ options.mods,
\ gina#util#shellescape(options.opener, '--opener='),
\ gina#util#shellescape(candidate.rev),
\)
endfor
endfunction

View File

@ -0,0 +1,77 @@
function! gina#action#patch#define(binder) abort
let params = {
\ 'description': 'Open three buffers to patch changes to an index',
\ 'mapping_mode': 'nv',
\ 'requirements': ['path'],
\}
call a:binder.define('patch', function('s:on_patch'), extend({
\ 'options': {},
\}, params))
call a:binder.define('patch:split', function('s:on_patch'), extend({
\ 'hidden': 1,
\ 'options': {'opener': 'new'},
\}, params))
call a:binder.define('patch:vsplit', function('s:on_patch'), extend({
\ 'hidden': 1,
\ 'options': {'opener': 'vnew'},
\}, params))
call a:binder.define('patch:tab', function('s:on_patch'), extend({
\ 'hidden': 1,
\ 'options': {'opener': 'tabedit'},
\}, params))
call a:binder.define('patch:oneside', function('s:on_patch'), extend({
\ 'options': {'oneside': 1},
\}, params))
call a:binder.define('patch:oneside:split', function('s:on_patch'), extend({
\ 'hidden': 1,
\ 'options': {'oneside': 1, 'opener': 'new'},
\}, params))
call a:binder.define('patch:oneside:vsplit', function('s:on_patch'), extend({
\ 'hidden': 1,
\ 'options': {'oneside': 1, 'opener': 'vnew'},
\}, params))
call a:binder.define('patch:oneside:tab', function('s:on_patch'), extend({
\ 'hidden': 1,
\ 'options': {'oneside': 1, 'opener': 'tabedit'},
\}, params))
" Alias
call a:binder.alias('patch:above', 'leftabove patch:split')
call a:binder.alias('patch:below', 'belowright patch:split')
call a:binder.alias('patch:left', 'leftabove patch:vsplit')
call a:binder.alias('patch:right', 'belowright patch:vsplit')
call a:binder.alias('patch:top', 'topleft patch:split')
call a:binder.alias('patch:bottom', 'botright patch:split')
call a:binder.alias('patch:leftest', 'topleft patch:vsplit')
call a:binder.alias('patch:rightest', 'botright patch:vsplit')
call a:binder.alias('patch:oneside:above', 'leftabove patch:oneside:split')
call a:binder.alias('patch:oneside:below', 'belowright patch:oneside:split')
call a:binder.alias('patch:oneside:left', 'leftabove patch:oneside:vsplit')
call a:binder.alias('patch:oneside:right', 'belowright patch:oneside:vsplit')
call a:binder.alias('patch:oneside:top', 'topleft patch:oneside:split')
call a:binder.alias('patch:oneside:bottom', 'botright patch:oneside:split')
call a:binder.alias('patch:oneside:leftest', 'topleft patch:oneside:vsplit')
call a:binder.alias('patch:oneside:rightest', 'botright patch:oneside:vsplit')
endfunction
" Private --------------------------------------------------------------------
function! s:on_patch(candidates, options) abort
if empty(a:candidates)
return
endif
let options = extend({
\ 'opener': '',
\ 'oneside': 0,
\}, a:options)
for candidate in a:candidates
execute printf(
\ '%s Gina patch %s %s %s %s %s',
\ options.mods,
\ options.oneside ? '--oneside' : '',
\ gina#util#shellescape(options.opener, '--opener='),
\ gina#util#shellescape(get(candidate, 'line'), '--line='),
\ gina#util#shellescape(get(candidate, 'col'), '--col='),
\ gina#util#shellescape(candidate.path),
\)
endfor
endfunction

View File

@ -0,0 +1,120 @@
function! gina#action#show#define(binder) abort
let params = {
\ 'description': 'Show a commit or a content at a commit',
\ 'mapping_mode': 'nv',
\ 'requirements': [],
\}
call a:binder.define('show', function('s:on_show'), extend({
\ 'options': {},
\}, params))
call a:binder.define('show:split', function('s:on_show'), extend({
\ 'hidden': 1,
\ 'options': {'opener': 'new'},
\}, params))
call a:binder.define('show:vsplit', function('s:on_show'), extend({
\ 'hidden': 1,
\ 'options': {'opener': 'vnew'},
\}, params))
call a:binder.define('show:tab', function('s:on_show'), extend({
\ 'hidden': 1,
\ 'options': {'opener': 'tabedit'},
\}, params))
call a:binder.define('show:preview', function('s:on_show'), extend({
\ 'hidden': 1,
\ 'options': {'opener': 'pedit'},
\}, params))
let params = {
\ 'description': 'Show a commit',
\ 'mapping_mode': 'nv',
\ 'requirements': [],
\}
call a:binder.define('show:commit', function('s:on_commit'), extend({
\ 'options': {},
\}, params))
call a:binder.define('show:commit:split', function('s:on_commit'), extend({
\ 'hidden': 1,
\ 'options': {'opener': 'new'},
\}, params))
call a:binder.define('show:commit:vsplit', function('s:on_commit'), extend({
\ 'hidden': 1,
\ 'options': {'opener': 'vnew'},
\}, params))
call a:binder.define('show:commit:tab', function('s:on_commit'), extend({
\ 'hidden': 1,
\ 'options': {'opener': 'tabedit'},
\}, params))
call a:binder.define('show:commit:preview', function('s:on_commit'), extend({
\ 'hidden': 1,
\ 'options': {'opener': 'pedit'},
\}, params))
" Alias
call a:binder.alias('show:above', 'leftabove show:split')
call a:binder.alias('show:below', 'belowright show:split')
call a:binder.alias('show:left', 'leftabove show:vsplit')
call a:binder.alias('show:right', 'belowright show:vsplit')
call a:binder.alias('show:top', 'topleft show:split')
call a:binder.alias('show:bottom', 'botright show:split')
call a:binder.alias('show:leftest', 'topleft show:vsplit')
call a:binder.alias('show:rightest', 'botright show:vsplit')
call a:binder.alias('show:preview:top', 'topleft show:preview')
call a:binder.alias('show:preview:bottom', 'botright show:preview')
call a:binder.alias('show:commit:above', 'leftabove show:commit:split')
call a:binder.alias('show:commit:below', 'belowright show:commit:split')
call a:binder.alias('show:commit:left', 'leftabove show:commit:vsplit')
call a:binder.alias('show:commit:right', 'belowright show:commit:vsplit')
call a:binder.alias('show:commit:top', 'topleft show:commit:split')
call a:binder.alias('show:commit:bottom', 'botright show:commit:split')
call a:binder.alias('show:commit:leftest', 'topleft show:commit:vsplit')
call a:binder.alias('show:commit:rightest', 'botright show:commit:vsplit')
call a:binder.alias('show:commit:preview:top', 'topleft show:commit:preview')
call a:binder.alias('show:commit:preview:bottom', 'botright show:commit:preview')
endfunction
" Private --------------------------------------------------------------------
function! s:on_show(candidates, options) abort
if empty(a:candidates)
return
endif
let options = extend({
\ 'opener': '',
\}, a:options)
for candidate in a:candidates
let treeish = gina#core#treeish#build(
\ gina#util#get(candidate, 'rev'),
\ gina#util#get(candidate, 'path', v:null),
\)
execute printf(
\ '%s Gina show %s %s %s %s -- %s',
\ options.mods,
\ gina#util#shellescape(options.opener, '--opener='),
\ gina#util#shellescape(get(candidate, 'line'), '--line='),
\ gina#util#shellescape(get(candidate, 'col'), '--col='),
\ gina#util#shellescape(treeish),
\ gina#util#shellescape(gina#util#get(candidate, 'residual')),
\)
endfor
endfunction
function! s:on_commit(candidates, options) abort
if empty(a:candidates)
return
endif
let options = extend({
\ 'opener': '',
\}, a:options)
for candidate in a:candidates
let treeish = gina#core#treeish#build(
\ gina#util#get(candidate, 'rev'),
\ v:null
\)
execute printf(
\ '%s Gina show %s %s -- %s',
\ options.mods,
\ gina#util#shellescape(options.opener, '--opener='),
\ gina#util#shellescape(treeish),
\ gina#util#shellescape(gina#util#get(candidate, 'residual')),
\)
endfor
endfunction

View File

@ -0,0 +1,218 @@
function! gina#action#stash#define(binder) abort
let params = {
\ 'description': 'Show changes in a stash',
\ 'mapping_mode': 'nv',
\ 'requirements': ['stash'],
\}
call a:binder.define('stash:show', function('s:on_show'), extend({
\ 'options': {},
\}, params))
call a:binder.define('stash:show:split', function('s:on_show'), extend({
\ 'hidden': 1,
\ 'options': {'opener': 'new'},
\}, params))
call a:binder.define('stash:show:vsplit', function('s:on_show'), extend({
\ 'hidden': 1,
\ 'options': {'opener': 'vnew'},
\}, params))
call a:binder.define('stash:show:tab', function('s:on_show'), extend({
\ 'hidden': 1,
\ 'options': {'opener': 'tabedit'},
\}, params))
call a:binder.define('stash:show:preview', function('s:on_show'), extend({
\ 'hidden': 1,
\ 'options': {'opener': 'pedit'},
\}, params))
call a:binder.define('stash:drop', function('s:on_drop'), {
\ 'description': 'Remove a stashed state',
\ 'mapping_mode': 'nv',
\ 'requirements': ['stash'],
\ 'options': {},
\})
call a:binder.define('stash:drop:force', function('s:on_drop'), {
\ 'hidden': 1,
\ 'description': 'Remove a stashed state',
\ 'mapping_mode': 'nv',
\ 'requirements': ['stash'],
\ 'options': {'force': 1},
\})
call a:binder.define('stash:pop', function('s:on_pop'), {
\ 'description': 'Remove a stashed state and apply to a working tree',
\ 'mapping_mode': 'nv',
\ 'requirements': ['stash'],
\ 'options': {},
\})
call a:binder.define('stash:pop:index', function('s:on_pop'), {
\ 'hidden': 1,
\ 'description': 'Remove a stashed state and apply to a working tree',
\ 'mapping_mode': 'nv',
\ 'requirements': ['stash'],
\ 'options': {'index': 1},
\})
call a:binder.define('stash:apply', function('s:on_apply'), {
\ 'description': 'Apply a stashed state to a working tree',
\ 'mapping_mode': 'nv',
\ 'requirements': ['stash'],
\ 'options': {},
\})
call a:binder.define('stash:apply:index', function('s:on_apply'), {
\ 'hidden': 1,
\ 'description': 'Apply a stashed state to a working tree',
\ 'mapping_mode': 'nv',
\ 'requirements': ['stash'],
\ 'options': {'index': 1},
\})
call a:binder.define('stash:branch', function('s:on_branch'), {
\ 'description': 'Create a new branch with a stashed state',
\ 'mapping_mode': 'nv',
\ 'requirements': ['stash'],
\ 'options': {},
\})
call a:binder.define('stash:clear', function('s:on_clear'), {
\ 'description': 'Remove all stashed states',
\ 'mapping_mode': 'n',
\ 'requirements': [],
\ 'options': {},
\})
call a:binder.define('stash:clear:force', function('s:on_clear'), {
\ 'hidden': 1,
\ 'description': 'Remove all stashed states',
\ 'mapping_mode': 'n',
\ 'requirements': [],
\ 'options': {'force': 1},
\})
" Alias
call a:binder.alias('stash:show:above', 'leftabove stash:show:split')
call a:binder.alias('stash:show:below', 'belowright stash:show:split')
call a:binder.alias('stash:show:left', 'leftabove stash:show:vsplit')
call a:binder.alias('stash:show:right', 'belowright stash:show:vsplit')
call a:binder.alias('stash:show:top', 'topleft stash:show:split')
call a:binder.alias('stash:show:bottom', 'botright stash:show:split')
call a:binder.alias('stash:show:leftest', 'topleft stash:show:vsplit')
call a:binder.alias('stash:show:rightest', 'botright stash:show:vsplit')
endfunction
" Private --------------------------------------------------------------------
function! s:on_show(candidates, options) abort
if empty(a:candidates)
return
endif
let options = extend({
\ 'opener': '',
\}, a:options)
for candidate in a:candidates
execute printf(
\ '%s Gina stash show %s %s %s %s',
\ options.mods,
\ gina#util#shellescape(options.opener, '--opener='),
\ gina#util#shellescape(get(candidate, 'line'), '--line='),
\ gina#util#shellescape(get(candidate, 'col'), '--col='),
\ gina#util#shellescape(candidate.stash),
\)
endfor
endfunction
function! s:on_drop(candidates, options) abort
if empty(a:candidates)
return
endif
let options = extend({
\ 'force': 0,
\}, a:options)
if !options.force
call gina#core#console#warn(join([
\ 'A stash:drop action will drop selected stashes ',
\ 'and the operation is irreversible, mean that you have no ',
\ 'chance to revert the operation with safety mechanisms.',
\], "\n"))
call gina#core#console#info(
\ 'This operation will be performed to the following candidates:'
\)
for candidate in a:candidates
call gina#core#console#echo('- ' . candidate.stash)
endfor
if !gina#core#console#confirm('Are you sure to drop stashes?', 'n')
return
endif
endif
for candidate in a:candidates
execute printf(
\ '%s Gina stash drop --quiet %s',
\ options.mods,
\ gina#util#shellescape(candidate.stash),
\)
endfor
endfunction
function! s:on_pop(candidates, options) abort
if empty(a:candidates)
return
endif
let options = extend({
\ 'index': 0,
\}, a:options)
for candidate in a:candidates
execute printf(
\ '%s Gina stash pop --quiet %s %s',
\ options.mods,
\ options.index ? '--index' : '',
\ gina#util#shellescape(candidate.stash),
\)
endfor
endfunction
function! s:on_apply(candidates, options) abort
if empty(a:candidates)
return
endif
let options = extend({
\ 'index': 0,
\}, a:options)
for candidate in a:candidates
execute printf(
\ '%s Gina stash apply --quiet %s %s',
\ options.mods,
\ options.index ? '--index' : '',
\ gina#util#shellescape(candidate.stash),
\)
endfor
endfunction
function! s:on_branch(candidates, options) abort
if empty(a:candidates)
return
endif
let options = extend({}, a:options)
for candidate in a:candidates
let name = gina#core#console#ask_or_cancel(
\ 'Name: ', '',
\)
execute printf(
\ '%s Gina stash branch %s %s',
\ options.mods,
\ name,
\ gina#util#shellescape(candidate.stash),
\)
endfor
endfunction
function! s:on_clear(candidates, options) abort
let options = extend({
\ 'force': 0,
\}, a:options)
if !options.force
call gina#core#console#warn(join([
\ 'A stash:clear action will clear all stashes ',
\ 'and the operation is irreversible, mean that you have no ',
\ 'chance to revert the operation with safety mechanisms.',
\], "\n"))
if !gina#core#console#confirm('Are you sure to clear stashes?', 'n')
return
endif
endif
execute printf(
\ '%s Gina stash clear',
\ options.mods,
\)
endfunction

View File

@ -0,0 +1,114 @@
function! gina#action#tag#define(binder) abort
call a:binder.define('tag:new:lightweight', function('s:on_new'), {
\ 'description': 'Create a lightweight tag',
\ 'mapping_mode': 'n',
\ 'requirements': [],
\ 'options': {},
\})
call a:binder.define('tag:new:annotate', function('s:on_new'), {
\ 'description': 'Create an unsigned, annotated tag',
\ 'mapping_mode': 'n',
\ 'requirements': [],
\ 'options': {'annotate': 1},
\})
call a:binder.define('tag:new:sign', function('s:on_new'), {
\ 'description': 'Create a GPG-signed tag',
\ 'mapping_mode': 'n',
\ 'requirements': [],
\ 'options': {'sign': 1},
\})
call a:binder.define('tag:new:lightweight:force', function('s:on_new'), {
\ 'hidden': 1,
\ 'description': 'Create a lightweight tag',
\ 'mapping_mode': 'n',
\ 'requirements': [],
\ 'options': {'force': 1},
\})
call a:binder.define('tag:new:annotate:force', function('s:on_new'), {
\ 'hidden': 1,
\ 'description': 'Create an unsigned, annotated tag',
\ 'mapping_mode': 'n',
\ 'requirements': [],
\ 'options': {'annotate': 1, 'force': 1},
\})
call a:binder.define('tag:new:sign:force', function('s:on_new'), {
\ 'hidden': 1,
\ 'description': 'Create a GPG-signed tag',
\ 'mapping_mode': 'n',
\ 'requirements': [],
\ 'options': {'sign': 1, 'force': 1},
\})
call a:binder.define('tag:delete', function('s:on_delete'), {
\ 'description': 'Delete a tag',
\ 'mapping_mode': 'nv',
\ 'requirements': ['tag'],
\ 'options': {},
\})
call a:binder.define('tag:verify', function('s:on_verify'), {
\ 'description': 'Verify a tag',
\ 'mapping_mode': 'nv',
\ 'requirements': ['tag'],
\ 'options': {},
\})
" Alias
call a:binder.alias('tag:new', 'tag:new:annotate')
endfunction
" Private --------------------------------------------------------------------
function! s:on_new(candidates, options) abort
if empty(a:candidates)
return
endif
let options = extend({
\ 'annotate': 0,
\ 'sign': 0,
\ 'force': 0,
\}, a:options)
for candidate in a:candidates
let name = gina#core#console#ask_or_cancel(
\ 'Name: ', '',
\)
let from = gina#core#console#ask_or_cancel(
\ 'From: ', 'HEAD',
\ function('gina#complete#commit#branch')
\)
execute printf(
\ '%s Gina tag %s %s %s %s %s',
\ options.mods,
\ options.annotate ? '--annotate' : '',
\ options.sign ? '--sign' : '',
\ options.force ? '--force' : '',
\ gina#util#shellescape(name),
\ gina#util#shellescape(from),
\)
endfor
endfunction
function! s:on_delete(candidates, options) abort
if empty(a:candidates)
return
endif
let options = extend({}, a:options)
for candidate in a:candidates
execute printf(
\ '%s Gina tag --delete %s',
\ options.mods,
\ gina#util#shellescape(candidate.tag),
\)
endfor
endfunction
function! s:on_verify(candidates, options) abort
if empty(a:candidates)
return
endif
let options = extend({}, a:options)
for candidate in a:candidates
execute printf(
\ '%s Gina tag --verify %s',
\ options.mods,
\ gina#util#shellescape(candidate.tag),
\)
endfor
endfunction

View File

@ -0,0 +1,81 @@
function! gina#action#yank#define(binder) abort
call a:binder.define('yank:rev', function('s:on_yank_rev'), {
\ 'description': 'Yank the revision of a candidate under the cursor',
\ 'mapping_mode': 'nv',
\ 'requirements': ['rev'],
\ 'use_marks': 0,
\ 'clear_marks': 0,
\})
call a:binder.define('yank:path', function('s:on_yank_path'), {
\ 'description': 'Yank the path of a candidate under the cursor',
\ 'mapping_mode': 'nv',
\ 'requirements': ['path'],
\ 'use_marks': 0,
\ 'clear_marks': 0,
\})
call a:binder.define('yank:treeish', function('s:on_yank_treeish'), {
\ 'description': 'Yank the treeish (revision and path) of a candidate under the cursor',
\ 'mapping_mode': 'nv',
\ 'requirements': ['rev', 'path'],
\ 'use_marks': 0,
\ 'clear_marks': 0,
\})
endfunction
" Private --------------------------------------------------------------------
function! s:on_yank_rev(candidates, options) abort dict
if empty(a:candidates)
return
endif
let options = extend({}, a:options)
let candidates = map(
\ copy(a:candidates),
\ 's:get_rev(v:val)'
\)
call gina#util#yank(join(candidates, "\n"))
endfunction
function! s:on_yank_path(candidates, options) abort dict
if empty(a:candidates)
return
endif
let options = extend({}, a:options)
let candidates = map(
\ copy(a:candidates),
\ 's:get_path(v:val)'
\)
call gina#util#yank(join(candidates, "\n"))
endfunction
function! s:on_yank_treeish(candidates, options) abort dict
if empty(a:candidates)
return
endif
let options = extend({}, a:options)
let candidates = map(
\ copy(a:candidates),
\ 's:get_treeish(v:val)'
\)
call gina#util#yank(join(candidates, "\n"))
endfunction
function! s:get_rev(candidate) abort
let rev = gina#util#get(a:candidate, 'rev', v:null)
return rev is v:null
\ ? gina#core#buffer#param(bufname('%'), 'rev')
\ : rev
endfunction
function! s:get_path(candidate) abort
let path = gina#util#get(a:candidate, 'path', v:null)
return path is v:null
\ ? gina#core#buffer#param(bufname('%'), 'path')
\ : path
endfunction
function! s:get_treeish(candidate) abort
let rev = s:get_rev(a:candidate)
let path = s:get_path(a:candidate)
return gina#core#treeish#build(rev, path)
endfunction

View File

@ -0,0 +1,65 @@
function! gina#command#call(bang, range, rargs, mods) abort
if a:bang ==# '!' && a:rargs[0] ==# '!'
return gina#command#call('', a:range, '_shell ' . a:rargs[1:], a:mods)
elseif a:bang ==# '!'
return gina#command#call('', a:range, '_raw ' . a:rargs, a:mods)
endif
let args = gina#core#args#new(a:rargs)
if empty(args.params.scheme)
" The scheme becomes empty when Gina-xxxxx is given
call gina#core#console#error(printf(
\ 'The "Gina%s" is not correct gina command. You may want ":Gina %s"',
\ a:rargs,
\ a:rargs[1:],
\))
return
endif
try
call gina#core#revelator#call(
\ printf('gina#command#%s#call', args.params.scheme),
\ [a:range, args, a:mods],
\)
return
catch /^Vim\%((\a\+)\)\=:E117: [^:]\+: gina#command#[^#]\+#call/
call gina#core#console#debug(v:exception)
call gina#core#console#debug(v:throwpoint)
endtry
return gina#command#call('', a:range, '_raw ' . a:rargs, a:mods)
endfunction
function! gina#command#complete(arglead, cmdline, cursorpos) abort
if a:cmdline =~# '^.\{-}Gina!'
return gina#command#complete(
\ a:arglead,
\ substitute(a:cmdline, '^\(.\{-}\)Gina!', '\1Gina _raw', ''),
\ a:cursorpos,
\)
elseif a:cmdline =~# printf('^.\{-}Gina\s\+%s$', a:arglead)
return gina#complete#common#command(a:arglead, a:cmdline, a:cursorpos)
endif
let cmdline = matchstr(a:cmdline, '^.\{-}Gina\s\+\zs.*')
let scheme = matchstr(cmdline, '^\S\+')
let scheme = substitute(scheme, '!$', '', '')
let scheme = substitute(scheme, '\W', '_', 'g')
try
return gina#core#revelator#call(
\ printf('gina#command#%s#complete', scheme),
\ [a:arglead, cmdline, a:cursorpos],
\)[:g:gina#complete_threshold]
catch /^Vim\%((\a\+)\)\=:E117: [^:]\+: gina#command#[^#]\+#complete/
call gina#core#console#debug(v:exception)
call gina#core#console#debug(v:throwpoint)
endtry
return gina#command#complete(
\ a:arglead,
\ substitute(a:cmdline, '^\(.\{-}\)Gina', '\1Gina _raw', ''),
\ a:cursorpos,
\)[:g:gina#complete_threshold]
endfunction
function! gina#command#scheme(sfile) abort
let name = fnamemodify(a:sfile, ':t')
let name = matchstr(name, '.*\ze\.vim')
let scheme = substitute(name, '_', '-', 'g')
return scheme
endfunction

View File

@ -0,0 +1,154 @@
let s:String = vital#gina#import('Data.String')
let s:SCHEME = gina#command#scheme(expand('<sfile>'))
let s:current = {
\ 'bufnr': 0,
\ 'middleware': {},
\}
function! gina#command#_events#call(range, args, mods) abort
let git = gina#core#get_or_fail()
let args = s:build_args(git, a:args)
let bufname = gina#core#buffer#bufname(git, s:SCHEME)
call gina#core#buffer#open(bufname, {
\ 'mods': a:mods,
\ 'group': args.params.group,
\ 'opener': args.params.opener,
\ 'cmdarg': args.params.cmdarg,
\ 'callback': {
\ 'fn': function('s:init'),
\ 'args': [args],
\ }
\})
endfunction
" Private --------------------------------------------------------------------
function! s:build_args(git, args) abort
let args = a:args.clone()
let args.params.group = args.pop('--group', '')
let args.params.opener = args.pop('--opener', '')
let args.params.detail = args.pop('--detail')
return args.lock()
endfunction
function! s:init(args) abort
call gina#core#meta#set('args', a:args)
if exists('b:gina_initialized')
return
endif
let b:gina_initialized = 1
setlocal winfixwidth
setlocal winfixheight
setlocal buftype=nofile
setlocal bufhidden=wipe
setlocal noswapfile
setlocal nomodifiable
setlocal noautoread
setlocal nolist nospell
setlocal nowrap nonumber norelativenumber
augroup gina_internal_command
autocmd! * <buffer>
autocmd BufReadCmd <buffer> call s:BufReadCmd()
autocmd BufWipeout <buffer> call s:BufWipeout()
augroup END
endfunction
function! s:BufReadCmd() abort
call gina#core#emitter#remove_middleware(s:current.middleware)
setlocal nobuflisted
setlocal filetype=gina-_events
let args = gina#core#meta#get_or_fail('args')
let s:current.bufnr = bufnr('%')
let s:current.middleware = copy(s:middleware)
let s:current.middleware.detail = args.params.detail
call gina#core#emitter#add_middleware(s:current.middleware)
endfunction
function! s:BufWipeout() abort
call gina#core#emitter#remove_middleware(s:current.middleware)
endfunction
function! s:print_message(msg) abort
let bufnr = s:current.bufnr
if !bufnr || bufwinnr(bufnr) < 1
call gina#core#emitter#remove_middleware(s:current.middleware)
let s:current.bufnr = 0
return
endif
" NOTE:
" 'timer_start' is required for prevent E523 Not allowed here raised when
" events are emitted from 'statusline' or 'tabline'
call timer_start(0, { -> gina#core#writer#replace(bufnr, -1, -1, [a:msg]) })
endfunction
function! s:print_event(prefix, name, attrs) abort
let width = gina#util#winwidth(bufwinnr(s:current.bufnr))
let head = printf('%-5s: %s: %s',
\ a:prefix,
\ s:now(),
\ a:name,
\)
let tail = printf('<%s>', bufname('%'))
let args = join(map(copy(a:attrs), 'string(v:val)'), ', ')
let args = substitute(args, '\r\?\n', '\\n', 'g')
let args = substitute(args, '\e', '^[', 'g')
let args = s:String.truncate_skipping(
\ printf('(%s)', args),
\ width - len(head) - len(tail) - 1,
\ 3, '...'
\)
let head = head . args . ' '
let message = head . s:String.pad_left(tail, width - len(head))
call s:print_message(message)
endfunction
function! s:print_listeners(listeners) abort
for [Listener, instance] in a:listeners
call s:print_message(printf(
\ '| %s [%s]',
\ string(Listener),
\ string(instance),
\))
endfor
endfunction
if has('python3')
python3 import datetime
function! s:now() abort
return py3eval('datetime.datetime.now().strftime("%H:%M:%S.%f")')
endfunction
elseif has('python')
python import datetime
function! s:now() abort
return pyeval('datetime.datetime.now().strftime("%H:%M:%S.%f")')
endfunction
else
function! s:now() abort
return strftime('%H:%M:%S.??????')
endfunction
endif
" Middleware -----------------------------------------------------------------
let s:middleware = {'detail': 0}
function! s:middleware.on_emit_pre(name, listeners, attrs) abort
call s:print_event('pre', a:name, a:attrs)
if self.detail
call s:print_listeners(a:listeners)
endif
endfunction
function! s:middleware.on_emit_post(name, listeners, attrs) abort
call s:print_event('post', a:name, a:attrs)
if self.detail
call s:print_listeners(a:listeners)
endif
endfunction

View File

@ -0,0 +1,113 @@
function! gina#command#_raw#call(range, args, mods) abort
let git = gina#core#get()
let args = s:build_args(git, a:args)
let pipe = a:mods =~# '\<silent\>'
\ ? deepcopy(s:pipe_silent)
\ : deepcopy(s:pipe)
return gina#process#open(git, args, pipe)
endfunction
function! gina#command#_raw#complete(arglead, cmdline, cursorpos) abort
let args = gina#core#args#new(matchstr(a:cmdline, '^.*\ze .*'))
if empty(args.get(1))
return gina#complete#common#raw_command(a:arglead, a:cmdline, a:cursorpos)
endif
if args.get(1) =~# '^\%(fetch\|pull\|push\|switch\)$'
return s:{args.get(1)}_complete(a:arglead, a:cmdline, a:cursorpos)
endif
return []
endfunction
" Private --------------------------------------------------------------------
function! s:build_args(git, args) abort
let args = a:args.clone()
if args.get(0) ==# '_raw'
" Remove leading '_raw' if exists
call args.pop(0)
endif
return args.lock()
endfunction
function! s:fetch_complete(arglead, cmdline, cursorpos) abort
let args = gina#core#args#new(matchstr(a:cmdline, '^.*\ze .*'))
if empty(args.get(2))
return gina#complete#common#remote(a:arglead, a:cmdline, a:cursorpos)
endif
if empty(args.get(3))
" TODO: Return refspecs in remote repository "args.get(2)".
endif
return []
endfunction
function! s:pull_complete(arglead, cmdline, cursorpos) abort
let args = gina#core#args#new(matchstr(a:cmdline, '^.*\ze .*'))
if empty(args.get(2))
return gina#complete#common#remote(a:arglead, a:cmdline, a:cursorpos)
endif
if empty(args.get(3))
" TODO: Return refspecs in remote repository "args.get(2)".
endif
return []
endfunction
function! s:push_complete(arglead, cmdline, cursorpos) abort
let args = gina#core#args#new(matchstr(a:cmdline, '^.*\ze .*'))
if empty(args.get(2))
return gina#complete#common#remote(a:arglead, a:cmdline, a:cursorpos)
endif
if empty(args.get(3))
return gina#complete#commit#local_branch(a:arglead, a:cmdline, a:cursorpos)
endif
return []
endfunction
function! s:switch_complete(arglead, cmdline, cursorpos) abort
let args = gina#core#args#new(matchstr(a:cmdline, '^.*\ze .*'))
if empty(args.get(2))
return gina#complete#commit#local_branch(a:arglead, a:cmdline, a:cursorpos)
endif
return []
endfunction
" Pipe -----------------------------------------------------------------------
function! s:_pipe_on_exit(exitval) abort dict
call call(s:original_pipe.on_exit, [a:exitval], self)
call gina#core#emitter#emit(
\ 'command:called:raw',
\ self.params.scheme,
\)
endfunction
function! s:_pipe_silent_on_exit(exitval) abort dict
call call(s:original_pipe_silent.on_exit, [a:exitval], self)
call gina#core#emitter#emit(
\ 'command:called:raw',
\ self.params.scheme,
\)
endfunction
let s:original_pipe = gina#process#pipe#echo()
let s:original_pipe_silent = gina#process#pipe#default()
let s:pipe = extend(deepcopy(s:original_pipe), {
\ 'on_exit': function('s:_pipe_on_exit'),
\})
let s:pipe_silent = extend(deepcopy(s:original_pipe_silent), {
\ 'on_exit': function('s:_pipe_silent_on_exit'),
\})
" Event ----------------------------------------------------------------------
function! s:on_command_called_raw(...) abort
call gina#core#emitter#emit('modified:delay')
endfunction
if !exists('s:subscribed')
let s:subscribed = 1
call gina#core#emitter#subscribe(
\ 'command:called:raw',
\ function('s:on_command_called_raw')
\)
endif

View File

@ -0,0 +1,31 @@
function! gina#command#_shell#call(range, args, mods) abort
let git = gina#core#get()
let args = gina#process#build_raw_args(git, s:build_args(git, a:args))
let cmdline = join(map(args, 's:shellescape(v:val)'))
if has('nvim')
tabnew
execute ':terminal' cmdline
augroup gina_command__shell_internal
autocmd! * <buffer>
autocmd TermClose <buffer> call gina#core#emitter#emit('modified:delay')
augroup END
else
execute ':!' cmdline
call gina#core#emitter#emit('modified:delay')
endif
endfunction
" Private --------------------------------------------------------------------
function! s:build_args(git, args) abort
let args = a:args.clone()
if args.get(0) ==# '_shell'
" Remove leading '_shell' if exists
call args.pop(0)
endif
return args.lock()
endfunction
function! s:shellescape(val) abort
return a:val =~# '\s' ? shellescape(a:val) : a:val
endfunction

View File

@ -0,0 +1,404 @@
let s:Dict = vital#gina#import('Data.Dict')
let s:Group = vital#gina#import('Vim.Buffer.Group')
let s:Opener = vital#gina#import('Vim.Buffer.Opener')
let s:SCHEME = gina#command#scheme(expand('<sfile>'))
function! gina#command#blame#call(range, args, mods) abort
call gina#core#options#help_if_necessary(a:args, s:get_options())
call gina#process#register(s:SCHEME, 1)
try
call s:call(a:range, a:args, a:mods)
finally
call gina#process#unregister(s:SCHEME, 1)
endtry
endfunction
function! gina#command#blame#complete(arglead, cmdline, cursorpos) abort
let args = gina#core#args#new(matchstr(a:cmdline, '^.*\ze .*'))
if a:arglead[0] ==# '-' || !empty(args.get(1))
let options = s:get_options()
return options.complete(a:arglead, a:cmdline, a:cursorpos)
endif
return gina#complete#common#treeish(a:arglead, a:cmdline, a:cursorpos)
endfunction
" Private --------------------------------------------------------------------
function! s:get_options() abort
let options = gina#core#options#new()
call options.define(
\ '-h|--help',
\ 'Show this help.',
\)
call options.define(
\ '--opener=',
\ 'A Vim command to open a new buffer.',
\ ['edit', 'split', 'vsplit', 'tabedit', 'pedit'],
\)
call options.define(
\ '--line=',
\ 'An initial line number.',
\)
call options.define(
\ '--col=',
\ 'An initial column number.',
\)
call options.define(
\ '--group1=',
\ 'A window group name used for a blame body buffer.',
\)
call options.define(
\ '--group2=',
\ 'A window group name used for a blame navigation buffer.',
\)
call options.define(
\ '--width=',
\ 'A window width used for a blame navigation buffer.',
\)
call options.define(
\ '--format=',
\ 'Format string used to construct the navi line.',
\)
call options.define(
\ '--root',
\ 'Do not treat root commits as boundaries.',
\)
call options.define(
\ '-L',
\ 'Annotate only the given line range. May be specified multiple times.',
\)
call options.define(
\ '--reverse=',
\ 'Walk history forward instead of backward.',
\ function('gina#complete#range#any'),
\)
call options.define(
\ '--encoding=',
\ 'Specifies the encoding used to output.',
\)
call options.define(
\ '--content=', join([
\ 'This flag makes the command pretend as if the working tree copy',
\ 'has the contents of the named file.',
\ 'Works only when {rev} is not specified.'
\ ]),
\ function('gina#complete#filename#any'),
\)
call options.define(
\ '-M',
\ 'Detect moved or copied lines within a file.',
\)
call options.define(
\ '-C',
\ 'In addition to -M, detect lines moved or copied from other files.',
\)
call options.define(
\ '-w',
\ 'Ignore whitespace when comparing.',
\)
return options
endfunction
function! s:build_args(git, args, range) abort
let args = a:args.clone()
let args.params.groups = [
\ args.pop('--group1', 'blame-body'),
\ args.pop('--group2', 'blame-navi'),
\]
let args.params.opener = args.pop('--opener', 'tabnew')
let args.params.width = args.pop('--width', v:null)
let args.params.format = args.pop('--format', v:null)
" Warn deperecated feature
if args.pop('--use-author-instead')
call gina#core#console#warn(
\ '--use-author-instead option is removed. Use --format instead.'
\)
endif
call gina#core#args#extend_treeish(a:git, args, args.pop(1, ':'))
call gina#core#args#extend_line(a:git, args, args.pop('--line'))
if empty(args.params.path)
throw gina#core#revelator#warning(printf(
\ 'No filename is specified. Did you mean "Gina blame %s:"?',
\ args.params.rev,
\))
endif
if !(a:range[0] == 1 && a:range[1] == line('$'))
" Apply visual range
call args.set('-L', join(a:range, ','))
endif
call args.pop('--porcelain')
call args.pop('--line-porcelain')
call args.set('--incremental', 1)
call args.set(1, substitute(args.params.rev, '^:0$', '', ''))
call args.residual([args.params.path])
" Check no unknown options are specified
call s:validate(args)
return args.lock()
endfunction
function! s:validate(args) abort
" Remove all known options
let args = a:args.clone()
let options = s:get_options()
for option in values(options._options)
for name in option.names
call args.pop(name)
endfor
endfor
call args.pop('--incremental')
" Get remaining
let unknown = args.get('^-')
if !empty(unknown)
throw gina#core#revelator#error(printf(
\ 'Unknwon options %s has specified',
\ string(unknown)
\))
endif
endfunction
function! s:call(range, args, mods) abort
let git = gina#core#get_or_fail()
let args = s:build_args(git, a:args, a:range)
let mods = gina#util#contain_direction(a:mods)
\ ? 'keepalt ' . a:mods
\ : join(['keepalt', 'rightbelow', a:mods])
let group = s:Group.new({
\ 'on_close_fail': function('s:on_close_fail'),
\})
" Content
call s:open(mods, args.params.opener, args.params)
call group.add()
call gina#util#windo('setlocal noscrollbind')
setlocal scrollbind nowrap nofoldenable
augroup gina_command_blame_internal
autocmd! * <buffer>
autocmd WinLeave <buffer> call s:WinLeave()
autocmd WinEnter <buffer> call s:WinEnter()
augroup END
" Navi
let bufname = gina#core#buffer#bufname(git, 'blame', {
\ 'treeish': args.params.treeish,
\ 'noautocmd': 1,
\})
call gina#core#buffer#open(bufname, {
\ 'mods': 'leftabove',
\ 'group': args.params.groups[1],
\ 'opener': (args.params.width ? args.params.width : g:gina#command#blame#default_navi_width) . 'vsplit',
\ 'cmdarg': args.params.cmdarg,
\ 'width': args.params.width,
\ 'line': args.params.line,
\ 'callback': {
\ 'fn': function('s:init'),
\ 'args': [args],
\ }
\})
call group.add()
setlocal scrollbind
call gina#util#syncbind()
call gina#core#emitter#emit('command:called', s:SCHEME)
endfunction
function! s:on_close_fail(winnr, member) abort dict
let bufname = bufname(winbufnr(a:winnr))
let abspath = gina#core#repo#abspath(
\ gina#core#get_or_fail(),
\ gina#core#buffer#param(bufname, 'path')
\)
echomsg abspath
if filereadable(abspath)
execute printf('edit %s', fnameescape(abspath))
else
enew
endif
endfunction
function! s:open(mods, opener, params) abort
if s:Opener.is_preview_opener(a:opener)
throw gina#core#revelator#warning(printf(
\ 'An opener "%s" is not allowed.',
\ a:opener,
\))
endif
let treeish = gina#core#treeish#build(a:params.rev, a:params.path)
execute printf(
\ '%s Gina show %s %s %s %s %s',
\ a:mods,
\ a:params.cmdarg,
\ gina#util#shellescape(a:opener, '--opener='),
\ gina#util#shellescape(a:params.groups[0], '--group='),
\ gina#util#shellescape(a:params.line, '--line='),
\ gina#util#shellescape(treeish),
\)
endfunction
function! s:init(args) abort
call gina#core#meta#set('args', a:args)
if exists('b:gina_initialized')
return
endif
let b:gina_initialized = 1
setlocal buftype=nofile
setlocal bufhidden=wipe
setlocal noswapfile
setlocal nomodifiable
" While navi must be vertical, set winfixwidth
setlocal winfixwidth
" Attach modules
call gina#action#attach(function('s:get_candidates'))
" Mapping
nnoremap <buffer><silent> <Plug>(gina-blame-redraw)
\ :<C-u>call <SID>redraw_content()<CR>
nnoremap <buffer><silent> <Plug>(gina-blame-C-L)
\ :<C-u>call <SID>redraw_content()<CR>:execute "normal! \<C-L>"<CR>
augroup gina_command_blame_internal
autocmd! * <buffer>
autocmd WinLeave <buffer> call s:redraw_content_if_necessary()
autocmd WinEnter <buffer> call s:redraw_content_if_necessary()
autocmd VimResized <buffer> call s:redraw_content_if_necessary()
autocmd WinLeave <buffer> call s:WinLeave()
autocmd WinEnter <buffer> call s:WinEnter()
autocmd BufReadCmd <buffer>
\ call gina#core#revelator#call(function('s:BufReadCmd'), [])
augroup END
endfunction
function! s:WinLeave() abort
let git = gina#core#get_or_fail()
let params = gina#core#buffer#parse('%')
if params.scheme ==# 'blame'
let alternate = gina#core#buffer#bufname(git, 'show', {
\ 'params': params.params,
\ 'treeish': params.treeish,
\})
else
let alternate = gina#core#buffer#bufname(git, 'blame', {
\ 'params': params.params,
\ 'treeish': params.treeish,
\})
endif
if bufwinnr(alternate) != -1
call setbufvar(alternate, 'gina_syncbind_line', line('.'))
endif
endfunction
function! s:WinEnter() abort
if exists('b:gina_syncbind_line')
call setpos('.', [0, b:gina_syncbind_line, col('.'), 0])
unlet b:gina_syncbind_line
endif
syncbind
endfunction
function! s:BufReadCmd() abort
let git = gina#core#get_or_fail()
let args = gina#core#meta#get_or_fail('args')
let pipe = gina#command#blame#pipe#incremental()
let job = gina#process#open(git, args, pipe)
let status = job.wait()
if status
throw gina#process#errormsg({
\ 'args': job.args,
\ 'status': status,
\ 'content': pipe._stderr,
\})
endif
call gina#core#meta#set('chunks', job.chunks)
call gina#core#meta#set('revisions', job.revisions)
call s:redraw_content()
call gina#util#syncbind()
setlocal filetype=gina-blame
endfunction
function! s:redraw_content() abort
let args = gina#core#meta#get_or_fail('args')
let chunks = gina#core#meta#get_or_fail('chunks')
let revisions = gina#core#meta#get_or_fail('revisions')
let formatter = gina#command#blame#formatter#new(
\ gina#util#winwidth(0),
\ args.params.rev,
\ revisions,
\ {
\ 'format': args.params.format,
\ }
\)
let content = []
call map(copy(chunks), 'extend(content, formatter.format(v:val))')
call gina#core#writer#replace('%', 0, -1, content)
call gina#util#syncbind()
let b:gina_previous_winwidth = winwidth(0)
endfunction
function! s:redraw_content_if_necessary() abort
if exists('b:gina_previous_winwidth') && b:gina_previous_winwidth != winwidth(0)
call s:redraw_content()
endif
endfunction
function! s:get_candidates(fline, lline) abort
let args = gina#core#meta#get_or_fail('args')
let chunks = gina#core#meta#get_or_fail('chunks')
if empty(chunks)
return []
endif
let revisions = gina#core#meta#get_or_fail('revisions')
let fidx = s:binary_search(chunks, a:fline, 0, len(chunks) - 1)
let lidx = s:binary_search(chunks, a:lline, 0, len(chunks) - 1)
let candidates = map(
\ range(fidx, lidx),
\ 's:translate_candidate(args.params.rev, chunks[v:val], revisions)'
\)
return candidates
endfunction
function! s:binary_search(chunks, lnum, imin, imax) abort
if a:imax < a:imin
return v:null
endif
let imid = a:imin + (a:imax - a:imin) / 2
let chunk = a:chunks[imid]
if chunk.lnum > a:lnum
return s:binary_search(a:chunks, a:lnum, a:imin, imid - 1)
elseif (chunk.lnum + chunk.nlines - 1) < a:lnum
return s:binary_search(a:chunks, a:lnum, imid + 1, a:imax)
else
return imid
endif
endfunction
function! s:translate_candidate(rev, chunk, revisions) abort
let chunk = copy(a:chunk)
call extend(chunk, s:Dict.omit(a:revisions[chunk.revision], [
\ 'lnum_from', 'lnum', 'nlines',
\]))
let rev = chunk.revision
let path = chunk.filename
let line = chunk.lnum_from
return extend(chunk, {
\ 'rev': rev,
\ 'path': path,
\ 'line': line,
\})
endfunction
" Config ---------------------------------------------------------------------
call gina#config(expand('<sfile>'), {
\ 'use_default_aliases': 1,
\ 'use_default_mappings': 1,
\ 'writer_threshold': 0,
\ 'default_navi_width': 50,
\})

View File

@ -0,0 +1,127 @@
let s:String = vital#gina#import('Data.String')
let s:Formatter = vital#gina#import('Data.String.Formatter')
let s:N_COLORS = 16
let s:FORMAT_MAP = {
\ 'su': 'summary',
\ 'au': 'author',
\ 'ma': 'mark',
\ 'in': 'index',
\ 'ti': 'timestamp',
\}
function! gina#command#blame#formatter#new(width, current, revisions, ...) abort
let options = extend({
\ 'format': v:null,
\ 'separator': v:null,
\ 'current_mark': v:null,
\ 'timestamp_months': v:null,
\ 'timestamp_format1': v:null,
\ 'timestamp_format2': v:null,
\}, get(a:000, 0, {})
\)
let formatter = deepcopy(s:formatter)
let formatter._cache = {}
let formatter._width = a:width
let formatter._current = empty(a:current) ? 'X' : a:current
let formatter._revisions = s:index_revisions(a:revisions)
let formatter._previous = 1
let formatter._timestamper = gina#core#timestamper#new({
\ 'months': empty(options.timestamp_months)
\ ? g:gina#command#blame#formatter#timestamp_months
\ : options.timestamp_months,
\ 'format1': empty(options.timestamp_format1)
\ ? g:gina#command#blame#formatter#timestamp_format1
\ : options.timestamp_format1,
\ 'format2': empty(options.timestamp_format2)
\ ? g:gina#command#blame#formatter#timestamp_format2
\ : options.timestamp_format2,
\})
let formatter._current_mark = empty(options.current_mark)
\ ? g:gina#command#blame#formatter#current_mark
\ : options.current_mark
let formatter._separator = empty(options.separator)
\ ? g:gina#command#blame#formatter#separator
\ : options.separator
let formatter._format = empty(options.format)
\ ? g:gina#command#blame#formatter#format
\ : options.format
return formatter
endfunction
" Private --------------------------------------------------------------------
function! s:index_revisions(revisions) abort
let n = s:calc_nindicators(a:revisions)
let revisions = deepcopy(a:revisions)
let keys = keys(revisions)
for index in range(len(revisions))
let revisions[keys[index]].index = s:String.pad_left(
\ s:String.nr2hex(index), n, '0'
\)
endfor
return revisions
endfunction
function! s:calc_nindicators(revisions) abort
let n = len(a:revisions)
let x = 1
while pow(s:N_COLORS, x) < n
let x+= 1
endwhile
return x
endfunction
" Formatter ------------------------------------------------------------------
let s:formatter = {}
function! s:formatter.format(chunk) abort
let revision = a:chunk.revision
let revinfo = self._revisions[revision]
let content = repeat(
\ [self._format_line(a:chunk, revision, revinfo)],
\ a:chunk.nlines,
\)
" Fill missing lines from previous
let mlines = a:chunk.lnum - self._previous
let self._previous = a:chunk.lnum + a:chunk.nlines
return repeat([''], mlines) + content
endfunction
function! s:formatter._format_line(chunk, revision, revinfo) abort
if has_key(self._cache, a:revision)
return self._cache[a:revision]
endif
let precursor = s:Formatter.format(self._format, s:FORMAT_MAP, {
\ 'summary': a:revinfo.summary,
\ 'author': a:revinfo.author,
\ 'index': a:revinfo.index,
\ 'mark': a:revision =~# '^' . self._current
\ ? self._current_mark
\ : repeat(' ', len(self._current_mark)),
\ 'timestamp': self._timestamper.format(
\ a:revinfo.author_time,
\ a:revinfo.author_tz
\ ),
\})
let [head, tail] = split(precursor . '%=', '%=', 1)[0 : 1]
let width = self._width - strwidth(tail) - 1
let head = s:String.truncate_skipping(head, width, 3, self._separator)
let head = s:String.pad_right(head, width)
let self._cache[a:revision] = head . ' ' . tail
return self._cache[a:revision]
endfunction
" Config ---------------------------------------------------------------------
call gina#config(expand('<sfile>'), {
\ 'format': '%su%=on %ti %ma%in',
\ 'separator': '...',
\ 'current_mark': '|',
\ 'timestamp_months': 3,
\ 'timestamp_format1': '%d %b',
\ 'timestamp_format2': '%d %b, %Y',
\})

View File

@ -0,0 +1,107 @@
let s:KEYWORDS = [
\ 'author-mail',
\ 'author-time',
\ 'author-tz',
\ 'author',
\ 'committer-mail',
\ 'committer-time',
\ 'committer-tz',
\ 'committer',
\ 'summary',
\ 'previous',
\ 'filename',
\ 'boundary',
\]
call map(
\ s:KEYWORDS,
\ '[v:val, len(v:val), substitute(v:val, ''-'', ''_'', ''g'')]',
\)
function! gina#command#blame#pipe#incremental() abort
let parser_pipe = deepcopy(s:parser_pipe)
let parser_pipe.revisions = {}
let parser_pipe.chunks = []
let parser_pipe._stdout = ['']
let parser_pipe._stderr = ['']
let parser_pipe._chunk = {}
return parser_pipe
endfunction
" Parser pipe ----------------------------------------------------------------
function! s:_parser_pipe_on_stdout(data) abort dict
let self._stdout[-1] .= a:data[0]
call extend(self._stdout, a:data[1:])
endfunction
function! s:_parser_pipe_on_stderr(data) abort dict
let self._stderr[-1] .= a:data[0]
call extend(self._stderr, a:data[1:])
endfunction
function! s:_parser_pipe_on_exit(exitval) abort dict
call call(s:original_pipe.on_exit, [a:exitval], self)
if a:exitval
throw gina#process#errormsg({
\ 'args': self.args,
\ 'content': self._stderr,
\})
endif
" Parse records to create chunks
call map(filter(self._stdout, '!empty(v:val)'), 's:parse(self, v:val)')
" Sort chunks and assign indices
call sort(self.chunks, { a, b -> a.lnum - b.lnum })
call map(self.chunks, 'extend(v:val, {''index'': v:key})')
endfunction
let s:original_pipe = gina#process#pipe#default()
let s:parser_pipe = extend(deepcopy(s:original_pipe), {
\ 'on_stdout': function('s:_parser_pipe_on_stdout'),
\ 'on_stderr': function('s:_parser_pipe_on_stderr'),
\ 'on_exit': function('s:_parser_pipe_on_exit'),
\})
" Private --------------------------------------------------------
function! s:parse(pipe, record) abort
let chunk = a:pipe._chunk
let revisions = a:pipe.revisions
call extend(chunk, s:parse_record(a:record))
if !has_key(chunk, 'filename')
return
endif
if !has_key(revisions, chunk.revision)
let revisions[chunk.revision] = chunk
let chunk = {
\ 'filename': chunk.filename,
\ 'revision': chunk.revision,
\ 'lnum_from': chunk.lnum_from,
\ 'lnum': chunk.lnum,
\ 'nlines': chunk.nlines,
\}
endif
call add(a:pipe.chunks, chunk)
let a:pipe._chunk = {}
endfunction
function! s:parse_record(record) abort
for [prefix, length, vname] in s:KEYWORDS
if a:record[:length-1] ==# prefix
return {vname : a:record[length+1:]}
endif
endfor
let terms = split(a:record)
let nterms = len(terms)
if nterms >= 3
return {
\ 'revision': terms[0],
\ 'lnum_from': terms[1] + 0,
\ 'lnum': terms[2] + 0,
\ 'nlines': nterms == 3 ? 1 : (terms[3] + 0),
\}
endif
throw gina#core#revelator#critical(printf(
\ 'Failed to parse a record "%s"',
\ a:record,
\))
endfunction

View File

@ -0,0 +1,249 @@
let s:Path = vital#gina#import('System.Filepath')
let s:String = vital#gina#import('Data.String')
let s:SCHEME = gina#command#scheme(expand('<sfile>'))
function! gina#command#branch#call(range, args, mods) abort
call gina#core#options#help_if_necessary(a:args, s:get_options())
if s:is_raw_command(a:args)
" Remove non git options
let args = a:args.clone()
call args.pop('--group')
call args.pop('--opener')
" Call raw git command
return gina#command#_raw#call(a:range, args, a:mods)
endif
let git = gina#core#get_or_fail()
let args = s:build_args(git, a:args)
let bufname = gina#core#buffer#bufname(git, s:SCHEME)
call gina#core#buffer#open(bufname, {
\ 'mods': a:mods,
\ 'group': args.params.group,
\ 'opener': args.params.opener,
\ 'cmdarg': args.params.cmdarg,
\ 'callback': {
\ 'fn': function('s:init'),
\ 'args': [args],
\ }
\})
endfunction
function! gina#command#branch#complete(arglead, cmdline, cursorpos) abort
let args = gina#core#args#new(matchstr(a:cmdline, '^.*\ze .*'))
if a:arglead[0] ==# '-' || !empty(args.get(1))
let options = s:get_options()
return options.complete(a:arglead, a:cmdline, a:cursorpos)
endif
return gina#complete#commit#branch(a:arglead, a:cmdline, a:cursorpos)
endfunction
" Private --------------------------------------------------------------------
function! s:get_options() abort
let options = gina#core#options#new()
call options.define(
\ '-h|--help',
\ 'Show this help.',
\)
call options.define(
\ '--opener=',
\ 'A Vim command to open a new buffer.',
\ ['edit', 'split', 'vsplit', 'tabedit', 'pedit'],
\)
call options.define(
\ '--group=',
\ 'A window group name.',
\)
call options.define(
\ '-d|--delete',
\ 'Delete a branch.',
\)
call options.define(
\ '-D',
\ 'Shortcut for --delete --force.',
\)
call options.define(
\ '-l|--create-reflog',
\ 'Create the branch''s reflog.',
\)
call options.define(
\ '-f|--force',
\ 'Operate forcedly.',
\)
call options.define(
\ '-m|--move',
\ 'Move/rename a branch and the corresponding reflog.',
\)
call options.define(
\ '-M',
\ 'Shortcut for --move --force.',
\)
call options.define(
\ '-i|--ignore-case',
\ 'Sorting and filtering branches are case insensitive.',
\)
call options.define(
\ '-r|--remotes',
\ 'List or delete (if used with -d) the remote-tracking branches.',
\)
call options.define(
\ '-a|--all',
\ 'List both remote-tracking branches and local branches.',
\)
call options.define(
\ '--list',
\ 'Activate the list mode. Mainly for <pattern> match.',
\)
call options.define(
\ '-v|--verbose',
\ 'Show sha1 and commit subject line for each head.'
\)
call options.define(
\ '-q|--quiet',
\ 'Be more quiet when creating or deleting a branch.',
\)
call options.define(
\ '-t|--track',
\ 'Set up a branch.<name>.remote and branch.<name>.merge config.',
\)
call options.define(
\ '--no-track',
\ 'Do not set up "upstream" config.',
\)
call options.define(
\ '--set-upstream',
\ 'Set up a "upstream" as like --track for non existing branch.',
\)
call options.define(
\ '-u|--set-upstream-to=',
\ 'Set up "upstream" to <upstream>.',
\ function('gina#complete#commit#branch'),
\)
call options.define(
\ '--unset-upstream',
\ 'Remove the upstream information.',
\)
call options.define(
\ '--contains=',
\ 'Only list branches which contain the specified commit.',
\ function('gina#complete#commit#any')
\)
call options.define(
\ '--merged=',
\ 'Only list branches whose tips are reachable from the specified commit.',
\ function('gina#complete#commit#any')
\)
call options.define(
\ '--no-merged=',
\ 'Only list branches whose tips are not reachable from the specified commit.',
\ function('gina#complete#commit#any')
\)
return options
endfunction
function! s:build_args(git, args) abort
let args = a:args.clone()
let args.params.group = args.pop('--group', '')
let args.params.opener = args.pop('--opener', '')
call args.set('--color', 'always')
return args.lock()
endfunction
function! s:is_raw_command(args) abort
if a:args.get('--list')
return 0
elseif !empty(a:args.get('-u|--set-upstream-to', ''))
return 1
elseif a:args.get('--unset-upstream')
return 1
elseif a:args.get('-m|--move') || a:args.get('-M')
return 1
elseif a:args.get('-d|--delete') || a:args.get('-D')
return 1
endif
return !empty(a:args.get(1))
endfunction
function! s:init(args) abort
call gina#core#meta#set('args', a:args)
if exists('b:gina_initialized')
return
endif
let b:gina_initialized = 1
setlocal buftype=nofile
setlocal bufhidden=hide
setlocal noswapfile
setlocal nomodifiable
setlocal autoread
" Attach modules
call gina#core#locator#attach()
call gina#action#attach(function('s:get_candidates'), {
\ 'markable': 1,
\})
augroup gina_command_branch_internal
autocmd! * <buffer>
autocmd BufReadCmd <buffer>
\ call gina#core#revelator#call(function('s:BufReadCmd'), [])
augroup END
endfunction
function! s:BufReadCmd() abort
let git = gina#core#get_or_fail()
let args = gina#core#meta#get_or_fail('args')
let pipe = gina#process#pipe#stream(s:writer)
call gina#core#buffer#assign_cmdarg()
call gina#process#open(git, args, pipe)
setlocal filetype=gina-branch
endfunction
function! s:get_candidates(fline, lline) abort
let candidates = map(
\ getline(a:fline, a:lline),
\ 's:parse_record(v:val)'
\)
return filter(candidates, '!empty(v:val)')
endfunction
function! s:parse_record(record) abort
let record = s:String.remove_ansi_sequences(a:record)
let m = matchlist(record, '\(\*\|\s\) \([^ ]\+\)\%( -> \([^ ]\+\)\)\?')
let remote = matchstr(m[2], '^remotes/\zs[^ /]\+')
let rev = matchstr(m[2], '^\%(remotes/\)\?\zs[^ ]\+')
let branch = matchstr(rev, printf('^\%%(%s/\)\?\zs[^ ]\+', remote))
return {
\ 'word': record,
\ 'abbr': a:record,
\ 'sign': m[1],
\ 'alias': m[3],
\ 'remote': remote,
\ 'rev': rev,
\ 'branch': branch,
\}
endfunction
" Writer ---------------------------------------------------------------------
function! s:_writer_on_exit() abort dict
call call(s:original_writer.on_exit, [], self)
call gina#core#emitter#emit('command:called', s:SCHEME)
endfunction
let s:original_writer = gina#process#pipe#stream_writer()
let s:writer = extend(deepcopy(s:original_writer), {
\ 'on_exit': function('s:_writer_on_exit'),
\})
" Config ---------------------------------------------------------------------
call gina#config(expand('<sfile>'), {
\ 'use_default_aliases': 1,
\ 'use_default_mappings': 1,
\})

View File

@ -0,0 +1,217 @@
let s:Formatter = vital#gina#import('Data.String.Formatter')
let s:Git = vital#gina#import('Git')
let s:Path = vital#gina#import('System.Filepath')
let s:SCHEME = gina#command#scheme(expand('<sfile>'))
let s:FORMAT_MAP = {
\ 'pt': 'path',
\ 'ls': 'line_start',
\ 'le': 'line_end',
\ 'c0': 'commit0',
\ 'c1': 'commit1',
\ 'c2': 'commit2',
\ 'h0': 'hash0',
\ 'h1': 'hash1',
\ 'h2': 'hash2',
\ 'r0': 'rev0',
\ 'r1': 'rev1',
\ 'r2': 'rev2',
\}
function! gina#command#browse#call(range, args, mods) abort
call gina#core#options#help_if_necessary(a:args, s:get_options())
call gina#process#register(s:SCHEME, 1)
try
call s:call(a:range, a:args, a:mods)
finally
call gina#process#unregister(s:SCHEME, 1)
endtry
endfunction
function! gina#command#browse#complete(arglead, cmdline, cursorpos) abort
let args = gina#core#args#new(matchstr(a:cmdline, '^.*\ze .*'))
if a:arglead[0] ==# '-' || !empty(args.get(1))
let options = s:get_options()
return options.complete(a:arglead, a:cmdline, a:cursorpos)
endif
return gina#complete#common#treeish(a:arglead, a:cmdline, a:cursorpos)
endfunction
" Private --------------------------------------------------------------------
function! s:get_options() abort
let options = gina#core#options#new()
call options.define(
\ '-h|--help',
\ 'Show this help.',
\)
call options.define(
\ '--scheme=',
\ 'Specify a URL scheme to open.',
\ ['_', 'root', 'blame', 'compare'],
\)
call options.define(
\ '--exact',
\ 'Use a sha1 instead of a branch name.',
\)
call options.define(
\ '--yank',
\ 'Yank a URL instead of opening.',
\)
return options
endfunction
function! s:build_args(git, args, range) abort
let args = a:args.clone()
let args.params.yank = args.pop('--yank')
let args.params.exact = args.pop('--exact')
let args.params.range = a:range == [1, line('$')] ? [] : a:range
let args.params.scheme = args.pop('--scheme', v:null)
call gina#core#args#extend_treeish(a:git, args, args.pop(1))
return args.lock()
endfunction
function! s:call(range, args, mods) abort
let git = gina#core#get_or_fail()
let args = s:build_args(git, a:args, a:range)
let rev = gina#util#get(args.params, 'rev')
let path = gina#util#get(args.params, 'path')
let revinfo = s:parse_rev(git, rev)
let base_url = s:build_base_url(
\ s:get_remote_url(git, revinfo.commit1, revinfo.commit2),
\ args.params.scheme is# v:null
\ ? empty(path) ? 'root' : '_'
\ : args.params.scheme,
\)
let url = s:Formatter.format(base_url, s:FORMAT_MAP, {
\ 'path': substitute(path, ' ', '%20', 'g'),
\ 'line_start': get(args.params.range, 0, ''),
\ 'line_end': get(args.params.range, 1, ''),
\ 'commit0': revinfo.commit0,
\ 'commit1': revinfo.commit1,
\ 'commit2': revinfo.commit2,
\ 'hash0': revinfo.hash0,
\ 'hash1': revinfo.hash1,
\ 'hash2': revinfo.hash2,
\ 'rev0': args.params.exact ? revinfo.hash0 : revinfo.commit0,
\ 'rev1': args.params.exact ? revinfo.hash1 : revinfo.commit1,
\ 'rev2': args.params.exact ? revinfo.hash2 : revinfo.commit2,
\})
if empty(url)
throw gina#core#revelator#warning(printf(
\ 'No url translation pattern for "%s" is found.',
\ rev,
\))
endif
if args.params.yank
call gina#util#yank(url)
else
call gina#util#open(url)
endif
call gina#core#emitter#emit('command:called', s:SCHEME)
endfunction
function! s:parse_rev(git, rev) abort
let [commit1, commit2] = gina#core#treeish#split(a:rev)
let commit0 = empty(a:rev) ? 'HEAD' : a:rev
let commit1 = empty(commit1) ? 'HEAD' : commit1
let commit2 = empty(commit2) ? 'HEAD' : commit2
let hash0 = gina#core#treeish#sha1(a:git, commit0)
let hash1 = gina#core#treeish#sha1(a:git, commit1)
let hash2 = gina#core#treeish#sha1(a:git, commit2)
return {
\ 'commit0': gina#core#treeish#resolve(a:git, commit0, 1),
\ 'commit1': gina#core#treeish#resolve(a:git, commit1, 1),
\ 'commit2': gina#core#treeish#resolve(a:git, commit2, 1),
\ 'hash0': hash0,
\ 'hash1': hash1,
\ 'hash2': hash2,
\}
endfunction
function! s:get_remote_url(git, commit1, commit2) abort
let config = gina#core#repo#config(a:git)
" Find a corresponding 'remote'
let candidates = [a:commit1, a:commit2, 'master']
for candidate in candidates
let remote_name = get(config, printf('branch.%s.remote', candidate), '')
if !empty(remote_name) && remote_name !=# '.'
break
endif
endfor
let remote_name = empty(remote_name) ? 'origin' : remote_name
let result = gina#process#call(a:git, ['remote', 'get-url', remote_name])
if result.status
throw gina#process#errormsg(result)
endif
return result.content[0]
endfunction
function! s:build_base_url(remote_url, scheme) abort
for [domain, info] in items(g:gina#command#browse#translation_patterns)
for pattern in info[0]
let pattern = substitute(pattern, '\C' . '%domain', domain, 'g')
if a:remote_url =~# pattern
let repl = get(info[1], a:scheme, a:remote_url)
let repl = escape(repl, '&')
return substitute(a:remote_url, '\C' . pattern, repl, 'g')
endif
endfor
endfor
return ''
endfunction
" Config ---------------------------------------------------------------------
call gina#config(expand('<sfile>'), {
\ 'translation_patterns': {
\ 'github\.com': [
\ [
\ '\vhttps?://(%domain)/(.{-})/(.{-})%(\.git)?$',
\ '\vgit://(%domain)/(.{-})/(.{-})%(\.git)?$',
\ '\vgit\@(%domain):(.{-})/(.{-})%(\.git)?$',
\ '\vssh://git\@(%domain)/(.{-})/(.{-})%(\.git)?$',
\ ], {
\ '_': 'https://\1/\2/\3/blob/%r0/%pt%{#L|}ls%{-L|}le',
\ 'root': 'https://\1/\2/\3/tree/%r0/',
\ 'blame': 'https://\1/\2/\3/blame/%r0/%pt%{#L|}ls%{-L|}le',
\ 'compare': 'https://\1/\2/\3/compare/%h1...%h2',
\ },
\ ],
\ 'gitlab\.com': [
\ [
\ '\vhttps?://(%domain)/(.{-})/(.{-})%(\.git)?$',
\ '\vgit://(%domain)/(.{-})/(.{-})%(\.git)?$',
\ '\vgit\@(%domain):(.{-})/(.{-})%(\.git)?$',
\ '\vssh://git\@(%domain)/(.{-})/(.{-})%(\.git)?$',
\ ], {
\ '_': 'https://\1/\2/\3/blob/%r0/%pt%{#L|}ls%{-L|}le',
\ 'root': 'https://\1/\2/\3/tree/%r0/',
\ 'blame': 'https://\1/\2/\3/blame/%r0/%pt%{#L|}ls%{-L|}le',
\ 'compare': 'https://\1/\2/\3/compare/%h1...%h2',
\ },
\ ],
\ 'bitbucket\.org': [
\ [
\ '\vhttps?://(%domain)/(.{-})/(.{-})%(\.git)?$',
\ '\vgit://(%domain)/(.{-})/(.{-})%(\.git)?$',
\ '\vgit\@(%domain):(.{-})/(.{-})%(\.git)?$',
\ '\vssh://git\@(%domain)/(.{-})/(.{-})%(\.git)?$',
\ ], {
\ '_': 'https://\1/\2/\3/src/%r0/%pt%{#cl-|}ls',
\ 'root': 'https://\1/\2/\3/commits/%r0',
\ 'blame': 'https://\1/\2/\3/annotate/%r0/%pt',
\ 'compare': 'https://\1/\2/\3/diff/%pt?diff1=%h1&diff2=%h2',
\ },
\ ],
\ '.*\.visualstudio\.com': [
\ [
\ '\vhttps?://(%domain)/(.{-})/_git/(.{-})$',
\ ], {
\ '_': 'https://\1/\2/_git/\3/?path=%pt&version=GB%r0%{&line=|}ls%{&lineEnd=|}le',
\ 'root': 'https://\1/\2/_git/\3/?version=GB%r0',
\ },
\ ],
\ },
\})

View File

@ -0,0 +1,56 @@
let s:Path = vital#gina#import('System.Filepath')
let s:SCHEME = gina#command#scheme(expand('<sfile>'))
function! gina#command#cd#call(range, args, mods) abort
call gina#core#options#help_if_necessary(a:args, s:get_options())
call gina#process#register(s:SCHEME, 1)
try
call s:call(a:range, a:args, a:mods)
finally
call gina#process#unregister(s:SCHEME, 1)
endtry
endfunction
function! gina#command#cd#complete(arglead, cmdline, cursorpos) abort
let args = gina#core#args#new(matchstr(a:cmdline, '^.*\ze .*'))
if a:arglead[0] ==# '-' || !empty(args.get(1))
let options = s:get_options()
return options.complete(a:arglead, a:cmdline, a:cursorpos)
endif
return gina#complete#filename#directory(a:arglead, a:cmdline, a:cursorpos)
endfunction
" Private --------------------------------------------------------------------
function! s:get_options() abort
let options = gina#core#options#new()
call options.define(
\ '-h|--help',
\ 'Show this help.',
\)
call options.define(
\ '--local',
\ 'Use "lcd" command instead of "cd" command.',
\)
return options
endfunction
function! s:build_args(git, args) abort
let args = a:args.clone()
let args.params.local = args.get('--local')
call gina#core#args#extend_path(a:git, args, args.pop(1, v:null))
return args.lock()
endfunction
function! s:call(range, args, mods) abort
let git = gina#core#get_or_fail()
let args = s:build_args(git, a:args)
let path = gina#util#get(args.params, 'path', '')
let abspath = gina#core#repo#abspath(git, path)
let command = args.params.local ? 'lcd' : 'cd'
execute command gina#util#fnameescape(s:Path.realpath(abspath))
call gina#core#emitter#emit('command:called', s:SCHEME)
endfunction

View File

@ -0,0 +1,166 @@
let s:SCHEME = gina#command#scheme(expand('<sfile>'))
function! gina#command#changes#call(range, args, mods) abort
call gina#core#options#help_if_necessary(a:args, s:get_options())
let git = gina#core#get_or_fail()
let args = s:build_args(git, a:args)
let bufname = gina#core#buffer#bufname(git, s:SCHEME, {
\ 'rev': args.params.rev,
\ 'params': [
\ args.params.cached ? 'cached' : '',
\ args.params.partial ? '--' : '',
\ ],
\})
call gina#core#buffer#open(bufname, {
\ 'mods': a:mods,
\ 'group': args.params.group,
\ 'opener': args.params.opener,
\ 'cmdarg': args.params.cmdarg,
\ 'callback': {
\ 'fn': function('s:init'),
\ 'args': [args],
\ }
\})
endfunction
function! gina#command#changes#complete(arglead, cmdline, cursorpos) abort
let args = gina#core#args#new(matchstr(a:cmdline, '^.*\ze .*'))
if a:cmdline =~# '\s--\s'
return gina#complete#filename#any(a:arglead, a:cmdline, a:cursorpos)
elseif a:arglead[0] ==# '-' || !empty(args.get(1))
let options = s:get_options()
return options.complete(a:arglead, a:cmdline, a:cursorpos)
endif
return gina#complete#range#any(a:arglead, a:cmdline, a:cursorpos)
endfunction
" Private --------------------------------------------------------------------
function! s:get_options() abort
let options = gina#core#options#new()
call options.define(
\ '-h|--help',
\ 'Show this help.',
\)
call options.define(
\ '--group=',
\ 'A window group name used for a buffer.',
\)
call options.define(
\ '--opener=',
\ 'A Vim command to open a new buffer.',
\ ['edit', 'split', 'vsplit', 'tabedit', 'pedit'],
\)
call options.define(
\ '--cached',
\ 'Compare changes to the index instead of the working tree.',
\)
return options
endfunction
function! s:build_args(git, args) abort
let args = a:args.clone()
let args.params.group = args.pop('--group', '')
let args.params.opener = args.pop('--opener', '')
let args.params.cached = args.get('--cached')
let args.params.partial = !empty(args.residual())
let args.params.rev = args.get(1, gina#core#buffer#param('%', 'rev'))
" Use {rev}^..{rev} instead
" https://github.com/lambdalisue/gina.vim/issues/147
if !empty(args.params.rev) && args.params.rev !~# '^.\{-}\.\.\.\?.*$'
let args.params.rev = printf(
\ '%s^..%s',
\ args.params.rev,
\ args.params.rev,
\)
endif
" NOTE:
" --stat is more human friendly but --stat with long filename truncate the
" filename so could not be used.
call args.set('--numstat', 1)
call args.set(0, 'diff')
call args.set(1, args.params.rev)
return args.lock()
endfunction
function! s:init(args) abort
call gina#core#meta#set('args', a:args)
if exists('b:gina_initialized')
return
endif
let b:gina_initialized = 1
setlocal buftype=nofile
setlocal bufhidden=hide
setlocal noswapfile
setlocal nomodifiable
" Attach modules
call gina#core#locator#attach()
call gina#action#attach(function('s:get_candidates'))
augroup gina_command_changes_internal
autocmd! * <buffer>
autocmd BufReadCmd <buffer>
\ call gina#core#revelator#call(function('s:BufReadCmd'), [])
augroup END
endfunction
function! s:BufReadCmd() abort
let git = gina#core#get_or_fail()
let args = gina#core#meta#get_or_fail('args')
let pipe = gina#process#pipe#stream(s:writer)
call gina#core#buffer#assign_cmdarg()
call gina#process#open(git, args, pipe)
setlocal filetype=gina-changes
endfunction
function! s:get_candidates(fline, lline) abort
let args = gina#core#meta#get_or_fail('args')
let rev = args.params.rev
let residual = args.residual()
let candidates = map(
\ getline(a:fline, a:lline),
\ 's:parse_record(v:val, rev, residual)'
\)
return filter(candidates, '!empty(v:val)')
endfunction
function! s:parse_record(record, rev, residual) abort
let m = matchlist(
\ a:record,
\ '^\(\d\+\)\s\+\(\d\+\)\s\+\(.\+\)$'
\)
return empty(m) ? {} : {
\ 'word': a:record,
\ 'added': m[1],
\ 'removed': m[2],
\ 'path': m[3],
\ 'rev': a:rev,
\ 'residual': a:residual,
\}
endfunction
" Writer ---------------------------------------------------------------------
function! s:_writer_on_exit() abort dict
call call(s:original_writer.on_exit, [], self)
call gina#core#emitter#emit('command:called', s:SCHEME)
endfunction
let s:original_writer = gina#process#pipe#stream_writer()
let s:writer = extend(deepcopy(s:original_writer), {
\ 'on_exit': function('s:_writer_on_exit'),
\})
" Config ---------------------------------------------------------------------
call gina#config(expand('<sfile>'), {
\ 'use_default_aliases': 1,
\ 'use_default_mappings': 1,
\})

View File

@ -0,0 +1,196 @@
let s:String = vital#gina#import('Data.String')
let s:SCHEME = gina#command#scheme(expand('<sfile>'))
let s:WORKTREE = '@@'
let s:REGION_PATTERN = printf('%s.\{-}%s\r\?\n\?',
\ printf('%s[^\n]\{-}\%%(\n\|$\)', repeat('<', 7)),
\ printf('%s[^\n]\{-}\%%(\n\|$\)', repeat('>', 7))
\)
function! gina#command#chaperon#call(range, args, mods) abort
call gina#core#options#help_if_necessary(a:args, s:get_options())
call gina#process#register(s:SCHEME, 1)
try
call s:call(a:range, a:args, a:mods)
finally
call gina#process#unregister(s:SCHEME, 1)
endtry
endfunction
function! gina#command#chaperon#complete(arglead, cmdline, cursorpos) abort
let args = gina#core#args#new(matchstr(a:cmdline, '^.*\ze .*'))
if a:arglead[0] ==# '-' || !empty(args.get(1))
let options = s:get_options()
return options.complete(a:arglead, a:cmdline, a:cursorpos)
endif
return gina#complete#filename#conflicted(a:arglead, a:cmdline, a:cursorpos)
endfunction
" Private --------------------------------------------------------------------
function! s:get_options() abort
let options = gina#core#options#new()
call options.define(
\ '-h|--help',
\ 'Show this help.',
\)
call options.define(
\ '--opener=',
\ 'A Vim command to open a new buffer.',
\ ['edit', 'split', 'vsplit', 'tabedit', 'pedit'],
\)
call options.define(
\ '--group1=',
\ 'A window group name used for a LOCAL (our) buffer.',
\)
call options.define(
\ '--group2=',
\ 'A window group name used for a MERGE (working tree) buffer.',
\)
call options.define(
\ '--group3=',
\ 'A window group name used for a REMOTE (theirs) buffer.',
\)
call options.define(
\ '--line=',
\ 'An initial line number.',
\)
call options.define(
\ '--col=',
\ 'An initial column number.',
\)
call options.define(
\ '--diffoff',
\ 'Call diffoff! prior to open buffers.',
\)
return options
endfunction
function! s:build_args(git, args) abort
let args = a:args.clone()
let args.params.groups = [
\ args.pop('--group1', ''),
\ args.pop('--group2', ''),
\ args.pop('--group3', ''),
\]
let args.params.opener = args.pop('--opener', 'tabnew')
let args.params.diffoff = args.pop('--diffoff')
call gina#core#args#extend_path(a:git, args, args.pop(1))
call gina#core#args#extend_line(a:git, args, args.pop('--line'))
call gina#core#args#extend_col(a:git, args, args.pop('--col'))
return args.lock()
endfunction
function! s:call(range, args, mods) abort
let git = gina#core#get_or_fail()
let args = s:build_args(git, a:args)
let mods = gina#util#contain_direction(a:mods)
\ ? 'keepalt ' . a:mods
\ : join(['keepalt', 'rightbelow', a:mods])
let opener1 = args.params.opener
let opener2 = empty(matchstr(&diffopt, 'vertical'))
\ ? 'split'
\ : 'vsplit'
if !empty(args.params.diffoff)
diffoff!
endif
call s:open(0, mods, opener1, ':2', args.params)
let bufnr1 = bufnr('%')
call s:open(1, mods, opener2, s:WORKTREE, args.params)
let bufnr2 = bufnr('%')
call s:open(2, mods, opener2, ':3', args.params)
let bufnr3 = bufnr('%')
" :3 Theirs (REMOTE)
call gina#util#diffthis()
call s:define_plug_mapping('diffput', bufnr2)
if g:gina#command#chaperon#use_default_mappings
nmap dp <Plug>(gina-diffput)
endif
" :2 Ours (Local)
execute printf('%dwincmd w', bufwinnr(bufnr1))
call gina#util#diffthis()
call s:define_plug_mapping('diffput', bufnr2)
if g:gina#command#chaperon#use_default_mappings
nmap dp <Plug>(gina-diffput)
endif
" WORKTREE
execute printf('%dwincmd w', bufwinnr(bufnr2))
call gina#util#diffthis()
call s:define_plug_mapping('diffget', bufnr1, '-l')
call s:define_plug_mapping('diffget', bufnr3, '-r')
if g:gina#command#chaperon#use_default_mappings
nmap dol <Plug>(gina-diffget-l)
nmap dor <Plug>(gina-diffget-r)
endif
if !&l:modified
let content = s:strip_conflict(getline(1, '$'))
silent keepjumps %delete _
call setline(1, content)
setlocal modified
endif
call gina#util#diffupdate()
normal! zm
call gina#core#emitter#emit('command:called', s:SCHEME)
endfunction
function! s:define_plug_mapping(command, bufnr, ...) abort
let suffix = a:0 ? a:1 : ''
let lhs = printf('<Plug>(gina-%s%s)', a:command, suffix)
let rhs = printf(':<C-u>%s %d<CR>:diffupdate<CR>', a:command, a:bufnr)
call gina#util#map(lhs, rhs, {
\ 'mode': 'n',
\ 'noremap': 1,
\ 'silent': 1,
\})
endfunction
function! s:open(n, mods, opener, commit, params) abort
if a:commit ==# s:WORKTREE
execute printf(
\ '%s Gina edit %s %s %s %s %s %s',
\ a:mods,
\ a:params.cmdarg,
\ gina#util#shellescape(a:opener, '--opener='),
\ gina#util#shellescape(a:params.groups[a:n], '--group='),
\ gina#util#shellescape(a:params.line, '--line='),
\ gina#util#shellescape(a:params.col, '--col='),
\ gina#util#shellescape(a:params.path),
\)
else
let treeish = gina#core#treeish#build(a:commit, a:params.path)
execute printf(
\ '%s Gina show %s %s %s %s %s %s',
\ a:mods,
\ a:params.cmdarg,
\ gina#util#shellescape(a:opener, '--opener='),
\ gina#util#shellescape(a:params.groups[a:n], '--group='),
\ gina#util#shellescape(a:params.line, '--line='),
\ gina#util#shellescape(a:params.col, '--col='),
\ gina#util#shellescape(treeish),
\)
endif
endfunction
function! s:strip_conflict(content) abort
let ff = &fileformat
let newline = ff ==# 'unix' ? "\n" : ff ==# 'dos' ? "\r\n" : "\r"
let text = s:String.join_posix_lines(a:content, "\n")
let text = substitute(text, s:REGION_PATTERN, '', 'g')
return s:String.split_posix_text(text, newline)
endfunction
" Config ---------------------------------------------------------------------
call gina#config(expand('<sfile>'), {
\ 'use_default_mappings': 1,
\})

View File

@ -0,0 +1,524 @@
let s:String = vital#gina#import('Data.String')
let s:Git = vital#gina#import('Git')
let s:SCHEME = gina#command#scheme(expand('<sfile>'))
let s:messages = {}
function! gina#command#commit#call(range, args, mods) abort
call gina#core#options#help_if_necessary(a:args, s:get_options())
if s:is_raw_command(a:args)
" Remove non git options
let args = a:args.clone()
call args.pop('--group')
call args.pop('--opener')
" Call raw git command
return gina#command#_raw#call(a:range, args, a:mods)
endif
let git = gina#core#get_or_fail()
let args = s:build_args(git, a:args)
let bufname = gina#core#buffer#bufname(git, s:SCHEME)
call gina#core#buffer#open(bufname, {
\ 'mods': a:mods,
\ 'group': args.params.group,
\ 'opener': args.params.opener,
\ 'cmdarg': args.params.cmdarg,
\ 'callback': {
\ 'fn': function('s:init'),
\ 'args': [args],
\ }
\})
endfunction
function! gina#command#commit#complete(arglead, cmdline, cursorpos) abort
let args = gina#core#args#new(matchstr(a:cmdline, '^.*\ze .*'))
if a:arglead =~# '^\%(-C\|--reuse-message=\|-c\|--reedit-message=\|--fixup=\|--squash=\)'
let leading = matchstr(
\ a:arglead,
\ '^\%(-C\|--reuse-message=\|-c\|--reedit-message\|--fixup=\|--squash=\)'
\)
let candidates = gina#complete#commit#any(
\ matchstr(a:arglead, '^' . leading . '\zs.*'),
\ a:cmdline, a:cursorpos,
\)
return map(candidates, 'leading . v:val')
elseif a:arglead[0] ==# '-' || !empty(args.get(1))
let options = s:get_options()
return options.complete(a:arglead, a:cmdline, a:cursorpos)
elseif a:cmdline !~# '\s--\s'
return gina#complete#filename#any(a:arglead, a:cmdline, a:cursorpos)
endif
return gina#complete#filename#tracked(a:arglead, a:cmdline, a:cursorpos)
endfunction
" Private --------------------------------------------------------------------
function! s:get_options() abort
let options = gina#core#options#new()
call options.define(
\ '-h|--help',
\ 'Show this help.',
\)
call options.define(
\ '--opener=',
\ 'A Vim command to open a new buffer.',
\ ['edit', 'split', 'vsplit', 'tabedit', 'pedit'],
\)
call options.define(
\ '--group=',
\ 'A window group name used for a buffer.',
\)
call options.define(
\ '--restore',
\ 'Restore the previous buffer when the window is closed.',
\)
call options.define(
\ '-a|--all',
\ 'Commit all changed files',
\)
call options.define(
\ '-C|--reuse-message=',
\ 'Reuse message from specified commit',
\)
call options.define(
\ '-c|--reedit-message=',
\ 'Reuse and edit message from specified commit',
\)
call options.define(
\ '--fixup=',
\ 'Use autosquash formatted message to fixup specified commit',
\)
call options.define(
\ '--squash=',
\ 'Use autosquash formatted message to squash specified commit',
\)
call options.define(
\ '--reset-author',
\ 'The commit is authored by me now (used with -C/-c/--amend)',
\)
call options.define(
\ '-F|--file=',
\ 'Read message from file',
\)
call options.define(
\ '--author=',
\ 'Override the commit author',
\)
call options.define(
\ '--date=',
\ 'Override the author date used in the commit',
\)
call options.define(
\ '-m|--message=',
\ 'Use the given message as the commit message',
\)
call options.define(
\ '-t|--template=',
\ 'Read message from file and use it as a template',
\)
call options.define(
\ '-s|--signoff',
\ 'Add Signed-off-by:',
\)
call options.define(
\ '--allow-empty',
\ 'Allow empty commit',
\)
call options.define(
\ '--allow-empty-message',
\ 'Allow empty commit message',
\)
call options.define(
\ '--cleanup=',
\ 'How to strip spaces and #comments from message',
\ ['strip', 'whitespace', 'verbatim', 'scissors', 'default'],
\)
call options.define(
\ '-e|--edit',
\ 'Force edit of commit',
\)
call options.define(
\ '--no-edit',
\ 'Use the selected commit message without editing',
\)
call options.define(
\ '--amend',
\ 'Replace the tip of the current branch by creating a new commit',
\)
call options.define(
\ '-i|--include',
\ 'Add specified files to index for commit',
\)
call options.define(
\ '-o|--only',
\ 'Commit only specified files',
\)
call options.define(
\ '-u|--untracked-files=',
\ 'Show untracked files, optional modes: all, normal, no (Default: all)',
\ ['no', 'normal', 'all'],
\)
call options.define(
\ '-v|--verbose',
\ 'Show unified diff between the HEAD commit and what would be committed',
\)
call options.define(
\ '-q|--quiet',
\ 'Suppress commit summary message',
\)
call options.define(
\ '--dry-run',
\ 'Do not create a commit, but show a list of paths that are to be committed',
\)
call options.define(
\ '--status',
\ 'Include status in commit message template',
\)
call options.define(
\ '--no-status',
\ 'Do not include status in commit message template',
\)
call options.define(
\ '-S|--gpg-sign',
\ 'GPG sign commit',
\)
call options.define(
\ '--no-gpg-sign',
\ 'Do not GPG sign commit',
\)
return options
endfunction
function! s:build_args(git, args) abort
let args = a:args.clone()
let args.params.group = args.pop('--group', '')
let args.params.opener = args.pop('--opener', '')
let args.params.restore = args.pop(
\ '--restore',
\ empty(args.params.opener) || args.params.opener ==# 'edit',
\)
let args.params.amend = args.get('--amend')
call gina#core#args#extend_diff(a:git, args, '')
return args.lock()
endfunction
function! s:is_raw_command(args) abort
if a:args.get('-e|--edit')
return 0
elseif a:args.get('--no-edit')
return 1
elseif a:args.get('--dry-run')
return 1
elseif !empty(a:args.get('-C|--reuse-message', ''))
return 1
elseif !empty(a:args.get('-c|--reedit-message', ''))
return 0
elseif a:args.get('-F|--file')
return 1
elseif !empty(a:args.get('-m|--message', ''))
return 1
elseif a:args.get('-t|--template')
return 0
elseif !empty(a:args.get('--fixup', ''))
return 1
endif
return 0
endfunction
function! s:init(args) abort
call gina#core#meta#set('args', a:args)
silent! unlet b:gina_QuitPre
silent! unlet b:gina_BufWriteCmd
if exists('b:gina_initialized')
return
endif
let b:gina_initialized = 1
setlocal nobuflisted
setlocal buftype=acwrite
setlocal bufhidden=hide
setlocal noswapfile
setlocal modifiable
augroup gina_command_commit_internal
autocmd! * <buffer>
autocmd BufReadCmd <buffer> call s:BufReadCmd()
autocmd BufWriteCmd <buffer> call s:BufWriteCmd()
autocmd QuitPre <buffer> call s:QuitPre()
autocmd WinLeave <buffer> call s:WinLeave()
autocmd WinEnter <buffer> silent! unlet! b:gina_QuitPre
augroup END
nnoremap <silent><buffer> <Plug>(gina-commit-amend)
\ :<C-u>call <SID>toggle_amend()<CR>
if a:args.get('-v|--verbose')
nnoremap <buffer><silent> <Plug>(gina-diff-jump)
\ :<C-u>call gina#core#diffjump#jump()<CR>
nnoremap <buffer><silent> <Plug>(gina-diff-jump-split)
\ :<C-u>call gina#core#diffjump#jump('split')<CR>
nnoremap <buffer><silent> <Plug>(gina-diff-jump-vsplit)
\ :<C-u>call gina#core#diffjump#jump('vsplit')<CR>
if g:gina#command#commit#use_default_mappings
nmap <buffer> <CR> <Plug>(gina-diff-jump)
endif
endif
endfunction
function! s:BufReadCmd() abort
let git = gina#core#get_or_fail()
let args = gina#core#meta#get_or_fail('args')
if v:cmdbang
let content = s:get_commitmsg_template(git, args)
else
let content = s:get_commitmsg(git, args)
endif
call gina#core#buffer#assign_cmdarg()
call gina#core#writer#replace('%', 0, -1, content)
call gina#core#emitter#emit('command:called', s:SCHEME)
setlocal filetype=gina-commit
endfunction
function! s:BufWriteCmd() abort
let b:gina_BufWriteCmd = 1
let git = gina#core#get_or_fail()
let args = gina#core#meta#get_or_fail('args')
call gina#core#revelator#call(
\ function('s:set_commitmsg'),
\ [git, args, getline(1, '$')]
\)
setlocal nomodified
endfunction
function! s:QuitPre() abort
" Restore the previous buffer if 'restore' is specified
let args = gina#core#meta#get('args', v:null)
if args isnot# v:null && get(args.params, 'restore')
let win_id = win_getid()
if !empty(bufname('#')) && bufnr('#') isnot# -1
silent keepalt keepjumps 1split #
else
silent keepalt keepjumps 1new
endif
call win_gotoid(win_id)
endif
" Do not perform commit when user hit :q!
if histget('cmd', -1) !~# '^q\%[uit]!'
let b:gina_QuitPre = 1
" If this is a last window, open a new window to prevent quit
if tabpagenr('$') == 1 && winnr('$') == 1
let win_id = win_getid()
silent keepalt keepjumps 1new
call win_gotoid(win_id)
endif
endif
" Clear the flag set by :w but keep it when user hit :wq
if histget('cmd', -1) !~# '^\(wq\|x\%[it]\|exi\%[t]\)'
silent! unlet b:gina_BufWriteCmd
endif
endfunction
" NOTE:
" :w -- BufWriteCmd
" <C-w>p -- WinLeave
" :wq -- QuitPre -> BufWriteCmd -> WinLeave (-8.2.2899)
" BufWriteCmd -> QuitPre -> WinLeave (8.2.2900-)
" :q -- QuitPre -> WinLeave
function! s:WinLeave() abort
if exists('b:gina_QuitPre')
let git = gina#core#get_or_fail()
let args = gina#core#meta#get_or_fail('args')
if exists('b:gina_BufWriteCmd')
" User execute 'wq' so do not confirm
call gina#core#revelator#call(
\ function('s:commit_commitmsg'),
\ [git, args]
\)
else
" User execute 'q' so confirm if commit message is written
if !empty(s:get_cached_commitmsg(git, args))
call gina#core#revelator#call(
\ function('s:commit_commitmsg_confirm'),
\ [git, args]
\)
else
redraw | echo ''
endif
endif
endif
endfunction
function! s:toggle_amend() abort
let args = gina#core#meta#get_or_fail('args')
let args = args.clone()
if args.get('--amend')
call args.pop('--amend')
else
call args.set('--amend', 1)
endif
call gina#core#meta#set('args', args)
edit
endfunction
function! s:get_cleanup(git, args) abort
let config = gina#core#repo#config(a:git)
if a:args.get('--cleanup')
return a:args.get('--cleanup')
endif
return get(config, 'commit.cleanup', 'strip')
endfunction
function! s:get_commitmsg(git, args) abort
let content = s:get_cached_commitmsg(a:git, a:args)
if empty(content)
return s:get_commitmsg_template(a:git, a:args)
else
call gina#core#console#debug('Use a cached commit message:')
return s:get_commitmsg_cleanedup(a:git, a:args, content)
endif
endfunction
function! s:get_commitmsg_template(git, args) abort
let args = a:args.clone()
let filename = s:Git.resolve(a:git, 'COMMIT_EDITMSG')
let previous_content = filereadable(filename)
\ ? readfile(filename)
\ : []
try
" Build a new commit message template
call args.pop('--no-edit')
call args.set('-e|--edit', 1)
let result = gina#process#call(a:git, args)
if !result.status || (
\ match(result.stderr, 'error: unable to start editor') is# -1 &&
\ match(result.stderr, 'error: There was a problem with the editor') is# -1
\)
" While git is executed with '-c core.editor=false', the command above
" should fail after that create a COMMIT_EDITMSG for the current
" situation
throw gina#process#errormsg(result)
endif
" Get a built commitmsg template
return readfile(filename)
finally
" Restore the content
call writefile(previous_content, filename)
endtry
endfunction
" Note:
" Commit the cached messate temporary to build a correct COMMIT_EDITMSG
" This hacky implementation is required due to the lack of cleanup command.
" https://github.com/lambdalisue/gina.vim/issues/37#issuecomment-281661605
" Note:
" It is not possible to remove diff content when user does
" 1. Gina commit --verbose
" 2. Save content
" 3. Gina commit
" 4. The diff part is cached so shown and no chance to remove that
" This is a bit anoyying but I don't have any way to remove that so I just
" ended up. PRs for this issue is welcome.
" https://github.com/lambdalisue/gina.vim/issues/37#issuecomment-281687325
function! s:get_commitmsg_cleanedup(git, args, content) abort
let args = a:args.clone()
let filename = s:Git.resolve(a:git, 'COMMIT_EDITMSG')
let previous_content = readfile(filename)
let tempfile = tempname()
try
call writefile(a:content, tempfile)
call args.set('--cleanup', s:get_cleanup(a:git, args))
call args.set('-F|--file', tempfile)
call args.set('--no-edit', 1)
call args.set('--allow-empty', 1)
call args.set('--allow-empty-message', 1)
call args.pop('-C|--reuse-message')
call args.pop('-m|--message')
call args.pop('-e|--edit')
call gina#process#call_or_fail(a:git, args)
" Reset the temporary commit and remove all logs
call gina#process#call_or_fail(a:git, ['reset', '--soft', 'HEAD@{1}'])
call gina#process#call_or_fail(a:git, ['reflog', 'delete', 'HEAD@{0}'])
call gina#process#call_or_fail(a:git, ['reflog', 'delete', 'HEAD@{0}'])
" Get entire content of commitmsg
return readfile(filename)
finally
call delete(tempfile)
call writefile(previous_content, filename)
endtry
endfunction
function! s:set_commitmsg(git, args, content) abort
call s:set_cached_commitmsg(a:git, a:args, a:content)
endfunction
function! s:commit_commitmsg(git, args) abort
let args = a:args.clone()
let content = s:get_cached_commitmsg(a:git, args)
let tempfile = tempname()
try
call writefile(content, tempfile)
call args.set('--no-edit', 1)
call args.set('--cleanup', s:get_cleanup(a:git, args))
call args.set('-F|--file', tempfile)
call args.pop('-C|--reuse-message')
call args.pop('-m|--message')
call args.pop('-e|--edit')
let result = gina#process#call(a:git, args)
call gina#process#inform(result)
call s:remove_cached_commitmsg(a:git)
call gina#core#emitter#emit('command:called:commit')
bwipeout
finally
call delete(tempfile)
endtry
endfunction
function! s:commit_commitmsg_confirm(git, args) abort
if gina#core#console#confirm('Do you want to commit changes?', 'y')
call s:commit_commitmsg(a:git, a:args)
else
redraw | echo ''
endif
endfunction
function! s:get_cached_commitmsg(git, args) abort
let wname = a:git.worktree
let cname = a:args.get('--amend') ? 'amend' : '_'
let s:messages[wname] = get(s:messages, wname, {})
return get(s:messages[wname], cname, [])
endfunction
function! s:set_cached_commitmsg(git, args, commitmsg) abort
let wname = a:git.worktree
let cname = a:args.get('--amend') ? 'amend' : '_'
let s:messages[wname] = get(s:messages, wname, {})
let s:messages[wname][cname] = a:commitmsg
endfunction
function! s:remove_cached_commitmsg(git) abort
let cname = a:git.worktree
let s:messages[cname] = {}
endfunction
" Event ----------------------------------------------------------------------
function! s:on_command_called_commit(...) abort
call gina#core#emitter#emit('modified:delay')
endfunction
if !exists('s:subscribed')
let s:subscribed = 1
call gina#core#emitter#subscribe(
\ 'command:called:commit',
\ function('s:on_command_called_commit')
\)
endif
" Config ---------------------------------------------------------------------
call gina#config(expand('<sfile>'), {
\ 'use_default_mappings': 1,
\})

View File

@ -0,0 +1,150 @@
let s:Opener = vital#gina#import('Vim.Buffer.Opener')
let s:SCHEME = gina#command#scheme(expand('<sfile>'))
function! gina#command#compare#call(range, args, mods) abort
call gina#core#options#help_if_necessary(a:args, s:get_options())
call gina#process#register(s:SCHEME, 1)
try
call s:call(a:range, a:args, a:mods)
finally
call gina#process#unregister(s:SCHEME, 1)
endtry
endfunction
function! gina#command#compare#complete(arglead, cmdline, cursorpos) abort
let args = gina#core#args#new(matchstr(a:cmdline, '^.*\ze .*'))
if a:arglead[0] ==# '-'
let options = s:get_options()
return options.complete(a:arglead, a:cmdline, a:cursorpos)
endif
return gina#complete#common#treeish(a:arglead, a:cmdline, a:cursorpos)
endfunction
" Private --------------------------------------------------------------------
function! s:get_options() abort
let options = gina#core#options#new()
call options.define(
\ '-h|--help',
\ 'Show this help.',
\)
call options.define(
\ '--opener=',
\ 'A Vim command to open a new buffer.',
\ ['edit', 'split', 'vsplit', 'tabedit', 'pedit'],
\)
call options.define(
\ '--group1=',
\ 'A window group name used for the 1st buffer.',
\)
call options.define(
\ '--group2=',
\ 'A window group name used for the 2nd buffer.',
\)
call options.define(
\ '--line',
\ 'An initial line number.',
\)
call options.define(
\ '--col',
\ 'An initial column number.',
\)
call options.define(
\ '--cached',
\ 'Compare to the index rather than the working tree',
\)
call options.define(
\ '-R',
\ 'Reverse the buffer order',
\)
call options.define(
\ '--diffoff',
\ 'Call diffoff! prior to open buffers.',
\)
return options
endfunction
function! s:build_args(git, args) abort
let args = a:args.clone()
let args.params.groups = [
\ args.pop('--group1', ''),
\ args.pop('--group2', ''),
\]
let args.params.opener = args.pop('--opener', 'tabnew')
let args.params.cached = args.get('--cached')
let args.params.R = args.get('-R')
let args.params.diffoff = args.pop('--diffoff')
call gina#core#args#extend_treeish(a:git, args, args.pop(1, ':'))
call gina#core#args#extend_diff(a:git, args, args.params.rev)
call gina#core#args#extend_line(a:git, args, args.pop('--line'))
call gina#core#args#extend_col(a:git, args, args.pop('--col'))
if empty(args.params.path)
throw gina#core#revelator#warning(printf(
\ 'No filename is specified. Did you mean "Gina compare %s:"?',
\ args.params.rev,
\))
endif
return args.lock()
endfunction
function! s:call(range, args, mods) abort
let git = gina#core#get_or_fail()
let args = s:build_args(git, a:args)
let mods = gina#util#contain_direction(a:mods)
\ ? 'keepalt ' . a:mods
\ : join(['keepalt', 'rightbelow', a:mods])
if !empty(args.params.diffoff)
diffoff!
endif
let opener1 = args.params.opener
let opener2 = empty(matchstr(&diffopt, 'vertical'))
\ ? 'split'
\ : 'vsplit'
call s:open(0, mods, opener1, args.params.rev1, args.params)
call gina#util#diffthis()
call s:open(1, mods, opener2, args.params.rev2, args.params)
call gina#util#diffthis()
call gina#util#diffupdate()
normal! zm
call gina#core#emitter#emit('command:called', s:SCHEME)
endfunction
function! s:open(n, mods, opener, rev, params) abort
if s:Opener.is_preview_opener(a:opener)
throw gina#core#revelator#warning(printf(
\ 'An opener "%s" is not allowed.',
\ a:opener,
\))
endif
if gina#core#args#is_worktree(a:rev)
execute printf(
\ '%s Gina edit %s %s %s %s %s %s',
\ a:mods,
\ a:params.cmdarg,
\ gina#util#shellescape(a:opener, '--opener='),
\ gina#util#shellescape(a:params.groups[a:n], '--group='),
\ gina#util#shellescape(a:params.line, '--line='),
\ gina#util#shellescape(a:params.col, '--col='),
\ gina#util#shellescape(a:params.path),
\)
else
let treeish = gina#core#treeish#build(a:rev, a:params.path)
execute printf(
\ '%s Gina show %s %s %s %s %s %s',
\ a:mods,
\ a:params.cmdarg,
\ gina#util#shellescape(a:opener, '--opener='),
\ gina#util#shellescape(a:params.groups[a:n], '--group='),
\ gina#util#shellescape(a:params.line, '--line='),
\ gina#util#shellescape(a:params.col, '--col='),
\ gina#util#shellescape(treeish),
\)
endif
endfunction

View File

@ -0,0 +1,407 @@
let s:SCHEME = gina#command#scheme(expand('<sfile>'))
function! gina#command#diff#call(range, args, mods) abort
call gina#core#options#help_if_necessary(a:args, s:get_options())
let git = gina#core#get_or_fail()
let args = s:build_args(git, a:args)
let bufname = gina#core#buffer#bufname(git, s:SCHEME, {
\ 'treeish': args.params.treeish,
\ 'params': [
\ args.params.cached ? 'cached' : '',
\ args.params.R ? 'R' : '',
\ args.params.partial ? '--' : '',
\ ],
\ 'noautocmd': !empty(args.params.path),
\})
call gina#core#buffer#open(bufname, {
\ 'mods': a:mods,
\ 'group': args.params.group,
\ 'opener': args.params.opener,
\ 'cmdarg': args.params.cmdarg,
\ 'callback': {
\ 'fn': function('s:init'),
\ 'args': [args],
\ }
\})
endfunction
function! gina#command#diff#complete(arglead, cmdline, cursorpos) abort
let args = gina#core#args#new(matchstr(a:cmdline, '^.*\ze .*'))
if a:arglead =~# '^--opener='
return gina#complete#common#opener(a:arglead, a:cmdline, a:cursorpos)
elseif a:arglead =~# '^\%(--diff-algorithm=\)'
let leading = matchstr(a:arglead, '^--diff-algorithm=')
return gina#util#filter(a:arglead, map(
\ ['patience', 'minimal', 'histogram', 'myers'],
\ 'leading . v:val'
\))
elseif a:arglead =~# '^\%(--dirstat=\)'
let leading = matchstr(a:arglead, '^--dirstat=')
let dirstat = matchstr(a:arglead, '^--dirstat=\zs\%([^,]\+,\)*[^,]*')
let candidates = filter(
\ ['changes', 'lines', 'files', 'cumulative'],
\ 'dirstat !~# ''\<'' . v:val . ''\>''',
\)
return gina#util#filter(a:arglead, map(
\ candidates, 'leading . dirstat . v:val'
\))
elseif a:arglead =~# '^\%(--submodule=\)'
let leading = matchstr(a:arglead, '^--submodule=')
return gina#util#filter(a:arglead, map(
\ ['short', 'log', 'diff'],
\ 'leading . v:val'
\))
elseif a:arglead =~# '^\%(--diff-filter=\)'
let leading = matchstr(a:arglead, '^--diff-filter=[ACDMRTUXB]*')
return gina#util#filter(a:arglead, map(
\ split('ACDMRTUXB', '\zs'),
\ 'leading . v:val'
\))
elseif a:arglead =~# '^\%(--ignore-submodules=\)'
let leading = matchstr(a:arglead, '^--ignore-submodules=')
return gina#util#filter(a:arglead, map(
\ ['none', 'untracked', 'dirty', 'all'],
\ 'leading . v:val'
\))
elseif a:cmdline =~# '\s--\s'
return gina#complete#filename#any(a:arglead, a:cmdline, a:cursorpos)
elseif a:arglead[0] ==# '-'
let options = s:get_options()
return options.complete(a:arglead, a:cmdline, a:cursorpos)
endif
return gina#complete#common#treeish(a:arglead, a:cmdline, a:cursorpos)
endfunction
" Private --------------------------------------------------------------------
function! s:get_options() abort
let options = gina#core#options#new()
call options.define(
\ '-h|--help',
\ 'Show this help.',
\)
call options.define(
\ '--opener=',
\ 'A Vim command to open a new buffer.',
\ ['edit', 'split', 'vsplit', 'tabedit', 'pedit'],
\)
call options.define(
\ '--cached',
\ 'Compare to the index rather than the working tree',
\)
call options.define(
\ '-U|--unified=',
\ 'Generate diffs with <n> lines of context',
\)
call options.define(
\ '--raw',
\ 'Generate the diff in raw format',
\)
call options.define(
\ '--patch-with-raw',
\ 'Synonym for -p --raw',
\)
call options.define(
\ '--minimal',
\ 'Spend extra time to make sure the smallest possible diff is produced',
\)
call options.define(
\ '--patience',
\ 'Generate a diff using the "patience diff" algorithm',
\)
call options.define(
\ '--histogram',
\ 'Generate a diff using the "histogram diff" algorithm',
\)
call options.define(
\ '--diff-algorithm',
\ 'Choose a diff algorithm',
\)
call options.define(
\ '--stat',
\ 'Generate a diffstat',
\)
call options.define(
\ '--numstat',
\ 'Similar to --stat, but with more machine friendly output',
\)
call options.define(
\ '--shortstat',
\ 'Output only the last line of the --stat format',
\)
call options.define(
\ '--dirstat',
\ 'Output the dirstat numbers',
\)
call options.define(
\ '--summary',
\ 'Output a condensed summary of extended header information',
\)
call options.define(
\ '--patch-with-stat',
\ 'Synonym for -p --stat',
\)
call options.define(
\ '--name-only',
\ 'Show only names of changed files',
\)
call options.define(
\ '--name-status',
\ 'Show only names and status of changed files',
\)
call options.define(
\ '--submodule',
\ 'Specify how differences in submodules are shown',
\)
call options.define(
\ '--submodule',
\ 'Specify how differences in submodules are shown',
\)
call options.define(
\ '--no-renames',
\ 'Turn off rename detection',
\)
call options.define(
\ '--check',
\ 'Warn if changes introduce conflict markers or whitespace errors',
\)
call options.define(
\ '--full-index',
\ 'Instead of the first handful of characters,show the full blob',
\)
call options.define(
\ '--binary',
\ 'In addition to --full-index, output a binary diff',
\)
call options.define(
\ '--abbrev',
\ 'Show only a particular prefix hexadecimal object name',
\)
call options.define(
\ '-B|--break-rewrites',
\ 'Break complete rewrite changes into pair of delete and create',
\)
call options.define(
\ '-M|--find-renames',
\ 'Detect renames. -M<n> to specify the threshold.',
\)
call options.define(
\ '-C|--find-copies',
\ 'Detect copies as well as renames. -C<n> to specify the threshold',
\)
call options.define(
\ '--find-copies-harder',
\ 'Detect copies more harder than -C/--find-copies',
\)
call options.define(
\ '-D|--irreversible-delete',
\ 'Omit the preimage for deletes',
\)
call options.define(
\ '-l',
\ 'Specify the threshold of -M or -C',
\)
call options.define(
\ '--diff-filter=', join([
\ 'Specify the Select only files that are Added(A), Copied(C), ',
\ 'Deleted (D), Modified (M), Renamed (R), ',
\ 'have their type changed (T), are Unmerged (U), ',
\ 'are Unknown (X), or have had their pairing Broken (B)',
\ ])
\)
call options.define(
\ '-S', join([
\ 'Look for differences that change the number of occurrences of ',
\ 'the specified string in a file.'
\ ])
\)
call options.define(
\ '-G', join([
\ 'Look for differences whose patch text contains added/removed ',
\ 'line that match <regex>'
\ ])
\)
call options.define(
\ '--pickaxe-all', join([
\ 'Wnen -S or -G finds a change, show all the changes in that ',
\ 'changeset, not just the files and contain the change'
\ ])
\)
call options.define(
\ '--pickaxe-regex', join([
\ 'Treat the <string> given to -S as an extended POSIX regular ',
\ 'expression to match'
\ ])
\)
call options.define(
\ '-O',
\ 'Output the patch in the order specified in the <orderfile>',
\)
call options.define(
\ '-R',
\ 'Swap two inputs',
\)
call options.define(
\ '--relative',
\ 'Show pathnames relative to',
\)
call options.define(
\ '-a|--text',
\ 'Treat all files as text',
\)
call options.define(
\ '--ignore-space-at-eol',
\ 'Ignore changes in whitespace at EOL',
\)
call options.define(
\ '-b|--ignore-space-change',
\ 'Ignore changes in amount of whitespace',
\)
call options.define(
\ '-w|--ignore-all-space',
\ 'Ignore whitespace when comparing lines',
\)
call options.define(
\ '--ignore-blank-lines',
\ 'Ignore changes whose line are all blank',
\)
call options.define(
\ '--inter-hunk-context=',
\ 'Show the context between diff hunks',
\)
call options.define(
\ '-W|--function-context',
\ 'Show whole surrounding functions of changes',
\)
call options.define(
\ '--ext-diff',
\ 'Allow an external diff helper to be executed',
\)
call options.define(
\ '--no-ext-diff',
\ 'Disallow external diff drivers',
\)
call options.define(
\ '--textconv',
\ 'Allow an external text conversion filters',
\)
call options.define(
\ '--no-textconv',
\ 'Disallow an external text conversion filters',
\)
call options.define(
\ '--ignore-submodules',
\ 'Ignore changes to submodules in the diff generation',
\)
call options.define(
\ '--src-prefix=',
\ 'Show the given source prefix instead of "a/"',
\)
call options.define(
\ '--dst-prefix=',
\ 'Show the given destination prefix instead of "a/"',
\)
call options.define(
\ '--no-prefix',
\ 'Do not show any source or destination prefix',
\)
call options.define(
\ '--line-prefix=',
\ 'Prepend an additional prefix to every line of output',
\)
return options
endfunction
function! s:build_args(git, args) abort
let args = a:args.clone()
let args.params.group = args.pop('--group', '')
let args.params.opener = args.pop('--opener', '')
let args.params.cached = args.get('--cached')
let args.params.R = args.get('-R')
let args.params.partial = !empty(args.residual())
" Remove unsupported options
call args.pop('-z')
call args.pop('--color')
call args.pop('--word-diff')
call args.pop('--word-diff-regex')
call args.pop('--color-words')
call args.pop('--ws-error-highlight')
" Force --no-color
call args.set('--no-color', 1)
call gina#core#args#extend_treeish(a:git, args, args.pop(1))
call gina#core#args#extend_diff(a:git, args, args.params.rev)
call args.set(1, args.params.rev)
if args.params.path isnot# v:null
call args.residual([args.params.path] + args.residual())
endif
return args.lock()
endfunction
function! s:init(args) abort
call gina#core#meta#set('args', a:args)
if exists('b:gina_initialized')
return
endif
let b:gina_initialized = 1
setlocal nomodeline
setlocal buftype=nowrite
setlocal noswapfile
setlocal nomodifiable
if a:args.params.partial
setlocal bufhidden=wipe
else
setlocal bufhidden&
endif
augroup gina_command_diff_internal
autocmd! * <buffer>
autocmd BufReadCmd <buffer>
\ call gina#core#revelator#call(function('s:BufReadCmd'), [])
autocmd BufWinEnter <buffer> call setbufvar(str2nr(expand('<abuf>')), '&buflisted', 1)
autocmd BufWinLeave <buffer> call setbufvar(str2nr(expand('<abuf>')), '&buflisted', 0)
augroup END
nnoremap <buffer><silent> <Plug>(gina-diff-jump)
\ :<C-u>call gina#core#diffjump#jump()<CR>
nnoremap <buffer><silent> <Plug>(gina-diff-jump-split)
\ :<C-u>call gina#core#diffjump#jump('split')<CR>
nnoremap <buffer><silent> <Plug>(gina-diff-jump-vsplit)
\ :<C-u>call gina#core#diffjump#jump('vsplit')<CR>
if g:gina#command#diff#use_default_mappings
nmap <buffer> <CR> <Plug>(gina-diff-jump)
endif
endfunction
function! s:BufReadCmd() abort
let git = gina#core#get_or_fail()
let args = gina#core#meta#get_or_fail('args')
let pipe = gina#process#pipe#stream(s:writer)
call gina#core#buffer#assign_cmdarg()
call gina#process#open(git, args, pipe)
setlocal filetype=diff
endfunction
" Writer ---------------------------------------------------------------------
function! s:_writer_on_exit() abort dict
call call(s:original_writer.on_exit, [], self)
call gina#core#emitter#emit('command:called', s:SCHEME)
endfunction
let s:original_writer = gina#process#pipe#stream_writer()
let s:writer = extend(deepcopy(s:original_writer), {
\ 'on_exit': function('s:_writer_on_exit'),
\})
" Config ---------------------------------------------------------------------
call gina#config(expand('<sfile>'), {
\ 'use_default_mappings': 1,
\})

View File

@ -0,0 +1,77 @@
let s:Path = vital#gina#import('System.Filepath')
let s:SCHEME = gina#command#scheme(expand('<sfile>'))
function! gina#command#edit#call(range, args, mods) abort
call gina#core#options#help_if_necessary(a:args, s:get_options())
call gina#process#register(s:SCHEME, 1)
try
call s:call(a:range, a:args, a:mods)
finally
call gina#process#unregister(s:SCHEME, 1)
endtry
endfunction
function! gina#command#edit#complete(arglead, cmdline, cursorpos) abort
let args = gina#core#args#new(matchstr(a:cmdline, '^.*\ze .*'))
if a:arglead[0] ==# '-'
let options = s:get_options()
return options.complete(a:arglead, a:cmdline, a:cursorpos)
endif
return gina#complete#filename#any(a:arglead, a:cmdline, a:cursorpos)
endfunction
" Private --------------------------------------------------------------------
function! s:get_options() abort
let options = gina#core#options#new()
call options.define(
\ '-h|--help',
\ 'Show this help.',
\)
call options.define(
\ '--opener=',
\ 'A Vim command to open a new buffer.',
\ ['edit', 'split', 'vsplit', 'tabedit', 'pedit'],
\)
call options.define(
\ '--group=',
\ 'A window group name used for the buffer.',
\)
call options.define(
\ '--line',
\ 'An initial line number.',
\)
call options.define(
\ '--col',
\ 'An initial column number.',
\)
return options
endfunction
function! s:build_args(git, args) abort
let args = a:args.clone()
let args.params.group = args.pop('--group', '')
let args.params.opener = args.pop('--opener', '')
call gina#core#args#extend_path(a:git, args, args.pop(1))
call gina#core#args#extend_line(a:git, args, args.pop('--line'))
call gina#core#args#extend_col(a:git, args, args.pop('--col'))
return args.lock()
endfunction
function! s:call(range, args, mods) abort
let git = gina#core#get()
let args = s:build_args(git, a:args)
let abspath = gina#core#repo#abspath(git, args.params.path)
let bufname = s:Path.realpath(abspath)
call gina#core#buffer#open(bufname, {
\ 'mods': a:mods,
\ 'group': args.params.group,
\ 'opener': args.params.opener,
\ 'cmdarg': args.params.cmdarg,
\ 'line': args.params.line,
\ 'col': args.params.col,
\})
call gina#core#emitter#emit('command:called', s:SCHEME)
endfunction

View File

@ -0,0 +1,321 @@
let s:String = vital#gina#import('Data.String')
let s:SCHEME = gina#command#scheme(expand('<sfile>'))
function! gina#command#grep#call(range, args, mods) abort
call gina#core#options#help_if_necessary(a:args, s:get_options())
let git = gina#core#get_or_fail()
let args = s:build_args(git, a:args)
let bufname = gina#core#buffer#bufname(git, s:SCHEME, {
\ 'params': [
\ args.params.partial ? '--' : '',
\ ],
\})
call gina#core#buffer#open(bufname, {
\ 'mods': a:mods,
\ 'group': args.params.group,
\ 'opener': args.params.opener,
\ 'cmdarg': args.params.cmdarg,
\ 'callback': {
\ 'fn': function('s:init'),
\ 'args': [args],
\ }
\})
endfunction
function! gina#command#grep#complete(arglead, cmdline, cursorpos) abort
let args = gina#core#args#new(matchstr(a:cmdline, '^.*\ze .*'))
if a:cmdline =~# '\s--\s'
return gina#complete#filename#any(a:arglead, a:cmdline, a:cursorpos)
elseif a:arglead[0] ==# '-' || !empty(args.get(2))
let options = s:get_options()
return options.complete(a:arglead, a:cmdline, a:cursorpos)
endif
return gina#complete#commit#any(a:arglead, a:cmdline, a:cursorpos)
endfunction
function! gina#command#grep#parse_record(...) abort
return call('s:parse_record', a:000)
endfunction
function! gina#command#grep#_is_column_supported(version) abort
return s:is_column_supported(a:version)
endfunction
" Private --------------------------------------------------------------------
function! s:is_column_supported(version) abort
" https://github.com/git/git/blob/master/Documentation/RelNotes/2.19.0.txt#L18-L19
return a:version =~# '\%(^[^012]\|^2\.[^01]\|^2\.19\)'
endfunction
function! s:get_options() abort
let options = gina#core#options#new()
call options.define(
\ '-h|--help',
\ 'Show this help.',
\)
call options.define(
\ '--opener=',
\ 'A Vim command to open a new buffer.',
\ ['edit', 'split', 'vsplit', 'tabedit', 'pedit'],
\)
call options.define(
\ '--group=',
\ 'A window group name used for the buffer.',
\)
call options.define(
\ '--cached',
\ 'Search in index instead of in the work tree',
\)
call options.define(
\ '--no-index',
\ 'Find in contents not managed by git',
\)
call options.define(
\ '--untracked',
\ 'Search in both tracked and untracked files',
\)
call options.define(
\ '--exclude-standard',
\ 'Ignore files specified via .gitignore',
\)
call options.define(
\ '-v|--invert-match',
\ 'Show non-matching lines',
\)
call options.define(
\ '-i|--ignore-case',
\ 'Case insensitive matching',
\)
call options.define(
\ '-w|--word-regexp',
\ 'Match patterns only at word boundaries',
\)
call options.define(
\ '-a|--text',
\ 'Process binary files as text',
\)
call options.define(
\ '-I',
\ 'Don''t match patterns in binary files',
\)
call options.define(
\ '--textconv',
\ 'Process binary files with textconv filters',
\)
call options.define(
\ '--max-depth=',
\ 'Descend at most <depth> levels',
\)
call options.define(
\ '-E|--extended-regexp',
\ 'Use extended POSIC regular expression',
\)
call options.define(
\ '-G|--basic-regexp',
\ 'Use basic POSIX regular expression',
\)
call options.define(
\ '-F|--fixed-string',
\ 'Interpret patterns as fixed strings',
\)
call options.define(
\ '-P|--perl-regexp',
\ 'Use Perl-compatible regular expression',
\)
call options.define(
\ '--break',
\ 'Print empty line between matches from different files',
\)
call options.define(
\ '-C|--context=',
\ 'Show <n> context lines before and after matches',
\)
call options.define(
\ '-B|--before-context=',
\ 'Show <n> context lines before matches',
\)
call options.define(
\ '-A|--after-context=',
\ 'Show <n> context lines after matches',
\)
call options.define(
\ '--threads=',
\ 'Use <n> worker threads',
\)
call options.define(
\ '-p|--show-function',
\ 'Show a line with the function name before matches',
\)
call options.define(
\ '-W|--function-context',
\ 'Show the surrounding function',
\)
call options.define(
\ '-f',
\ 'Read patterns from file',
\)
call options.define(
\ '-e',
\ 'Match <pattern>',
\)
call options.define(
\ '--and|--or|--not',
\ 'Combine patterns specified with -e',
\)
call options.define(
\ '--all-match',
\ 'Show only matches from files that match all patterns',
\)
return options
endfunction
function! s:build_args(git, args) abort
let args = a:args.clone()
let args.params.group = args.pop('--group', '')
let args.params.opener = args.pop('--opener', '')
let args.params.partial = !empty(args.residual())
" Ask pattern if no option has specified.
if !s:is_pattern_given(args)
let pattern = gina#core#console#ask('Pattern: ')
if empty(pattern)
throw gina#core#revelator#info('Cancel')
endif
call args.set('-e', pattern)
endif
" Remove unsupported options
call args.pop('-h')
call args.pop('-H')
call args.pop('-l|--files-with-matches')
call args.pop('--name-only')
call args.pop('-L|--files-without-match')
call args.pop('-z|--null')
call args.pop('-c|--count')
call args.pop('--heading')
" Force required options
if !args.has('--no-column') && s:is_column_supported(gina#core#git_version())
call insert(args.raw, '--no-column', 1)
endif
if !args.has('--line-number')
call insert(args.raw, '--line-number', 1)
endif
if !args.has('--full-name')
call insert(args.raw, '--full-name', 1)
endif
if !args.has('--color')
call insert(args.raw, '--color=always', 1)
else
call args.set('--color', 'always')
endif
return args.lock()
endfunction
function! s:init(args) abort
call gina#core#meta#set('args', a:args)
if exists('b:gina_initialized')
return
endif
let b:gina_initialized = 1
setlocal buftype=nofile
setlocal bufhidden=hide
setlocal noswapfile
setlocal nomodifiable
" Attach modules
call gina#core#locator#attach()
call gina#action#attach(function('s:get_candidates'), {
\ 'markable': 1,
\})
augroup gina_command_grep_internal
autocmd! * <buffer>
autocmd BufReadCmd <buffer>
\ call gina#core#revelator#call(function('s:BufReadCmd'), [])
augroup END
endfunction
function! s:BufReadCmd() abort
let git = gina#core#get_or_fail()
let args = gina#core#meta#get_or_fail('args')
let pipe = gina#process#pipe#stream(s:writer)
call gina#core#buffer#assign_cmdarg()
call gina#process#open(git, args, pipe)
setlocal filetype=gina-grep
endfunction
function! s:get_candidates(fline, lline) abort
let args = gina#core#meta#get_or_fail('args')
let residual = args.residual()
let candidates = map(
\ getline(a:fline, a:lline),
\ 's:parse_record(v:val, residual)'
\)
return filter(candidates, '!empty(v:val)')
endfunction
function! s:parse_record(record, residual) abort
let record = s:String.remove_ansi_sequences(a:record)
let m = matchlist(record, '^\([^:]\+:\)\?\(.*\):\(\d\+\):\(.*\)$')
if empty(m)
return {}
endif
let matched = matchstr(a:record, '\e\[1;31m\zs.\{-}\ze\e\[m')
let line = str2nr(m[3])
let col = stridx(m[4], matched) + 1
let candidate = {
\ 'word': m[4],
\ 'abbr': a:record,
\ 'line': line,
\ 'col': col,
\ 'path': m[2],
\ 'rev': m[1],
\ 'residual': a:residual,
\}
return candidate
endfunction
function! s:is_pattern_given(args) abort
let cmdline = join(a:args.raw[1:])
let value_options = [
\ '--max-depth',
\ '-C', '--context',
\ '-A', '--after-context',
\ '-B', '--before-context',
\ '--threads',
\]
for value_option in value_options
let cmdline = substitute(
\ cmdline,
\ value_option . '\s\+\d\+',
\ '', 'g',
\)
endfor
return cmdline =~# '\<\%(-e\|-f\|[^-].\{-}\)\>'
endfunction
" Writer ---------------------------------------------------------------------
function! s:_writer_on_exit() abort dict
call call(s:original_writer.on_exit, [], self)
call gina#core#emitter#emit('command:called', s:SCHEME)
endfunction
let s:original_writer = gina#process#pipe#stream_writer()
let s:writer = extend(deepcopy(s:original_writer), {
\ 'on_exit': function('s:_writer_on_exit'),
\})
" Config ---------------------------------------------------------------------
call gina#config(expand('<sfile>'), {
\ 'send_to_quickfix': 1,
\ 'use_default_aliases': 1,
\ 'use_default_mappings': 1,
\})

View File

@ -0,0 +1,16 @@
let s:SCHEME = gina#command#scheme(expand('<sfile>'))
function! gina#command#lcd#call(range, args, mods) abort
let args = a:args.clone()
call args.set('--local', 1)
call gina#command#cd#call(a:range, args, a:mods)
endfunction
function! gina#command#lcd#complete(arglead, cmdline, cursorpos) abort
let args = gina#core#args#new(matchstr(a:cmdline, '^.*\ze .*'))
if empty(args.get(1))
return gina#complete#filename#directory(a:arglead, a:cmdline, a:cursorpos)
endif
return []
endfunction

View File

@ -0,0 +1,152 @@
let s:String = vital#gina#import('Data.String')
let s:SCHEME = gina#command#scheme(expand('<sfile>'))
function! gina#command#log#call(range, args, mods) abort
call gina#core#options#help_if_necessary(a:args, s:get_options())
let git = gina#core#get_or_fail()
let args = s:build_args(git, a:args)
let bufname = gina#core#buffer#bufname(git, s:SCHEME, {
\ 'path': args.params.path,
\ 'params': [
\ args.params.partial ? '--' : '',
\ ],
\ 'noautocmd': !empty(args.params.path),
\})
call gina#core#buffer#open(bufname, {
\ 'mods': a:mods,
\ 'group': args.params.group,
\ 'opener': args.params.opener,
\ 'cmdarg': args.params.cmdarg,
\ 'callback': {
\ 'fn': function('s:init'),
\ 'args': [args],
\ }
\})
endfunction
function! gina#command#log#complete(arglead, cmdline, cursorpos) abort
let args = gina#core#args#new(matchstr(a:cmdline, '^.*\ze .*'))
if a:arglead[0] ==# '-' || !empty(args.get(1))
let options = s:get_options()
return options.complete(a:arglead, a:cmdline, a:cursorpos)
endif
return gina#complete#filename#any(a:arglead, a:cmdline, a:cursorpos)
endfunction
" Private --------------------------------------------------------------------
function! s:get_options() abort
let options = gina#core#options#new()
call options.define(
\ '-h|--help',
\ 'Show this help.',
\)
call options.define(
\ '--opener=',
\ 'A Vim command to open a new buffer.',
\ ['edit', 'split', 'vsplit', 'tabedit', 'pedit'],
\)
call options.define(
\ '--group=',
\ 'A window group name used for the buffer.',
\)
call options.define(
\ '--follow',
\ 'Continue listing the history of a file beyond renames',
\)
return options
endfunction
function! s:build_args(git, args) abort
let args = a:args.clone()
let args.params.group = args.pop('--group', '')
let args.params.opener = args.pop('--opener', '')
let args.params.partial = !empty(args.residual())
call args.set('--color', 'always')
call args.set('--pretty', "format:\e[32m%h\e[m %s \e[33;1m%cr\e[m \e[35;1m<%an>\e[m\e[36;1m%d\e[m")
call gina#core#args#extend_treeish(a:git, args, args.pop(1, v:null))
if args.params.path isnot# v:null
call args.residual([args.params.path] + args.residual())
elseif args.params.rev isnot# v:null
call args.set(1, args.params.rev)
endif
return args.lock()
endfunction
function! s:init(args) abort
call gina#core#meta#set('args', a:args)
if exists('b:gina_initialized')
return
endif
let b:gina_initialized = 1
setlocal buftype=nofile
setlocal bufhidden=hide
setlocal noswapfile
setlocal nomodifiable
" Attach modules
call gina#core#locator#attach()
call gina#action#attach(function('s:get_candidates'))
augroup gina_command_log_internal
autocmd! * <buffer>
autocmd BufReadCmd <buffer>
\ call gina#core#revelator#call(function('s:BufReadCmd'), [])
augroup END
endfunction
function! s:BufReadCmd() abort
let git = gina#core#get_or_fail()
let args = gina#core#meta#get_or_fail('args')
let pipe = gina#process#pipe#stream(s:writer)
call gina#core#buffer#assign_cmdarg()
call gina#process#open(git, args, pipe)
setlocal filetype=gina-log
endfunction
function! s:get_candidates(fline, lline) abort
let args = gina#core#meta#get_or_fail('args')
let path = args.params.path
let residual = args.residual()
let candidates = map(
\ getline(a:fline, a:lline),
\ 's:parse_record(v:val, path, residual)'
\)
return filter(candidates, '!empty(v:val)')
endfunction
function! s:parse_record(record, path, residual) abort
let record = s:String.remove_ansi_sequences(a:record)
let rev = matchstr(record, '^[|/\* ]*\s*\zs[a-z0-9]\+')
return empty(rev) ? {} : {
\ 'word': record,
\ 'abbr': a:record,
\ 'path': a:path,
\ 'rev': rev,
\ 'residual': a:residual,
\}
endfunction
" Writer ---------------------------------------------------------------------
function! s:_writer_on_exit() abort dict
call call(s:original_writer.on_exit, [], self)
call gina#core#emitter#emit('command:called', s:SCHEME)
endfunction
let s:original_writer = gina#process#pipe#stream_writer()
let s:writer = extend(deepcopy(s:original_writer), {
\ 'on_exit': function('s:_writer_on_exit'),
\})
" Config ---------------------------------------------------------------------
call gina#config(expand('<sfile>'), {
\ 'use_default_aliases': 1,
\ 'use_default_mappings': 1,
\})

View File

@ -0,0 +1,146 @@
let s:SCHEME = gina#command#scheme(expand('<sfile>'))
function! gina#command#ls#call(range, args, mods) abort
call gina#core#options#help_if_necessary(a:args, s:get_options())
let git = gina#core#get_or_fail()
let args = s:build_args(git, a:args)
let bufname = gina#core#buffer#bufname(git, s:SCHEME, {
\ 'rev': args.params.rev,
\ 'params': [
\ args.params.partial ? '--' : '',
\ ],
\})
call gina#core#buffer#open(bufname, {
\ 'mods': a:mods,
\ 'group': args.params.group,
\ 'opener': args.params.opener,
\ 'cmdarg': args.params.cmdarg,
\ 'callback': {
\ 'fn': function('s:init'),
\ 'args': [args],
\ }
\})
endfunction
function! gina#command#ls#complete(arglead, cmdline, cursorpos) abort
let args = gina#core#args#new(matchstr(a:cmdline, '^.*\ze .*'))
if a:arglead[0] ==# '-' || !empty(args.get(1))
let options = s:get_options()
return options.complete(a:arglead, a:cmdline, a:cursorpos)
endif
return gina#complete#commit#any(a:arglead, a:cmdline, a:cursorpos)
endfunction
" Private --------------------------------------------------------------------
function! s:get_options() abort
let options = gina#core#options#new()
call options.define(
\ '-h|--help',
\ 'Show this help.',
\)
call options.define(
\ '--opener=',
\ 'A Vim command to open a new buffer.',
\ ['edit', 'split', 'vsplit', 'tabedit', 'pedit'],
\)
call options.define(
\ '--group=',
\ 'A window group name used for the buffer.',
\)
return options
endfunction
function! s:build_args(git, args) abort
let args = a:args.clone()
let args.params.group = args.pop('--group', '')
let args.params.opener = args.pop('--opener', '')
let args.params.partial = !empty(args.residual())
let args.params.rev = args.get(1, gina#core#buffer#param('%', 'rev'))
if empty(args.params.rev)
call args.set(0, 'ls-files')
call args.set('--full-name', 1)
else
call args.set('--full-name', 1)
call args.set('--full-tree', 1)
call args.set('--name-only', 1)
call args.set('-r', 1)
call args.set(0, 'ls-tree')
call args.set(1, args.params.rev)
endif
return args.lock()
endfunction
function! s:init(args) abort
call gina#core#meta#set('args', a:args)
if exists('b:gina_initialized')
return
endif
let b:gina_initialized = 1
setlocal buftype=nofile
setlocal bufhidden=hide
setlocal noswapfile
setlocal nomodifiable
" Attach modules
call gina#core#locator#attach()
call gina#action#attach(function('s:get_candidates'))
augroup gina_command_ls_internal
autocmd! * <buffer>
autocmd BufReadCmd <buffer>
\ call gina#core#revelator#call(function('s:BufReadCmd'), [])
augroup END
endfunction
function! s:BufReadCmd() abort
let git = gina#core#get_or_fail()
let args = gina#core#meta#get_or_fail('args')
let pipe = gina#process#pipe#stream(s:writer)
call gina#core#buffer#assign_cmdarg()
call gina#process#open(git, args, pipe)
setlocal filetype=gina-ls
endfunction
function! s:get_candidates(fline, lline) abort
let args = gina#core#meta#get_or_fail('args')
let rev = args.params.rev
let residual = args.residual()
let candidates = map(
\ getline(a:fline, a:lline),
\ 's:parse_record(v:val, rev, residual)'
\)
return filter(candidates, '!empty(v:val)')
endfunction
function! s:parse_record(record, rev, residual) abort
return {
\ 'word': a:record,
\ 'path': a:record,
\ 'rev': a:rev,
\ 'residual': a:residual,
\}
endfunction
" Writer ---------------------------------------------------------------------
function! s:_writer_on_exit() abort dict
call call(s:original_writer.on_exit, [], self)
call gina#core#emitter#emit('command:called', s:SCHEME)
endfunction
let s:original_writer = gina#process#pipe#stream_writer()
let s:writer = extend(deepcopy(s:original_writer), {
\ 'on_exit': function('s:_writer_on_exit'),
\})
" Config ---------------------------------------------------------------------
call gina#config(expand('<sfile>'), {
\ 'use_default_aliases': 1,
\ 'use_default_mappings': 1,
\})

View File

@ -0,0 +1,352 @@
let s:Path = vital#gina#import('System.Filepath')
let s:String = vital#gina#import('Data.String')
let s:SCHEME = gina#command#scheme(expand('<sfile>'))
let s:WORKTREE = '@@'
let s:is_windows = has('win32') || has('win64')
function! gina#command#patch#call(range, args, mods) abort
call gina#core#options#help_if_necessary(a:args, s:get_options())
call gina#process#register(s:SCHEME, 1)
try
call s:call(a:range, a:args, a:mods)
finally
call gina#process#unregister(s:SCHEME, 1)
endtry
endfunction
function! gina#command#patch#complete(arglead, cmdline, cursorpos) abort
let args = gina#core#args#new(matchstr(a:cmdline, '^.*\ze .*'))
if a:arglead[0] ==# '-' || !empty(args.get(1))
let options = s:get_options()
return options.complete(a:arglead, a:cmdline, a:cursorpos)
endif
return gina#complete#filename#tracked(a:arglead, a:cmdline, a:cursorpos)
endfunction
" Private --------------------------------------------------------------------
function! s:get_options() abort
let options = gina#core#options#new()
call options.define(
\ '-h|--help',
\ 'Show this help.',
\)
call options.define(
\ '--opener=',
\ 'A Vim command to open a new buffer.',
\ ['edit', 'split', 'vsplit', 'tabedit', 'pedit'],
\)
call options.define(
\ '--group1=',
\ 'A window group name used for the 1st buffer.',
\)
call options.define(
\ '--group2=',
\ 'A window group name used for the 2nd buffer.',
\)
call options.define(
\ '--group3=',
\ 'A window group name used for the 3rd buffer.',
\)
call options.define(
\ '--line=',
\ 'An initial line number.',
\)
call options.define(
\ '--col=',
\ 'An initial column number.',
\)
call options.define(
\ '--oneside',
\ 'Use two buffers instead of three buffers.',
\)
call options.define(
\ '--diffoff',
\ 'Call diffoff! prior to open buffers.',
\)
return options
endfunction
function! s:build_args(git, args) abort
let args = a:args.clone()
let args.params.groups = [
\ args.pop('--group1', ''),
\ args.pop('--group2', ''),
\ args.pop('--group3', ''),
\]
let args.params.no_group = args.pop('--no-group', 0)
let args.params.opener = args.pop('--opener', 'tabnew')
let args.params.oneside = args.pop('--oneside', 0)
let args.params.diffoff = args.pop('--diffoff')
call gina#core#args#extend_path(a:git, args, args.pop(1))
call gina#core#args#extend_line(a:git, args, args.pop('--line'))
call gina#core#args#extend_col(a:git, args, args.pop('--col'))
return args.lock()
endfunction
function! s:open(n, mods, opener, rev, params) abort
if a:rev ==# s:WORKTREE
execute printf(
\ '%s Gina edit %s %s %s %s %s %s',
\ a:mods,
\ a:params.cmdarg,
\ gina#util#shellescape(a:opener, '--opener='),
\ gina#util#shellescape(a:params.groups[a:n], '--group='),
\ gina#util#shellescape(a:params.line, '--line='),
\ gina#util#shellescape(a:params.col, '--col='),
\ gina#util#shellescape(a:params.path),
\)
else
let treeish = gina#core#treeish#build(a:rev, a:params.path)
execute printf(
\ '%s Gina show %s %s %s %s %s %s',
\ a:mods,
\ a:params.cmdarg,
\ gina#util#shellescape(a:opener, '--opener='),
\ gina#util#shellescape(a:params.groups[a:n], '--group='),
\ gina#util#shellescape(a:params.line, '--line='),
\ gina#util#shellescape(a:params.col, '--col='),
\ gina#util#shellescape(treeish),
\)
endif
endfunction
function! s:call(range, args, mods) abort
let git = gina#core#get_or_fail()
let args = s:build_args(git, a:args)
let mods = gina#util#contain_direction(a:mods)
\ ? 'keepalt ' . a:mods
\ : join(['keepalt', 'rightbelow', a:mods])
if !empty(args.params.diffoff)
diffoff!
endif
let opener1 = args.params.opener
let opener2 = empty(matchstr(&diffopt, 'vertical'))
\ ? 'split'
\ : 'vsplit'
" Validate if all requirements exist
call gina#core#treeish#validate(git, ':0', args.params.path, printf(join([
\ 'The "%s" does not have an index content.',
\ 'Use "chaperon" instead if you would like to patch on conflicted file',
\], "\n"), args.params.path))
if args.params.oneside
call s:open(1, mods, opener1, ':0', args.params)
let bufnr2 = bufnr('%')
call s:open(2, mods, opener2, s:WORKTREE, args.params)
let bufnr3 = bufnr('%')
else
call s:open(0, mods, opener1, 'HEAD', args.params)
let bufnr1 = bufnr('%')
call s:open(1, mods, opener2, ':0', args.params)
let bufnr2 = bufnr('%')
call s:open(2, mods, opener2, s:WORKTREE, args.params)
let bufnr3 = bufnr('%')
endif
" WORKTREE
call gina#util#diffthis()
call s:define_plug_mapping('diffput', bufnr2)
call s:define_plug_mapping('diffget', bufnr2)
if g:gina#command#patch#use_default_mappings
nmap <buffer> dp <Plug>(gina-diffput)
nmap <buffer> do <Plug>(gina-diffget)
endif
" HEAD
if !args.params.oneside
execute printf('%dwincmd w', bufwinnr(bufnr1))
call gina#util#diffthis()
call s:define_plug_mapping('diffput', bufnr2)
if g:gina#command#patch#use_default_mappings
nmap <buffer> dp <Plug>(gina-diffput)
endif
endif
" INDEX
execute printf('%dwincmd w', bufwinnr(bufnr2))
call gina#util#diffthis()
call s:define_plug_mapping('diffput', bufnr3)
if !args.params.oneside
call s:define_plug_mapping('diffget', bufnr1, '-l')
endif
call s:define_plug_mapping('diffget', bufnr3, '-r')
if g:gina#command#patch#use_default_mappings
nmap <buffer> dp <Plug>(gina-diffput)
if !args.params.oneside
nmap <buffer> dol <Plug>(gina-diffget-l)
endif
nmap <buffer> dor <Plug>(gina-diffget-r)
endif
setlocal buftype=acwrite
setlocal modifiable
augroup gina_command_patch_internal
autocmd! * <buffer>
autocmd BufWriteCmd <buffer> call s:BufWriteCmd()
augroup END
call gina#util#diffupdate()
normal! zm
call gina#core#emitter#emit('command:called', s:SCHEME)
endfunction
function! s:define_plug_mapping(command, bufnr, ...) abort
let suffix = a:0 ? a:1 : ''
let lhs = printf('<Plug>(gina-%s%s)', a:command, suffix)
let rhs = printf(':<C-u>%s %d<CR>:diffupdate<CR>', a:command, a:bufnr)
call gina#util#map(lhs, rhs, {
\ 'mode': 'n',
\ 'noremap': 1,
\ 'silent': 1,
\})
endfunction
function! s:patch(git) abort
let abspath = gina#core#path#expand('%')
let path = gina#core#repo#relpath(a:git, abspath)
call gina#process#call_or_fail(a:git, [
\ 'add',
\ '--intent-to-add',
\ '--',
\ s:Path.realpath(abspath),
\])
let tempfile = tempname()
let tempfile1 = tempfile . '.index'
let tempfile2 = tempfile . '.buffer'
try
let diff = s:diff(a:git, path, getline(1, '$'), tempfile1, tempfile2)
let result = s:apply(a:git, diff)
finally
silent! call delete(tempfile1)
silent! call delete(tempfile2)
endtry
return result
endfunction
function! s:diff(git, path, buffer, tempfile1, tempfile2) abort
if writefile(s:index(a:git, a:path), a:tempfile1) == -1
return ''
endif
if writefile(a:buffer, a:tempfile2) == -1
return ''
endif
" NOTE:
" --no-index force --exit-code option.
" --exit-code mean that the program exits with 1 if there were differences
" and 0 means no differences
let result = gina#process#call(a:git, [
\ 'diff',
\ '--no-index',
\ '--unified=1',
\ '--',
\ a:tempfile1,
\ a:tempfile2,
\])
if !result.status
throw gina#core#revelator#info(
\ 'No difference between index and buffer'
\)
endif
return s:replace_filenames_in_diff(
\ result.stdout,
\ a:tempfile1,
\ a:tempfile2,
\ a:path,
\)
endfunction
function! s:index(git, path) abort
let result = gina#process#call(a:git, ['show', ':' . a:path])
if result.status
return []
endif
return result.stdout
endfunction
function! s:replace_filenames_in_diff(content, filename1, filename2, repl) abort
" replace tempfile1/tempfile2 in the header to a:filename
"
" diff --git a/<tempfile1> b/<tempfile2>
" index XXXXXXX..XXXXXXX XXXXXX
" --- a/<tempfile1>
" +++ b/<tempfile2>
"
let src1 = s:String.escape_pattern(a:filename1)
let src2 = s:String.escape_pattern(a:filename2)
if s:is_windows
" NOTE:
" '\' in {content} from 'git diff' are escaped so double escape is required
" to substitute such path
" NOTE:
" escape(src1, '\') cannot be used while other characters such as '.' are
" already escaped as well
let src1 = substitute(src1, '\\\\', '\\\\\\\\', 'g')
let src2 = substitute(src2, '\\\\', '\\\\\\\\', 'g')
endif
let repl = (a:filename1 =~# '^/' ? '/' : '') . a:repl
let content = copy(a:content)
let content[0] = substitute(content[0], src1, repl, '')
let content[0] = substitute(content[0], src2, repl, '')
let content[2] = substitute(content[2], src1, repl, '')
let content[3] = substitute(content[3], src2, repl, '')
return content
endfunction
function! s:apply(git, content) abort
let tempfile = tempname()
try
if writefile(a:content, tempfile) == -1
return
endif
let result = gina#process#call_or_fail(a:git, [
\ 'apply',
\ '--verbose',
\ '--cached',
\ '--',
\ tempfile,
\])
call gina#core#emitter#emit('command:called:patch')
return result
finally
silent! call delete(tempfile)
endtry
endfunction
function! s:BufWriteCmd() abort
let git = gina#core#get_or_fail()
let result = gina#core#revelator#call(function('s:patch'), [git])
if !empty(result)
setlocal nomodified
endif
call gina#util#diffupdate()
endfunction
" Event ----------------------------------------------------------------------
function! s:on_command_called_patch(...) abort
call gina#core#emitter#emit('modified:delay')
endfunction
if !exists('s:subscribed')
let s:subscribed = 1
call gina#core#emitter#subscribe(
\ 'command:called:patch',
\ function('s:on_command_called_patch')
\)
endif
" Config ---------------------------------------------------------------------
call gina#config(expand('<sfile>'), {
\ 'use_default_mappings': 1,
\})

View File

@ -0,0 +1,223 @@
let s:Guard = vital#gina#import('Vim.Guard')
let s:Path = vital#gina#import('System.Filepath')
let s:SCHEME = gina#command#scheme(expand('<sfile>'))
function! gina#command#qrep#call(range, args, mods) abort
call gina#core#options#help_if_necessary(a:args, s:get_options())
let git = gina#core#get_or_fail()
let args = s:build_args(git, a:args)
call gina#util#doautocmd('QuickfixCmdPre')
let result = gina#process#call(git, args)
let guard = s:Guard.store(['&more'])
try
set nomore
call gina#process#inform(result)
" XXX: Support rev
" 1. Globally enable BufReadCmd for gina://xxx:show/...
" 2. Use gina://xxx:show/... to open a content in a rev
let rev = ''
let residual = args.residual()
let items = map(
\ copy(result.stdout),
\ 's:parse_record(git, v:val, rev, residual)',
\)
call setqflist(
\ filter(items, '!empty(v:val)'),
\ args.params.action,
\)
finally
call guard.restore()
endtry
call gina#util#doautocmd('QuickfixCmdPost')
if !args.params.bang
cc
endif
call gina#core#emitter#emit('command:called', s:SCHEME)
endfunction
function! gina#command#qrep#complete(arglead, cmdline, cursorpos) abort
let args = gina#core#args#new(matchstr(a:cmdline, '^.*\ze .*'))
if a:cmdline =~# '\s--\s'
return gina#complete#filename#any(a:arglead, a:cmdline, a:cursorpos)
elseif a:arglead[0] ==# '-' || !empty(args.get(2))
let options = s:get_options()
return options.complete(a:arglead, a:cmdline, a:cursorpos)
endif
return gina#complete#commit#any(a:arglead, a:cmdline, a:cursorpos)
endfunction
" Private --------------------------------------------------------------------
function! s:get_options() abort
let options = gina#core#options#new()
call options.define(
\ '-h|--help',
\ 'Show this help.',
\)
call options.define(
\ '--opener=',
\ 'A Vim command to open a new buffer.',
\ ['edit', 'split', 'vsplit', 'tabedit', 'pedit'],
\)
call options.define(
\ '--group=',
\ 'A window group name used for the buffer.',
\)
call options.define(
\ '--cached',
\ 'Search in index instead of in the work tree',
\)
call options.define(
\ '--no-index',
\ 'Find in contents not managed by git',
\)
call options.define(
\ '--untracked',
\ 'Search in both tracked and untracked files',
\)
call options.define(
\ '--exclude-standard',
\ 'Ignore files specified via .gitignore',
\)
call options.define(
\ '-v|--invert-match',
\ 'Show non-matching lines',
\)
call options.define(
\ '-i|--ignore-case',
\ 'Case insensitive matching',
\)
call options.define(
\ '-w|--word-regexp',
\ 'Match patterns only at word boundaries',
\)
call options.define(
\ '-a|--text',
\ 'Process binary files as text',
\)
call options.define(
\ '-I',
\ 'Don''t match patterns in binary files',
\)
call options.define(
\ '--textconv',
\ 'Process binary files with textconv filters',
\)
call options.define(
\ '--max-depth=',
\ 'Descend at most <depth> levels',
\)
call options.define(
\ '-E|--extended-regexp',
\ 'Use extended POSIC regular expression',
\)
call options.define(
\ '-G|--basic-regexp',
\ 'Use basic POSIX regular expression',
\)
call options.define(
\ '-F|--fixed-string',
\ 'Interpret patterns as fixed strings',
\)
call options.define(
\ '-P|--perl-regexp',
\ 'Use Perl-compatible regular expression',
\)
call options.define(
\ '--break',
\ 'Print empty line between matches from different files',
\)
call options.define(
\ '-C|--context=',
\ 'Show <n> context lines before and after matches',
\)
call options.define(
\ '-B|--before-context=',
\ 'Show <n> context lines before matches',
\)
call options.define(
\ '-A|--after-context=',
\ 'Show <n> context lines after matches',
\)
call options.define(
\ '--threads=',
\ 'Use <n> worker threads',
\)
call options.define(
\ '-p|--show-function',
\ 'Show a line with the function name before matches',
\)
call options.define(
\ '-W|--function-context',
\ 'Show the surrounding function',
\)
call options.define(
\ '-f',
\ 'Read patterns from file',
\)
call options.define(
\ '-e',
\ 'Match <pattern>',
\)
call options.define(
\ '--and|--or|--not',
\ 'Combine patterns specified with -e',
\)
call options.define(
\ '--all-match',
\ 'Show only matches from files that match all patterns',
\)
call options.define(
\ '--action',
\ 'An action which is specified to setqflist()',
\)
return options
endfunction
function! s:build_args(git, args) abort
let args = a:args.clone()
let args.params.bang = args.get(0) =~# '!$'
let args.params.action = args.pop('--action', ' ')
let args.params.pattern = args.pop(1, '')
" Check if available grep patterns has specified and ask if not
if empty(args.params.pattern) && !(args.has('-e') || args.has('-f'))
let pattern = gina#core#console#ask('Pattern: ')
if empty(pattern)
throw gina#core#revelator#info('Cancel')
endif
let args.params.pattern = pattern
endif
if gina#command#grep#_is_column_supported(gina#core#git_version())
call args.set('--no-column', 1)
endif
call args.set('--line-number', 1)
call args.set('--color', 'always')
call args.set(0, 'grep')
call args.set(1, args.params.pattern)
return args.lock()
endfunction
function! s:parse_record(git, record, rev, residual) abort
" Parse record to make a gina candidate and translate it to a quickfix item
let candidate = gina#command#grep#parse_record(
\ a:record, a:residual,
\)
if empty(candidate)
return {}
endif
return {
\ 'filename': s:Path.realpath(
\ gina#core#repo#abspath(a:git, candidate.path)
\ ),
\ 'text': candidate.word,
\ 'lnum': candidate.line,
\ 'col': candidate.col,
\}
endfunction

View File

@ -0,0 +1,241 @@
let s:String = vital#gina#import('Data.String')
let s:SCHEME = gina#command#scheme(expand('<sfile>'))
function! gina#command#reflog#call(range, args, mods) abort
call gina#core#options#help_if_necessary(a:args, s:get_options_show())
if s:is_raw_command(a:args)
" Remove non git options
let args = a:args.clone()
call args.pop('--group')
call args.pop('--opener')
" Call raw git command
return gina#command#_raw#call(a:range, args, a:mods)
endif
let git = gina#core#get_or_fail()
let args = s:build_args(git, a:args)
let bufname = gina#core#buffer#bufname(git, s:SCHEME)
call gina#core#buffer#open(bufname, {
\ 'mods': a:mods,
\ 'group': args.params.group,
\ 'opener': args.params.opener,
\ 'cmdarg': args.params.cmdarg,
\ 'callback': {
\ 'fn': function('s:init'),
\ 'args': [args],
\ }
\})
endfunction
function! gina#command#reflog#complete(arglead, cmdline, cursorpos) abort
let args = gina#core#args#new(matchstr(a:cmdline, '^.*\ze .*'))
if args.get(1) =~# '^\%(show\|\)$'
if a:arglead =~# '^-'
let options = s:get_options_show()
return options.complete(a:arglead, a:cmdline, a:cursorpos)
endif
elseif args.get(1) ==# 'expire'
if a:arglead =~# '^-'
let options = s:get_options_expire()
return options.complete(a:arglead, a:cmdline, a:cursorpos)
else
return gina#complete#commit#any(a:arglead, a:cmdline, a:cursorpos)
endif
elseif args.get(1) ==# 'delete'
if a:arglead =~# '^-'
let options = s:get_options_delete()
return options.complete(a:arglead, a:cmdline, a:cursorpos)
else
return gina#complete#commit#any(a:arglead, a:cmdline, a:cursorpos)
endif
elseif args.get(1) ==# 'exists'
return gina#complete#commit#any(a:arglead, a:cmdline, a:cursorpos)
endif
return gina#util#filter(a:arglead, [
\ 'show',
\ 'expire',
\ 'delete',
\ 'exists',
\])
endfunction
" Private --------------------------------------------------------------------
function! s:get_options_show() abort
let options = gina#core#options#new()
call options.define(
\ '-h|--help',
\ 'Show this help.',
\)
call options.define(
\ '--opener=',
\ 'A Vim command to open a new buffer.',
\ ['edit', 'split', 'vsplit', 'tabedit', 'pedit'],
\)
call options.define(
\ '--group=',
\ 'A window group name used for the buffer.',
\)
call options.define(
\ '--follow',
\ 'Continue listing the history of a file beyond renames',
\)
return options
endfunction
function! s:get_options_expire() abort
let options = gina#core#options#new()
call options.define(
\ '-h|--help',
\ 'Show this help.',
\)
call options.define(
\ '--expire=',
\ 'Prune entries older than the specified time.',
\)
call options.define(
\ '--expire-unreachable=',
\ 'Prune entries older than the specified time that are not reachable.',
\)
call options.define(
\ '--rewrite',
\ 'Adjust old SHA-1 to be equal to the new SHA-1',
\)
call options.define(
\ '--updateref',
\ 'Update the reference to the value of the top reflog entry',
\)
call options.define(
\ '--stale-fix',
\ 'Prune broken commit reflog entries',
\)
call options.define(
\ '-n|--dry-run',
\ 'Do not actually prune any entries',
\)
call options.define(
\ '--verbose',
\ 'Print extra information',
\)
return options
endfunction
function! s:get_options_delete() abort
let options = gina#core#options#new()
call options.define(
\ '-h|--help',
\ 'Show this help.',
\)
call options.define(
\ '--rewrite',
\ 'Adjust old SHA-1 to be equal to the new SHA-1',
\)
call options.define(
\ '--updateref',
\ 'Update the reference to the value of the top reflog entry',
\)
call options.define(
\ '-n|--dry-run',
\ 'Do not actually prune any entries',
\)
call options.define(
\ '--verbose',
\ 'Print extra information',
\)
return options
endfunction
function! s:build_args(git, args) abort
let args = a:args.clone()
let args.params.group = args.pop('--group', '')
let args.params.opener = args.pop('--opener', '')
call args.set('--color', 'always')
return args.lock()
endfunction
function! s:is_raw_command(args) abort
if a:args.get(1) =~# '^\%(show\|\)$'
return 0
elseif a:args.get(1) ==# 'expire'
return 1
elseif a:args.get(1) ==# 'delete'
return 1
elseif a:args.get(1) ==# 'exists'
return 1
endif
return 0
endfunction
function! s:init(args) abort
call gina#core#meta#set('args', a:args)
if exists('b:gina_initialized')
return
endif
let b:gina_initialized = 1
setlocal buftype=nofile
setlocal bufhidden=hide
setlocal noswapfile
setlocal nomodifiable
" Attach modules
call gina#core#locator#attach()
call gina#action#attach(function('s:get_candidates'))
augroup gina_command_reflog_internal
autocmd! * <buffer>
autocmd BufReadCmd <buffer>
\ call gina#core#revelator#call(function('s:BufReadCmd'), [])
augroup END
endfunction
function! s:BufReadCmd() abort
let git = gina#core#get_or_fail()
let args = gina#core#meta#get_or_fail('args')
let pipe = gina#process#pipe#stream(s:writer)
call gina#core#buffer#assign_cmdarg()
call gina#process#open(git, args, pipe)
setlocal filetype=gina-reflog
endfunction
function! s:get_candidates(fline, lline) abort
let candidates = map(
\ getline(a:fline, a:lline),
\ 's:parse_record(v:val)'
\)
return filter(candidates, '!empty(v:val)')
endfunction
function! s:parse_record(record) abort
let record = s:String.remove_ansi_sequences(a:record)
let rev = matchstr(record, '^[a-z0-9]\+')
return {
\ 'word': record,
\ 'abbr': a:record,
\ 'rev': rev,
\}
endfunction
" Writer ---------------------------------------------------------------------
function! s:_writer_on_exit() abort dict
call call(s:original_writer.on_exit, [], self)
call gina#core#emitter#emit('command:called', s:SCHEME)
endfunction
let s:original_writer = gina#process#pipe#stream_writer()
let s:writer = extend(deepcopy(s:original_writer), {
\ 'on_exit': function('s:_writer_on_exit'),
\})
" Config ---------------------------------------------------------------------
call gina#config(expand('<sfile>'), {
\ 'use_default_aliases': 1,
\ 'use_default_mappings': 1,
\})

View File

@ -0,0 +1,158 @@
let s:Buffer = vital#gina#import('Vim.Buffer')
let s:Path = vital#gina#import('System.Filepath')
let s:SCHEME = gina#command#scheme(expand('<sfile>'))
function! gina#command#show#call(range, args, mods) abort
call gina#core#options#help_if_necessary(a:args, s:get_options())
let git = gina#core#get_or_fail()
let args = s:build_args(git, a:args)
let bufname = gina#core#buffer#bufname(git, s:SCHEME, {
\ 'treeish': args.params.treeish,
\ 'params': [
\ args.params.partial ? '--' : '',
\ ],
\})
call gina#core#buffer#open(bufname, {
\ 'mods': a:mods,
\ 'group': args.params.group,
\ 'opener': args.params.opener,
\ 'cmdarg': args.params.cmdarg,
\ 'line': args.params.line,
\ 'col': args.params.col,
\ 'callback': {
\ 'fn': function('s:init'),
\ 'args': [args],
\ }
\})
endfunction
function! gina#command#show#complete(arglead, cmdline, cursorpos) abort
let args = gina#core#args#new(matchstr(a:cmdline, '^.*\ze .*'))
if a:arglead =~# '^-'
let options = s:get_options()
return options.complete(a:arglead, a:cmdline, a:cursorpos)
endif
return gina#complete#common#treeish(a:arglead, a:cmdline, a:cursorpos)
endfunction
" Private --------------------------------------------------------------------
function! s:get_options() abort
let options = gina#core#options#new()
call options.define(
\ '-h|--help',
\ 'Show this help.',
\)
call options.define(
\ '--opener=',
\ 'A Vim command to open a new buffer.',
\ ['edit', 'split', 'vsplit', 'tabedit', 'pedit'],
\)
call options.define(
\ '--group=',
\ 'A window group name used for the buffer.',
\)
call options.define(
\ '--line',
\ 'An initial line number.',
\)
call options.define(
\ '--col',
\ 'An initial column number.',
\)
return options
endfunction
function! s:build_args(git, args) abort
let args = a:args.clone()
let args.params.group = args.pop('--group', '')
let args.params.opener = args.pop('--opener', '')
let args.params.partial = !empty(args.residual())
let args.params.cached = 0
let args.params.R = args.get('-R')
call gina#core#args#extend_treeish(a:git, args, args.pop(1))
call gina#core#args#extend_diff(a:git, args, args.params.rev)
" Enable --line/--col only when a path has specified
if args.params.path isnot# v:null
call gina#core#args#extend_line(a:git, args, args.pop('--line'))
call gina#core#args#extend_col(a:git, args, args.pop('--col'))
else
call args.pop('--line')
call args.pop('--col')
let args.params.line = v:null
let args.params.col = v:null
endif
return args.lock()
endfunction
function! s:init(args) abort
call gina#core#meta#set('args', a:args)
if exists('b:gina_initialized')
return
endif
let b:gina_initialized = 1
setlocal buftype=nowrite
setlocal noswapfile
setlocal nomodifiable
if a:args.params.partial
setlocal bufhidden=wipe
else
setlocal bufhidden&
endif
augroup gina_command_show_internal
autocmd! * <buffer>
autocmd BufReadCmd <buffer>
\ call gina#core#revelator#call(function('s:BufReadCmd'), [])
autocmd BufWinEnter <buffer> call setbufvar(str2nr(expand('<abuf>')), '&buflisted', 1)
autocmd BufWinLeave <buffer> call setbufvar(str2nr(expand('<abuf>')), '&buflisted', 0)
augroup END
if a:args.params.path is# v:null
nnoremap <buffer><silent> <Plug>(gina-diff-jump)
\ :<C-u>call gina#core#diffjump#jump()<CR>
nnoremap <buffer><silent> <Plug>(gina-diff-jump-split)
\ :<C-u>call gina#core#diffjump#jump('split')<CR>
nnoremap <buffer><silent> <Plug>(gina-diff-jump-vsplit)
\ :<C-u>call gina#core#diffjump#jump('vsplit')<CR>
if g:gina#command#show#use_default_mappings
nmap <buffer> <CR> <Plug>(gina-diff-jump)
endif
endif
endfunction
function! s:reassign_rev(git, args) abort
let rev = gina#core#treeish#resolve(a:git, a:args.params.rev)
let treeish = gina#core#treeish#build(rev, a:args.params.path)
call a:args.set(1, substitute(treeish, '^:0', '', ''))
return a:args
endfunction
function! s:BufReadCmd() abort
let git = gina#core#get_or_fail()
let args = gina#core#meta#get_or_fail('args')
let args = s:reassign_rev(git, args.clone())
let result = gina#process#call_or_fail(git, args)
call gina#core#buffer#assign_cmdarg()
call gina#core#writer#replace('%', 0, -1, result.stdout)
call gina#core#emitter#emit('command:called', s:SCHEME)
if args.params.path is# v:null
setlocal nomodeline
setfiletype git
else
call gina#util#doautocmd('BufRead')
endif
endfunction
" Config ---------------------------------------------------------------------
call gina#config(expand('<sfile>'), {
\ 'use_default_mappings': 1,
\})

View File

@ -0,0 +1,94 @@
let s:SCHEME = gina#command#scheme(expand('<sfile>'))
function! gina#command#stash#call(range, args, mods) abort
call gina#core#options#help_if_necessary(a:args, s:get_options_save())
let git = gina#core#get_or_fail()
let command = a:args.get(1, 'save')
if command ==# 'show'
return gina#command#stash#show#call(a:range, a:args, a:mods)
elseif command ==# 'list'
return gina#command#stash#list#call(a:range, a:args, a:mods)
endif
return gina#command#_raw#call(a:range, a:args, a:mods)
endfunction
function! gina#command#stash#complete(arglead, cmdline, cursorpos) abort
let args = gina#core#args#new(matchstr(a:cmdline, '^.*\ze .*'))
if args.get(1) ==# 'list'
return gina#command#stash#list#complete(a:arglead, a:cmdline, a:cursorpos)
elseif args.get(1) ==# 'show'
return gina#command#stash#show#complete(a:arglead, a:cmdline, a:cursorpos)
elseif args.get(1) =~# '^\%(save\|\)$'
if a:arglead =~# '^-'
let options = s:get_options_save()
return options.complete(a:arglead, a:cmdline, a:cursorpos)
endif
elseif args.get(1) ==# 'drop'
return gina#complete#stash#any(a:arglead, a:cmdline, a:cursorpos)
elseif args.get(1) =~# '^\%(pop\|apply\)$'
if a:arglead =~# '^-' || !empty(args.get(2))
let options = s:get_options_pop()
return options.complete(a:arglead, a:cmdline, a:cursorpos)
endif
return gina#complete#stash#any(a:arglead, a:cmdline, a:cursorpos)
elseif args.get(1) ==# 'branch'
if empty(args.get(2))
return gina#complete#commit#branch(a:arglead, a:cmdline, a:cursorpos)
endif
return gina#complete#stash#any(a:arglead, a:cmdline, a:cursorpos)
endif
return gina#util#filter(a:arglead, [
\ 'list',
\ 'show',
\ 'drop',
\ 'pop',
\ 'apply',
\ 'branch',
\ 'save',
\ 'clear',
\ 'create',
\ 'store',
\])
endfunction
" Private --------------------------------------------------------------------
function! s:get_options_save() abort
let options = gina#core#options#new()
call options.define(
\ '-h|--help',
\ 'Show this help.',
\)
call options.define(
\ '-k|--keep-index',
\ 'Left intact all changes already added to the index',
\)
call options.define(
\ '--no-keep-index',
\ 'Do not left intact all changes already added to the index',
\)
call options.define(
\ '-a|--all',
\ 'The ignored files and untracked files are stashed and cleaned',
\)
call options.define(
\ '--include-untracked',
\ 'The untracked files are stashed and cleaned',
\)
return options
endfunction
function! s:get_options_pop() abort
let options = gina#core#options#new()
call options.define(
\ '-h|--help',
\ 'Show this help.',
\)
call options.define(
\ '--index',
\ 'Tries to reinstate not only the working tree but also index',
\)
return options
endfunction

View File

@ -0,0 +1,133 @@
let s:SCHEME = 'stash'
function! gina#command#stash#list#call(range, args, mods) abort
let git = gina#core#get_or_fail()
let args = s:build_args(git, a:args)
let bufname = gina#core#buffer#bufname(git, s:SCHEME)
call gina#core#buffer#open(bufname, {
\ 'mods': a:mods,
\ 'group': args.params.group,
\ 'opener': args.params.opener,
\ 'cmdarg': args.params.cmdarg,
\ 'callback': {
\ 'fn': function('s:init'),
\ 'args': [args],
\ }
\})
endfunction
function! gina#command#stash#list#complete(arglead, cmdline, cursorpos) abort
let args = gina#core#args#new(matchstr(a:cmdline, '^.*\ze .*'))
if a:arglead =~# '^-'
let options = s:get_options()
return options.complete(a:arglead, a:cmdline, a:cursorpos)
endif
return []
endfunction
" Private --------------------------------------------------------------------
function! s:get_options() abort
let options = gina#core#options#new()
call options.define(
\ '-h|--help',
\ 'Show this help.',
\)
call options.define(
\ '--opener=',
\ 'A Vim command to open a new buffer.',
\ ['edit', 'split', 'vsplit', 'tabedit', 'pedit'],
\)
call options.define(
\ '--group=',
\ 'A window group name used for the buffer.',
\)
call options.define(
\ '--follow',
\ 'Continue listing the history of a file beyond renames',
\)
return options
endfunction
function! s:build_args(git, args) abort
let args = a:args.clone()
let args.params.group = args.pop('--group', '')
let args.params.opener = args.pop('--opener', '')
return args.lock()
endfunction
function! s:init(args) abort
call gina#core#meta#set('args', a:args)
if exists('b:gina_initialized')
return
endif
let b:gina_initialized = 1
setlocal buftype=nofile
setlocal bufhidden=hide
setlocal noswapfile
setlocal nomodifiable
setlocal autoread
" Attach modules
call gina#core#locator#attach()
call gina#action#attach(function('s:get_candidates'), {
\ 'markable': 1,
\})
augroup gina_command_stash_list_internal
autocmd! * <buffer>
autocmd BufReadCmd <buffer>
\ call gina#core#revelator#call(function('s:BufReadCmd'), [])
augroup END
endfunction
function! s:BufReadCmd() abort
let git = gina#core#get_or_fail()
let args = gina#core#meta#get_or_fail('args')
let pipe = gina#process#pipe#stream(s:writer)
call gina#core#buffer#assign_cmdarg()
call gina#process#open(git, args, pipe)
setlocal filetype=gina-stash-list
endfunction
function! s:get_candidates(fline, lline) abort
let candidates = map(
\ getline(a:fline, a:lline),
\ 's:parse_record(v:val)'
\)
return filter(candidates, '!empty(v:val)')
endfunction
function! s:parse_record(record) abort
let stash = matchstr(a:record, '^[^:]\+')
return {
\ 'word': a:record,
\ 'rev': stash,
\ 'stash': stash,
\}
endfunction
" Writer ---------------------------------------------------------------------
function! s:_writer_on_exit() abort dict
call call(s:original_writer.on_exit, [], self)
call gina#core#emitter#emit('command:called', s:SCHEME)
endfunction
let s:original_writer = gina#process#pipe#stream_writer()
let s:writer = extend(deepcopy(s:original_writer), {
\ 'on_exit': function('s:_writer_on_exit'),
\})
" Config ---------------------------------------------------------------------
call gina#config(expand('<sfile>'), {
\ 'use_default_aliases': 1,
\ 'use_default_mappings': 1,
\})

View File

@ -0,0 +1,139 @@
let s:String = vital#gina#import('Data.String')
let s:SCHEME = 'stash'
function! gina#command#stash#show#call(range, args, mods) abort
let git = gina#core#get_or_fail()
let args = s:build_args(git, a:args)
let bufname = gina#core#buffer#bufname(git, s:SCHEME, {
\ 'rev': args.params.rev,
\ 'params': ['show'],
\})
call gina#core#buffer#open(bufname, {
\ 'mods': a:mods,
\ 'group': args.params.group,
\ 'opener': args.params.opener,
\ 'cmdarg': args.params.cmdarg,
\ 'callback': {
\ 'fn': function('s:init'),
\ 'args': [args],
\ }
\})
endfunction
function! gina#command#stash#show#complete(arglead, cmdline, cursorpos) abort
let args = gina#core#args#new(matchstr(a:cmdline, '^.*\ze .*'))
if a:arglead =~# '^-' || !empty(args.get(2))
let options = s:get_options()
return options.complete(a:arglead, a:cmdline, a:cursorpos)
endif
return gina#complete#stash#any(a:arglead, a:cmdline, a:cursorpos)
endfunction
" Private --------------------------------------------------------------------
function! s:get_options() abort
let options = gina#core#options#new()
call options.define(
\ '-h|--help',
\ 'Show this help.',
\)
call options.define(
\ '--opener=',
\ 'A Vim command to open a new buffer.',
\ ['edit', 'split', 'vsplit', 'tabedit', 'pedit'],
\)
call options.define(
\ '--group=',
\ 'A window group name.',
\)
return options
endfunction
function! s:build_args(git, args) abort
let args = a:args.clone()
let args.params.group = args.pop('--group', '')
let args.params.opener = args.pop('--opener', '')
let args.params.rev = args.get(2, 'stash@{0}')
call args.set('--numstat', 1)
call args.set(1, 'show')
call args.set(2, args.params.rev)
return args.lock()
endfunction
function! s:init(args) abort
call gina#core#meta#set('args', a:args)
if exists('b:gina_initialized')
return
endif
let b:gina_initialized = 1
setlocal buftype=nofile
setlocal bufhidden=hide
setlocal noswapfile
setlocal nomodifiable
" Attach modules
call gina#core#locator#attach()
call gina#action#attach(function('s:get_candidates'))
augroup gina_command_stash_show_internal
autocmd! * <buffer>
autocmd BufReadCmd <buffer>
\ call gina#core#revelator#call(function('s:BufReadCmd'), [])
augroup END
endfunction
function! s:BufReadCmd() abort
let git = gina#core#get_or_fail()
let args = gina#core#meta#get_or_fail('args')
let pipe = gina#process#pipe#stream(s:writer)
call gina#core#buffer#assign_cmdarg()
call gina#process#open(git, args, pipe)
setlocal filetype=gina-stash-show
endfunction
function! s:get_candidates(fline, lline) abort
let args = gina#core#meta#get_or_fail('args')
let rev = args.params.rev
let candidates = map(
\ getline(a:fline, a:lline),
\ 's:parse_record(v:val, rev)'
\)
return filter(candidates, '!empty(v:val)')
endfunction
function! s:parse_record(record, rev) abort
let m = matchlist(
\ a:record,
\ '^\(\d\+\)\s\+\(\d\+\)\s\+\(.\+\)$'
\)
return empty(m) ? {} : {
\ 'word': a:record,
\ 'added': m[1],
\ 'removed': m[2],
\ 'path': m[3],
\ 'rev': a:rev,
\}
endfunction
" Writer ---------------------------------------------------------------------
function! s:_writer_on_exit() abort dict
call call(s:original_writer.on_exit, [], self)
call gina#core#emitter#emit('command:called', s:SCHEME)
endfunction
let s:original_writer = gina#process#pipe#stream_writer()
let s:writer = extend(deepcopy(s:original_writer), {
\ 'on_exit': function('s:_writer_on_exit'),
\})
" Config ---------------------------------------------------------------------
call gina#config(expand('<sfile>'), {
\ 'use_default_aliases': 1,
\ 'use_default_mappings': 1,
\})

View File

@ -0,0 +1,244 @@
let s:Path = vital#gina#import('System.Filepath')
let s:String = vital#gina#import('Data.String')
let s:SCHEME = gina#command#scheme(expand('<sfile>'))
function! gina#command#status#call(range, args, mods) abort
call gina#core#options#help_if_necessary(a:args, s:get_options())
let git = gina#core#get_or_fail()
let args = s:build_args(git, a:args)
let bufname = gina#core#buffer#bufname(git, s:SCHEME, {
\ 'params': [
\ args.params.partial ? '--' : '',
\ ],
\})
call gina#core#buffer#open(bufname, {
\ 'mods': a:mods,
\ 'group': args.params.group,
\ 'opener': args.params.opener,
\ 'cmdarg': args.params.cmdarg,
\ 'callback': {
\ 'fn': function('s:init'),
\ 'args': [args],
\ }
\})
endfunction
function! gina#command#status#complete(arglead, cmdline, cursorpos) abort
let args = gina#core#args#new(matchstr(a:cmdline, '^.*\ze .*'))
if a:arglead[0] ==# '-' || !empty(args.get(1))
let options = s:get_options()
return options.complete(a:arglead, a:cmdline, a:cursorpos)
endif
return gina#complete#filename#any(a:arglead, a:cmdline, a:cursorpos)
endfunction
" Private --------------------------------------------------------------------
function! s:get_options() abort
let options = gina#core#options#new()
call options.define(
\ '-h|--help',
\ 'Show this help.',
\)
call options.define(
\ '--opener=',
\ 'A Vim command to open a new buffer.',
\ ['edit', 'split', 'vsplit', 'tabedit', 'pedit'],
\)
call options.define(
\ '--group=',
\ 'A window group name used for the buffer.',
\)
return options
endfunction
function! s:build_args(git, args) abort
let args = a:args.clone()
let args.params.group = args.pop('--group', '')
let args.params.opener = args.pop('--opener', '')
let args.params.partial = !empty(args.residual())
return args.lock()
endfunction
function! s:init(args) abort
call gina#core#meta#set('args', a:args)
if exists('b:gina_initialized')
return
endif
let b:gina_initialized = 1
setlocal buftype=nofile
setlocal bufhidden=hide
setlocal noswapfile
setlocal nomodifiable
setlocal autoread
" Attach modules
call gina#core#locator#attach()
call gina#action#attach(function('s:get_candidates'), {
\ 'markable': 1,
\})
augroup gina_command_status_internal
autocmd! * <buffer>
autocmd BufReadCmd <buffer>
\ call gina#core#revelator#call(function('s:BufReadCmd'), [])
augroup END
endfunction
function! s:BufReadCmd() abort
let git = gina#core#get_or_fail()
let args = gina#core#meta#get_or_fail('args')
let pipe = gina#process#pipe#stream(s:writer)
call gina#core#buffer#assign_cmdarg()
call gina#process#open(git, args, pipe)
setlocal filetype=gina-status
endfunction
function! s:compare_record(a, b) abort
let a = matchstr(a:a, '...\zs.*')
let b = matchstr(a:b, '...\zs.*')
return a ==# b ? 0 : a > b ? 1 : -1
endfunction
function! s:get_candidates(fline, lline) abort
let git = gina#core#get_or_fail()
let args = gina#core#meta#get_or_fail('args')
let conf = gina#core#repo#config(git)
let residual = args.residual()
if args.get('-s|--short') || get(conf, 'status.short', 'false') ==? 'true'
let candidates = map(
\ getline(a:fline, a:lline),
\ 's:parse_record_short(v:val, residual)'
\)
else
let candidates = map(
\ getline(a:fline, a:lline),
\ 's:parse_record_normal(v:val, residual)'
\)
endif
return filter(candidates, '!empty(v:val)')
endfunction
function! s:parse_record_short(record, residual) abort
let record = s:String.remove_ansi_sequences(a:record)
let m = matchlist(
\ record,
\ '^\(..\) \("[^"]\{-}"\|.\{-}\)\%( -> \("[^"]\{-}"\|[^ ]\+\)\)\?$'
\)
if empty(m) || m[1] ==# '##'
return {}
endif
let candidate = {
\ 'word': record,
\ 'abbr': a:record,
\ 'sign': m[1],
\ 'residual': a:residual,
\}
if len(m) && !empty(m[3])
return extend(candidate, {
\ 'path': s:strip_quotes(m[3]),
\ 'path1': s:strip_quotes(m[2]),
\ 'path2': s:strip_quotes(m[3]),
\})
else
return extend(candidate, {
\ 'path': s:strip_quotes(m[2]),
\ 'path1': s:strip_quotes(m[2]),
\ 'path2': '',
\})
endif
endfunction
function! s:parse_record_normal(record, residual) abort
let signs = {
\ 'modified': 'M',
\ 'new file': 'A',
\ 'deleted': 'D',
\ 'renamed': 'R',
\ 'copied': 'C',
\ 'both added': 'AA',
\ 'both deleted': 'DD',
\ 'both modified': 'UU',
\ 'added by us': 'AU',
\ 'added by them': 'UA',
\ 'deleted by us': 'DU',
\ 'deleted by them': 'UD',
\}
let record = s:String.remove_ansi_sequences(a:record)
if record !~# '^.\?\t'
return {}
endif
let m = matchlist(record, printf(
\ '^.\?\s\+\(%s\):\s\+\("[^"]\{-}"\|.\{-}\)\%%( -> \("[^"]\{-}"\|[^ ]\+\)\)\?$',
\ join(keys(signs), '\|')
\))
if empty(m)
" Untracked files
let path = s:strip_quotes(substitute(record, '^\t', '', ''))
return {
\ 'word': record,
\ 'abbr': a:record,
\ 'sign': '??',
\ 'residual': a:residual,
\ 'path': path,
\ 'path1': path,
\ 'path2': '',
\}
endif
if search('^Unmerged paths:', 'bnW') != 0
" Conflict
let sign = signs[m[1]]
elseif search('^Changes not staged for commit:', 'bnW') != 0
" Unstaged
let sign = ' ' . signs[m[1]]
else
" Staged
let sign = signs[m[1]] . ' '
endif
let candidate = {
\ 'word': record,
\ 'abbr': a:record,
\ 'sign': sign,
\ 'residual': a:residual,
\}
if len(m) && !empty(m[3])
return extend(candidate, {
\ 'path': s:strip_quotes(m[3]),
\ 'path1': s:strip_quotes(m[2]),
\ 'path2': s:strip_quotes(m[3]),
\})
else
return extend(candidate, {
\ 'path': s:strip_quotes(m[2]),
\ 'path1': s:strip_quotes(m[2]),
\ 'path2': '',
\})
endif
endfunction
function! s:strip_quotes(str) abort
return a:str =~# '^\%(".*"\|''.*''\)$' ? a:str[1:-2] : a:str
endfunction
" Writer ---------------------------------------------------------------------
function! s:_writer_on_exit() abort dict
call call(s:original_writer.on_exit, [], self)
call gina#core#emitter#emit('command:called', s:SCHEME)
endfunction
let s:original_writer = gina#process#pipe#stream_writer()
let s:writer = extend(deepcopy(s:original_writer), {
\ 'on_exit': function('s:_writer_on_exit'),
\})
" Config ---------------------------------------------------------------------
call gina#config(expand('<sfile>'), {
\ 'use_default_aliases': 1,
\ 'use_default_mappings': 1,
\})

View File

@ -0,0 +1,246 @@
let s:SCHEME = gina#command#scheme(expand('<sfile>'))
function! gina#command#tag#call(range, args, mods) abort
call gina#core#options#help_if_necessary(a:args, s:get_options())
if s:is_edit_command(a:args)
return gina#command#tag#edit#call(a:range, a:args, a:mods)
elseif s:is_raw_command(a:args)
" Remove non git options
let args = a:args.clone()
call args.pop('--group')
call args.pop('--opener')
call args.pop('--restore')
" Call raw git command
return gina#command#_raw#call(a:range, args, a:mods)
endif
" list
let git = gina#core#get_or_fail()
let args = s:build_args(git, a:args)
let bufname = gina#core#buffer#bufname(git, s:SCHEME)
call gina#core#buffer#open(bufname, {
\ 'mods': a:mods,
\ 'group': args.params.group,
\ 'opener': args.params.opener,
\ 'cmdarg': args.params.cmdarg,
\ 'callback': {
\ 'fn': function('s:init'),
\ 'args': [args],
\ }
\})
endfunction
function! gina#command#tag#complete(arglead, cmdline, cursorpos) abort
let args = gina#core#args#new(matchstr(a:cmdline, '^.*\ze .*'))
if a:arglead[0] ==# '-'
let options = s:get_options()
return options.complete(a:arglead, a:cmdline, a:cursorpos)
elseif s:is_edit_command(args)
return gina#complete#commit#any(a:arglead, a:cmdline, a:cursorpos)
elseif s:is_raw_command(args)
return gina#complete#tag#any(a:arglead, a:cmdline, a:cursorpos)
endif
return []
endfunction
" Private --------------------------------------------------------------------
function! s:get_options() abort
let options = gina#core#options#new()
call options.define(
\ '-h|--help',
\ 'Show this help.',
\)
call options.define(
\ '--opener=',
\ 'A Vim command to open a new buffer.',
\ ['edit', 'split', 'vsplit', 'tabedit', 'pedit'],
\)
call options.define(
\ '--group=',
\ 'A window group name used for the buffer.',
\)
call options.define(
\ '--restore',
\ 'Restore the previous buffer when the window is closed.',
\)
call options.define(
\ '-a|--annotate',
\ 'Make an unsigned, annotated tag object',
\)
call options.define(
\ '-s|--sign',
\ 'Make a GPG-signed tag, using the default e-mail address key',
\)
call options.define(
\ '-U|--local-user=',
\ 'Make a GPG-signed tag, using the given key',
\)
call options.define(
\ '-f|--force',
\ 'Replace an existing tag with the given name',
\)
call options.define(
\ '-d|--delete',
\ 'Delete existing tags with the given name',
\)
call options.define(
\ '-n',
\ 'Print <n> lines from the annotation when using -l',
\)
call options.define(
\ '-l|--list=',
\ 'List tags with names that match the given pattern',
\)
call options.define(
\ '--sort=',
\ 'Sort based on the key given',
\)
call options.define(
\ '--contains=',
\ 'Only listtags which contains the specified commit',
\)
call options.define(
\ '--points-at=',
\ 'Only list tags of the given object',
\)
call options.define(
\ '-m|--message=',
\ 'Use the given tag message',
\)
call options.define(
\ '-F|--file=',
\ 'Take thetag message from the given file',
\)
call options.define(
\ '--cleanup=',
\ 'Set how thetag message is cleaned up',
\)
call options.define(
\ '--create-reflog',
\ 'Createa reflog for the tag',
\)
call options.define(
\ '--merged=',
\ 'Only list tags whose tips are reachable from the given commit',
\)
call options.define(
\ '--no-merged=',
\ 'Only list tags whose tips are not reachable from the given commit',
\)
return options
endfunction
function! s:is_edit_command(args) abort
if a:args.get('-a|--annotate')
return 1
elseif a:args.get('-s|--sign')
return 1
elseif !empty(a:args.get('-u|--local-user'))
return 1
endif
return 0
endfunction
function! s:is_raw_command(args) abort
if a:args.get('-l|--list')
return 0
elseif a:args.get('-d|--delete')
return 1
elseif a:args.get('-v|--verify')
return 1
elseif a:args.get('-m|--message')
" -a/--annotate is implied
return 1
elseif a:args.get('-f|--file')
" -a/--annotate is implied
return 1
elseif !empty(a:args.get(1))
" lightweight tag
return 1
endif
return 0
endfunction
function! s:build_args(git, args) abort
let args = a:args.clone()
let args.params.group = args.pop('--group', '')
let args.params.opener = args.pop('--opener', '')
" Remove unused option
call args.pop('--restore')
return args.lock()
endfunction
function! s:init(args) abort
call gina#core#meta#set('args', a:args)
if exists('b:gina_initialized')
return
endif
let b:gina_initialized = 1
setlocal buftype=nofile
setlocal bufhidden=hide
setlocal noswapfile
setlocal nomodifiable
setlocal autoread
" Attach modules
call gina#core#locator#attach()
call gina#action#attach(function('s:get_candidates'), {
\ 'markable': 1,
\})
augroup gina_command_tag_list_internal
autocmd! * <buffer>
autocmd BufReadCmd <buffer>
\ call gina#core#revelator#call(function('s:BufReadCmd'), [])
augroup END
endfunction
function! s:BufReadCmd() abort
let git = gina#core#get_or_fail()
let args = gina#core#meta#get_or_fail('args')
let pipe = gina#process#pipe#stream(s:writer)
call gina#core#buffer#assign_cmdarg()
call gina#process#open(git, args, pipe)
setlocal filetype=gina-tag
endfunction
function! s:get_candidates(fline, lline) abort
let candidates = map(
\ getline(a:fline, a:lline),
\ 's:parse_record(v:val)'
\)
return filter(candidates, '!empty(v:val)')
endfunction
function! s:parse_record(record) abort
return {
\ 'word': a:record,
\ 'branch': a:record,
\ 'rev': a:record,
\ 'tag': a:record,
\}
endfunction
" Writer ---------------------------------------------------------------------
function! s:_writer_on_exit() abort dict
call call(s:original_writer.on_exit, [], self)
call gina#core#emitter#emit('command:called', s:SCHEME)
endfunction
let s:original_writer = gina#process#pipe#stream_writer()
let s:writer = extend(deepcopy(s:original_writer), {
\ 'on_exit': function('s:_writer_on_exit'),
\})
" Config ---------------------------------------------------------------------
call gina#config(expand('<sfile>'), {
\ 'use_default_aliases': 1,
\ 'use_default_mappings': 1,
\})

View File

@ -0,0 +1,203 @@
let s:String = vital#gina#import('Data.String')
let s:Git = vital#gina#import('Git')
let s:SCHEME = 'tag'
function! gina#command#tag#edit#call(range, args, mods) abort
let git = gina#core#get_or_fail()
let args = s:build_args(a:args)
let bufname = gina#core#buffer#bufname(git, s:SCHEME, {
\ 'params': ['edit']
\})
call gina#core#buffer#open(bufname, {
\ 'mods': a:mods,
\ 'group': args.params.group,
\ 'opener': args.params.opener,
\ 'cmdarg': args.params.cmdarg,
\ 'callback': {
\ 'fn': function('s:init'),
\ 'args': [args],
\ }
\})
endfunction
" Private --------------------------------------------------------------------
function! s:build_args(args) abort
let args = a:args.clone()
let args.params.group = args.pop('--group', '')
let args.params.opener = args.pop('--opener', '')
let args.params.restore = args.pop(
\ '--restore',
\ empty(args.params.opener) || args.params.opener ==# 'edit',
\)
return args.lock()
endfunction
function! s:init(args) abort
call gina#core#meta#set('args', a:args)
silent! unlet b:gina_QuitPre
silent! unlet b:gina_BufWriteCmd
if exists('b:gina_initialized')
return
endif
let b:gina_initialized = 1
setlocal nobuflisted
setlocal buftype=acwrite
setlocal bufhidden=hide
setlocal noswapfile
setlocal modifiable
augroup gina_command_tag_edit_internal
autocmd! * <buffer>
autocmd BufReadCmd <buffer> call s:BufReadCmd()
autocmd BufWriteCmd <buffer> call s:BufWriteCmd()
autocmd QuitPre <buffer> call s:QuitPre()
autocmd WinLeave <buffer> call s:WinLeave()
autocmd WinEnter <buffer> silent! unlet! b:gina_QuitPre
augroup END
endfunction
function! s:BufReadCmd() abort
let git = gina#core#get_or_fail()
let args = gina#core#meta#get_or_fail('args')
let content = gina#core#revelator#call(
\ function('s:get_tagmsg_template'),
\ [git, args]
\)
call gina#core#buffer#assign_cmdarg()
call gina#core#writer#replace('%', 0, -1, content)
call gina#core#emitter#emit('command:called', s:SCHEME)
setlocal filetype=conf
endfunction
function! s:BufWriteCmd() abort
let b:gina_BufWriteCmd = 1
setlocal nomodified
endfunction
function! s:QuitPre() abort
" Restore the previous buffer if 'restore' is specified
let args = gina#core#meta#get('args', v:null)
if args isnot# v:null && get(args.params, 'restore')
let win_id = win_getid()
if bufnr('#') == -1
silent keepalt keepjumps 1new
else
silent keepalt keepjumps 1split #
endif
call win_gotoid(win_id)
endif
" Do not perform commit when user hit :q!
if histget('cmd', -1) !~# '^q\%[uit]!'
let b:gina_QuitPre = 1
" If this is a last window, open a new window to prevent quit
if tabpagenr('$') == 1 && winnr('$') == 1
let win_id = win_getid()
silent keepalt keepjumps 1new
call win_gotoid(win_id)
endif
endif
" Clear the flag set by :w but keep it when user hit :wq
if histget('cmd', -1) !~# '^\(wq\|x\%[it]\|exi\%[t]\)'
silent! unlet b:gina_BufWriteCmd
endif
endfunction
" NOTE:
" :w -- BufWriteCmd
" <C-w>p -- WinLeave
" :wq -- QuitPre -> BufWriteCmd -> WinLeave (-8.2.2899)
" BufWriteCmd -> QuitPre -> WinLeave (8.2.2900-)
" :q -- QuitPre -> WinLeave
function! s:WinLeave() abort
if exists('b:gina_QuitPre')
let git = gina#core#get_or_fail()
let args = gina#core#meta#get_or_fail('args')
if exists('b:gina_BufWriteCmd')
" User execute 'wq' so do not confirm
call gina#core#revelator#call(
\ function('s:apply_tagmsg'),
\ [git, args]
\)
else
" User execute 'q' so confirm
call gina#core#revelator#call(
\ function('s:apply_tagmsg_confirm'),
\ [git, args]
\)
endif
endif
endfunction
function! s:get_tagmsg_template(git, args) abort
let config = gina#core#repo#config(a:git)
let comment = get(config, 'core.commentchar', '#')
let tagname = a:args.get(1)
if a:args.get('--cleanup', 'strip') ==# 'strip'
let template = [
\ '',
\ 'Write a message for tag:',
\ ' ' . tagname,
\ printf(
\ 'Lines starting with ''%s'' will be ignored.',
\ comment,
\ ),
\]
else
let template = [
\ '',
\ 'Write a message for tag:',
\ ' ' . tagname,
\ printf(
\ 'Lines starting with ''%s'' will be kept; you may remove them yourself if you want to.',
\ comment,
\ ),
\]
endif
call map(template, 'comment . '' '' . v:val')
return [''] + template
endfunction
function! s:apply_tagmsg(git, args) abort
let args = a:args.clone()
let content = getline(1, '$')
let tempfile = s:Git.resolve(a:git, 'TAG_EDITMSG')
try
call writefile(content, tempfile)
call args.set('--cleanup', args.get('--cleanup', 'strip'))
call args.set('-F|--file', tempfile)
call args.pop('-m|--message')
let result = gina#process#call(a:git, args)
call gina#process#inform(result)
call gina#core#emitter#emit('command:called:tag')
finally
call delete(tempfile)
endtry
endfunction
function! s:apply_tagmsg_confirm(git, args) abort
if gina#core#console#confirm('Do you want to create a tag?', 'y')
call s:apply_tagmsg(a:git, a:args)
else
redraw | echo ''
endif
endfunction
" Event ----------------------------------------------------------------------
function! s:on_command_called_tag(...) abort
call gina#core#emitter#emit('modified:delay')
endfunction
if !exists('s:subscribed')
let s:subscribed = 1
call gina#core#emitter#subscribe(
\ 'command:called:tag',
\ function('s:on_command_called_tag')
\)
endif

View File

@ -0,0 +1,88 @@
let s:Git = vital#gina#import('Git')
let s:Path = vital#gina#import('System.Filepath')
let s:Store = vital#gina#import('System.Store')
function! gina#complete#commit#any(arglead, cmdline, cursorpos) abort
let candidates = ['']
let candidates += gina#complete#commit#branch(a:arglead, a:cmdline, a:cursorpos)
if !empty(a:arglead)
let candidates += gina#complete#commit#hashref(a:arglead, a:cmdline, a:cursorpos)
endif
return s:filter(a:arglead, candidates)
endfunction
function! gina#complete#commit#branch(arglead, cmdline, cursorpos) abort
let git = gina#core#get_or_fail()
let slug = eval(s:Store.get_slug_expr())
let store = s:Store.of(s:Git.resolve(git, 'config'))
let candidates = store.get(slug, [])
if empty(candidates)
let candidates = s:get_available_branches(git, ['--all'])
call store.set(slug, candidates)
endif
return s:filter(a:arglead, candidates)
endfunction
function! gina#complete#commit#local_branch(arglead, cmdline, cursorpos) abort
let git = gina#core#get_or_fail()
let slug = eval(s:Store.get_slug_expr())
let store = s:Store.of(s:Git.resolve(git, 'config'))
let candidates = store.get(slug, [])
if empty(candidates)
let candidates = s:get_available_branches(git, [])
call store.set(slug, candidates)
endif
return s:filter(a:arglead, candidates)
endfunction
function! gina#complete#commit#remote_branch(arglead, cmdline, cursorpos) abort
let git = gina#core#get_or_fail()
let slug = eval(s:Store.get_slug_expr())
let store = s:Store.of(s:Git.resolve(git, 'config'))
let candidates = store.get(slug, [])
if empty(candidates)
let candidates = s:get_available_branches(git, ['--remotes'])
call store.set(slug, candidates)
endif
return s:filter(a:arglead, candidates)
endfunction
function! gina#complete#commit#hashref(arglead, cmdline, cursorpos) abort
let git = gina#core#get_or_fail()
let slug = eval(s:Store.get_slug_expr())
let store = s:Store.of(s:Git.resolve(git, 'config'))
let candidates = store.get(slug, [])
if empty(candidates)
let candidates = s:get_available_commits(git, [])
call store.set(slug, candidates)
endif
return s:filter(a:arglead, candidates)
endfunction
" Public ---------------------------------------------------------------------
function! s:filter(arglead, candidates) abort
return gina#util#filter(a:arglead, a:candidates)
endfunction
function! s:get_available_commits(git, args) abort
let args = ['log', '--pretty=%h'] + a:args
let result = gina#process#call(a:git, args)
if result.status
return []
endif
return result.stdout
endfunction
function! s:get_available_branches(git, args) abort
let args = ['branch', '--no-color', '--list'] + a:args
let result = gina#process#call(a:git, args)
if result.status
return []
endif
let candidates = filter(copy(result.stdout), 'v:val !~# ''^.* -> .*$''')
call map(candidates, 'matchstr(v:val, ''^..\zs.*$'')')
call map(candidates, 'substitute(v:val, ''^remotes/'', '''', '''')')
return ['HEAD'] + filter(candidates, '!empty(v:val)')
endfunction

View File

@ -0,0 +1,101 @@
let s:Cache = vital#gina#import('System.Cache.Memory')
let s:Path = vital#gina#import('System.Filepath')
function! gina#complete#common#opener(arglead, cmdline, cursorpos) abort
if a:arglead !~# '^--opener='
return []
endif
let candidates = [
\ 'split',
\ 'vsplit',
\ 'tabedit',
\ 'pedit',
\]
let prefix = '--opener='
return gina#util#filter(a:arglead, map(candidates, 'prefix . v:val'))
endfunction
function! gina#complete#common#treeish(arglead, cmdline, cursorpos) abort
if a:arglead =~# '^[^:]*:'
let revision = matchstr(a:arglead, '^[^:]*\ze:')
let candidates = gina#complete#filename#tracked(
\ matchstr(a:arglead, '^[^:]*:\zs.*'),
\ a:cmdline,
\ a:cursorpos,
\ revision,
\)
return map(candidates, 'revision . '':'' . v:val')
else
let candidates = gina#complete#range#any(a:arglead, a:cmdline, a:cursorpos)
return map(candidates, 'v:val . '':''')
endif
endfunction
function! gina#complete#common#command(arglead, cmdline, cursorpos) abort
let cache = s:get_cache()
if !cache.has('command_names')
call cache.set('command_names', s:get_command_names())
endif
let command_names = cache.get('command_names')
return gina#util#filter(a:arglead, command_names, '^_')
endfunction
function! gina#complete#common#raw_command(arglead, cmdline, cursorpos) abort
return gina#util#filter(a:arglead, [
\ 'add',
\ 'bisect',
\ 'branch',
\ 'checkout',
\ 'clone',
\ 'commit',
\ 'diff',
\ 'fetch',
\ 'grep',
\ 'init',
\ 'log',
\ 'merge',
\ 'mv',
\ 'pull',
\ 'push',
\ 'rebase',
\ 'reset',
\ 'restore',
\ 'rm',
\ 'show',
\ 'status',
\ 'switch',
\ 'tag',
\])
endfunction
function! gina#complete#common#remote(arglead, cmdline, cursorpos) abort
let git = gina#core#get_or_fail()
let result = gina#process#call(git, ['remote'])
if result.status
return []
endif
return gina#util#filter(a:arglead, result.stdout)
endfunction
" Private --------------------------------------------------------------------
function! s:get_cache() abort
if exists('s:cache')
return s:cache
endif
let s:cache = s:Cache.new()
return s:cache
endfunction
function! s:get_command_names() abort
let suffix = s:Path.realpath('autoload/gina/command/*.vim')
let command_names = []
for runtimepath in split(&runtimepath, ',')
let names = map(
\ glob(s:Path.join(runtimepath, suffix), 0, 1),
\ 'matchstr(fnamemodify(v:val, '':t''), ''^.\+\ze\.vim$'')',
\)
call extend(command_names, names)
endfor
return sort(command_names)
endfunction

View File

@ -0,0 +1,164 @@
let s:Git = vital#gina#import('Git')
let s:Path = vital#gina#import('System.Filepath')
let s:Store = vital#gina#import('System.Store')
let s:String = vital#gina#import('Data.String')
function! gina#complete#filename#directory(arglead, cmdline, cursorpos) abort
let separator = s:Path.separator()
return filter(
\ gina#complete#filename#any(a:arglead, a:cmdline, a:cursorpos),
\ 'v:val =~# separator . ''$'''
\)
endfunction
function! gina#complete#filename#any(arglead, cmdline, cursorpos) abort
let git = gina#core#get_or_fail()
let candidates = s:get_available_filenames(git, [
\ '--cached', '--others', '--', a:arglead . '*',
\])
return s:filter(a:arglead, candidates)
endfunction
function! gina#complete#filename#tracked(arglead, cmdline, cursorpos, ...) abort
let rev = a:0 ? a:1 : ''
let git = gina#core#get_or_fail()
let slug = eval(s:Store.get_slug_expr()) . rev
let store = s:Store.of([
\ s:Git.resolve(git, 'HEAD'),
\ s:Git.resolve(git, 'index'),
\])
let candidates = store.get(slug, [])
if empty(candidates)
let candidates = s:get_available_filenames(git, [], rev)
call store.set(slug, candidates)
endif
return s:filter(a:arglead, candidates)
endfunction
function! gina#complete#filename#cached(arglead, cmdline, cursorpos) abort
let git = gina#core#get_or_fail()
let slug = eval(s:Store.get_slug_expr())
let store = s:Store.of([
\ s:Git.resolve(git, 'HEAD'),
\ s:Git.resolve(git, 'index'),
\])
let candidates = store.get(slug, [])
if empty(candidates)
let candidates = s:get_available_filenames(git, ['--cached'])
call store.set(slug, candidates)
endif
return s:filter(a:arglead, candidates)
endfunction
function! gina#complete#filename#deleted(arglead, cmdline, cursorpos) abort
let git = gina#core#get_or_fail()
let slug = eval(s:Store.get_slug_expr())
let store = s:Store.of([
\ s:Git.resolve(git, 'HEAD'),
\ s:Git.resolve(git, 'index'),
\])
let candidates = store.get(slug, [])
if empty(candidates)
let candidates = s:get_available_filenames(git, ['--deleted'])
call store.set(slug, candidates)
endif
return s:filter(a:arglead, candidates)
endfunction
function! gina#complete#filename#modified(arglead, cmdline, cursorpos) abort
let git = gina#core#get_or_fail()
let slug = eval(s:Store.get_slug_expr())
let store = s:Store.of([
\ s:Git.resolve(git, 'HEAD'),
\ s:Git.resolve(git, 'index'),
\])
let candidates = store.get(slug, [])
if empty(candidates)
let candidates = s:get_available_filenames(git, ['--modified'])
call store.set(slug, candidates)
endif
return s:filter(a:arglead, candidates)
endfunction
function! gina#complete#filename#others(arglead, cmdline, cursorpos) abort
let git = gina#core#get_or_fail()
let candidates = s:get_available_filenames(git, [
\ '--others', '--', a:arglead . '*',
\])
return s:filter(a:arglead, candidates)
endfunction
function! gina#complete#filename#unstaged(arglead, cmdline, cursorpos) abort
let git = gina#core#get_or_fail()
let candidates = s:get_available_filenames(git, [
\ '--others',
\ '--modified',
\ '--',
\ a:arglead . '*',
\])
return s:filter(a:arglead, candidates)
endfunction
function! gina#complete#filename#conflicted(arglead, cmdline, cursorpos) abort
let git = gina#core#get_or_fail()
let slug = eval(s:Store.get_slug_expr())
let store = s:Store.of([
\ s:Git.resolve(git, 'HEAD'),
\ s:Git.resolve(git, 'index'),
\])
let candidates = store.get(slug, [])
if empty(candidates)
let result = gina#process#call(git, [
\ 'status',
\ '--porcelain',
\ '--ignore-submodules',
\])
if result.status
return []
endif
call filter(
\ candidates,
\ 'v:val[:1] =~# ''^\%(DD\|AU\|UD\|UA\|DU\|AA\|UU\)'''
\)
call map(candidates, 'matchstr(v:val, ''.. \zs.*'')')
call store.set(slug, candidates)
endif
return s:filter(a:arglead, candidates)
endfunction
" Private --------------------------------------------------------------------
function! s:filter(arglead, candidates) abort
let pattern = s:String.escape_pattern(a:arglead)
let separator = s:Path.separator()
let candidates = gina#util#filter(a:arglead, a:candidates, '^\.')
call map(
\ candidates,
\ printf(
\ 'matchstr(v:val, ''^%s[^%s]*%s\?\ze'')',
\ pattern, separator, separator
\ ),
\)
return uniq(candidates)
endfunction
function! s:get_available_filenames(git, args, ...) abort
let rev = a:0 ? a:1 : ''
if empty(rev)
let args = ['ls-files', '--full-name']
else
let args = [
\ 'ls-tree',
\ '--full-name',
\ '--full-tree',
\ '--name-only',
\ '-r',
\ rev,
\]
endif
let result = gina#process#call(a:git, args + a:args)
if result.status
return []
endif
return map(result.stdout, 'gina#core#repo#relpath(a:git, v:val)')
endfunction

View File

@ -0,0 +1,48 @@
function! gina#complete#range#any(arglead, cmdline, cursorpos) abort
return s:complete(
\ function('gina#complete#commit#any'),
\ a:arglead, a:cmdline, a:cursorpos,
\)
endfunction
function! gina#complete#range#branch(arglead, cmdline, cursorpos) abort
return s:complete(
\ function('gina#complete#commit#branch'),
\ a:arglead, a:cmdline, a:cursorpos,
\)
endfunction
function! gina#complete#range#local_branch(arglead, cmdline, cursorpos) abort
return s:complete(
\ function('gina#complete#commit#branch'),
\ a:arglead, a:cmdline, a:cursorpos,
\)
endfunction
function! gina#complete#range#remote_branch(arglead, cmdline, cursorpos) abort
return s:complete(
\ function('gina#complete#commit#branch'),
\ a:arglead, a:cmdline, a:cursorpos,
\)
endfunction
function! gina#complete#range#hashref(arglead, cmdline, cursorpos) abort
return s:complete(
\ function('gina#complete#commit#branch'),
\ a:arglead, a:cmdline, a:cursorpos,
\)
endfunction
" Private --------------------------------------------------------------------
function! s:complete(fn, arglead, cmdline, cursorpos) abort
if a:arglead =~# '^[^.]*\.\.\.\?'
let lhs = matchstr(a:arglead, '^[^.]*\.\.\.\?')
let rhs = matchstr(a:arglead, '^[^.]*\.\.\.\?\zs.*')
let candidates = a:fn(rhs, a:cmdline, a:cursorpos)
return map(candidates, 'lhs . v:val')
else
return a:fn(a:arglead, a:cmdline, a:cursorpos)
endif
endfunction

View File

@ -0,0 +1,33 @@
let s:Git = vital#gina#import('Git')
let s:Store = vital#gina#import('System.Store')
function! gina#complete#stash#any(arglead, cmdline, cursorpos) abort
let git = gina#core#get_or_fail()
let slug = eval(s:Store.get_slug_expr())
let store = s:Store.of([
\ s:Git.resolve(git, 'HEAD'),
\ s:Git.resolve(git, 'index'),
\])
let candidates = store.get(slug, [])
if empty(candidates)
let candidates = s:get_available_stashes(git, [])
call store.set(slug, candidates)
endif
return s:filter(a:arglead, candidates)
endfunction
" Public ---------------------------------------------------------------------
function! s:filter(arglead, candidates) abort
return gina#util#filter(a:arglead, a:candidates)
endfunction
function! s:get_available_stashes(git, args) abort
let args = ['stash', 'list', '--format=%gd'] + a:args
let result = gina#process#call(a:git, args)
if result.status
return []
endif
let candidates = copy(result.stdout)
return filter(candidates, '!empty(v:val)')
endfunction

View File

@ -0,0 +1,34 @@
let s:Git = vital#gina#import('Git')
let s:Store = vital#gina#import('System.Store')
function! gina#complete#tag#any(arglead, cmdline, cursorpos) abort
let git = gina#core#get_or_fail()
let slug = eval(s:Store.get_slug_expr())
let store = s:Store.of([
\ s:Git.resolve(git, 'index'),
\])
let candidates = store.get(slug, [])
if empty(candidates)
let candidates = s:get_available_tags(git, [])
call store.set(slug, candidates)
endif
return s:filter(a:arglead, candidates)
endfunction
" Public ---------------------------------------------------------------------
function! s:filter(arglead, candidates) abort
return gina#util#filter(a:arglead, a:candidates)
endfunction
function! s:get_available_tags(git, args) abort
let args = ['tag', '--list', '--format=%(refname:strip=2)'] + a:args
let result = gina#process#call(a:git, args)
if result.status
return []
endif
let candidates = result.stdout
return filter(candidates, '!empty(v:val)')
endfunction

View File

@ -0,0 +1,115 @@
scriptencoding utf-8
let s:Git = vital#gina#import('Git')
let s:Path = vital#gina#import('System.Filepath')
let s:Store = vital#gina#import('System.Store')
function! gina#component#repo#name() abort
let git = gina#core#get()
if empty(git)
return ''
endif
return fnamemodify(git.worktree, ':t')
endfunction
function! gina#component#repo#branch() abort
let git = gina#core#get()
if empty(git)
return ''
endif
let slug = eval(s:Store.get_slug_expr())
let store = s:Store.of([
\ s:Git.resolve(git, 'HEAD'),
\ s:Git.resolve(git, 'config'),
\])
let branch = store.get(slug, '')
if !empty(branch)
return branch
endif
let content = get(readfile(s:Git.resolve(git, 'HEAD')), 0, '')
if content =~# '^ref:\s\+refs/heads'
let branch = matchstr(content, '^ref:\s\+refs/heads/\zs.\+')
elseif content =~# '^ref:'
let branch = matchstr(content, '^ref:\s\+refs/\zs.\+')
elseif g:gina#component#repo#commit_length > 0
let branch = content[:(g:gina#component#repo#commit_length - 1)]
else
let branch = content
endif
call store.set(slug, branch)
return branch
endfunction
function! gina#component#repo#track() abort
let git = gina#core#get()
if empty(git)
return ''
endif
let slug = eval(s:Store.get_slug_expr())
let store = s:Store.of([
\ s:Git.resolve(git, 'HEAD'),
\ s:Git.resolve(git, 'config'),
\])
let branch = store.get(slug, '')
if !empty(branch)
return branch
endif
if !exists('s:track_job')
let pipe = gina#process#pipe#store()
let pipe.__on_exit = pipe.on_exit
let pipe.on_exit = funcref('s:track_on_exit', [store, slug])
let s:track_job = gina#process#open(git, [
\ 'rev-parse',
\ '--abbrev-ref',
\ '--symbolic-full-name',
\ '@{upstream}',
\], pipe)
endif
return ''
endfunction
function! gina#component#repo#preset(...) abort
let git = gina#core#get()
if empty(git)
return ''
endif
let kind = get(a:000, 0, 'ascii')
return call('s:preset_' . kind, [])
endfunction
" Private --------------------------------------------------------------------
function! s:track_on_exit(store, slug, exitval) abort dict
call self.__on_exit(a:exitval)
silent! unlet! s:track_job
if a:exitval
return
endif
call a:store.set(a:slug, get(self.stdout, 0))
endfunction
function! s:preset_ascii() abort
let name = gina#component#repo#name()
let branch = gina#component#repo#branch()
let track = gina#component#repo#track()
if empty(track)
return printf('%s [%s]', name, branch)
endif
return printf('%s [%s -> %s]', name, branch, track)
endfunction
function! s:preset_fancy() abort
let name = gina#component#repo#name()
let branch = gina#component#repo#branch()
let track = gina#component#repo#track()
if empty(track)
return printf('%s [%s]', name, branch)
endif
return printf('%s [%s → %s]', name, branch, track)
endfunction
call gina#config(expand('<sfile>'), {
\ 'commit_length': 0,
\})

View File

@ -0,0 +1,147 @@
scriptencoding utf-8
let s:Git = vital#gina#import('Git')
let s:Path = vital#gina#import('System.Filepath')
let s:Store = vital#gina#import('System.Store')
let s:SLUG = eval(s:Store.get_slug_expr())
function! gina#component#status#staged() abort
let git = gina#core#get()
if empty(git)
return ''
endif
let c = get(s:status_count(git), 'staged', 0)
return c == 0 ? '' : (c . '')
endfunction
function! gina#component#status#unstaged() abort
let git = gina#core#get()
if empty(git)
return ''
endif
let c = get(s:status_count(git), 'unstaged', 0)
return c == 0 ? '' : (c . '')
endfunction
function! gina#component#status#conflicted() abort
let git = gina#core#get()
if empty(git)
return ''
endif
let c = get(s:status_count(git), 'conflicted', 0)
return c == 0 ? '' : (c . '')
endfunction
function! gina#component#status#preset(...) abort
let git = gina#core#get()
if empty(git)
return ''
endif
let kind = get(a:000, 0, 'ascii')
return call('s:preset_' . kind, [])
endfunction
" Private --------------------------------------------------------------------
function! s:get_store(git) abort
let ref = get(s:Git.ref(a:git, 'HEAD'), 'path', 'HEAD')
let store = s:Store.of([
\ s:Git.resolve(a:git, 'index'),
\ s:Git.resolve(a:git, ref),
\])
return store
endfunction
function! s:status_count(git) abort
let store = s:get_store(a:git)
let status_count = store.get(s:SLUG, {})
if !empty(status_count)
return status_count
endif
if !exists('s:status_job')
let pipe = gina#process#pipe#store()
let pipe.__on_exit = pipe.on_exit
let pipe.on_exit = funcref('s:status_on_exit', [store])
let s:status_job = gina#process#open(a:git, [
\ 'status',
\ '--porcelain',
\ '--ignore-submodules',
\], pipe)
endif
return {
\ 'staged': 0,
\ 'unstaged': 0,
\ 'conflicted': 0,
\}
endfunction
function! s:status_on_exit(store, exitval) abort dict
call self.__on_exit(a:exitval)
silent! unlet! s:status_job
if a:exitval
return
endif
let status_count = {
\ 'staged': 0,
\ 'unstaged': 0,
\ 'conflicted': 0,
\}
for record in self.stdout
let sign = record[:1]
if sign =~# '^\%(DD\|AU\|UD\|UA\|DU\|AA\|UU\)$'
let status_count.conflicted += 1
elseif sign ==# '??' || sign ==# '!!'
continue
else
if sign =~# '^\S.$'
let status_count.staged += 1
endif
if sign =~# '^.\S$'
let status_count.unstaged += 1
endif
endif
endfor
call a:store.set(s:SLUG, status_count)
endfunction
function! s:preset_ascii() abort
let staged = gina#component#status#staged()
let unstaged = gina#component#status#unstaged()
let conflicted = gina#component#status#conflicted()
let staged = empty(staged) ? '' : ('<' . staged)
let unstaged = empty(unstaged) ? '' : ('>' . unstaged)
let conflicted = empty(conflicted) ? '' : ('x' . conflicted)
return join(filter([staged, unstaged, conflicted], '!empty(v:val)'))
endfunction
function! s:preset_fancy() abort
let staged = gina#component#status#staged()
let unstaged = gina#component#status#unstaged()
let conflicted = gina#component#status#conflicted()
let staged = empty(staged) ? '' : ('«' . staged)
let unstaged = empty(unstaged) ? '' : ('»' . unstaged)
let conflicted = empty(conflicted) ? '' : ('×' . conflicted)
return join(filter([staged, unstaged, conflicted], '!empty(v:val)'))
endfunction
" NOTE:
" Tracked files might be changed (unstaged) so remove cache when 'modified'
" event has called.
function! s:on_modified(...) abort
let git = gina#core#get()
if empty(git)
return
endif
let store = s:get_store(git)
call store.remove(s:SLUG)
endfunction
if !exists('s:subscribed')
let s:subscribed = 1
call gina#core#emitter#subscribe(
\ 'modified',
\ function('s:on_modified')
\)
endif

View File

@ -0,0 +1,144 @@
scriptencoding utf-8
let s:Git = vital#gina#import('Git')
let s:Path = vital#gina#import('System.Filepath')
let s:Store = vital#gina#import('System.Store')
function! gina#component#traffic#ahead() abort
let git = gina#core#get()
if empty(git)
return ''
endif
let slug = eval(s:Store.get_slug_expr())
let ref = get(s:Git.ref(git, 'HEAD'), 'path', 'HEAD')
let track = s:get_tracking_ref(git)
let store = s:Store.of([
\ s:Git.resolve(git, 'index'),
\ s:Git.resolve(git, ref),
\ s:Git.resolve(git, track),
\])
let ahead = store.get(slug, '')
if !empty(ahead)
return str2nr(ahead)
endif
if !exists('s:ahead_job')
let pipe = gina#process#pipe#store()
let pipe.__on_exit = pipe.on_exit
let pipe.on_exit = funcref('s:ahead_on_exit', [store, slug])
let s:ahead_job = gina#process#open(git, [
\ 'log',
\ '--oneline',
\ '@{upstream}..',
\], pipe)
endif
return ''
endfunction
function! gina#component#traffic#behind() abort
let git = gina#core#get()
if empty(git)
return ''
endif
let slug = eval(s:Store.get_slug_expr())
let ref = get(s:Git.ref(git, 'HEAD'), 'path', 'HEAD')
let track = s:get_tracking_ref(git)
let store = s:Store.of([
\ s:Git.resolve(git, 'index'),
\ s:Git.resolve(git, ref),
\ s:Git.resolve(git, track),
\])
let behind = store.get(slug, '')
if !empty(behind)
return str2nr(behind)
endif
if !exists('s:behind_job')
let pipe = gina#process#pipe#store()
let pipe.__on_exit = pipe.on_exit
let pipe.on_exit = funcref('s:behind_on_exit', [store, slug])
let s:behind_job = gina#process#open(git, [
\ 'log',
\ '--oneline',
\ '..@{upstream}',
\], pipe)
endif
return ''
endfunction
function! gina#component#traffic#preset(...) abort
let git = gina#core#get()
if empty(git)
return ''
endif
let kind = get(a:000, 0, 'ascii')
return call('s:preset_' . kind, [])
endfunction
" Private --------------------------------------------------------------------
function! s:ahead_on_exit(store, slug, exitval) abort dict
call self.__on_exit(a:exitval)
silent! unlet! s:ahead_job
if a:exitval
return
endif
let ahead = len(filter(copy(self.stdout), '!empty(v:val)')) . ''
call a:store.set(a:slug, ahead)
endfunction
function! s:behind_on_exit(store, slug, exitval) abort dict
call self.__on_exit(a:exitval)
silent! unlet! s:behind_job
if a:exitval
return
endif
let behind = len(filter(copy(self.stdout), '!empty(v:val)')) . ''
call a:store.set(a:slug, behind)
endfunction
function! s:get_tracking_ref(git) abort
let slug = eval(s:Store.get_slug_expr())
let store = s:Store.of([
\ s:Git.resolve(a:git, 'HEAD'),
\ s:Git.resolve(a:git, 'config'),
\])
let ref = store.get(slug, '')
if !empty(ref)
return ref
endif
if !exists('s:tracking_ref_job')
let pipe = gina#process#pipe#store()
let pipe.__on_exit = pipe.on_exit
let pipe.on_exit = funcref('s:tracking_ref_on_exit', [store, slug])
let s:tracking_ref_job = gina#process#open(a:git, [
\ 'rev-parse',
\ '--symbolic-full-name',
\ '@{upstream}',
\], pipe)
endif
return ''
endfunction
function! s:tracking_ref_on_exit(store, slug, exitval) abort dict
call self.__on_exit(a:exitval)
silent! unlet! s:tracking_ref_job
if a:exitval
return
endif
call a:store.set(a:slug, get(self.stdout, 0))
endfunction
function! s:preset_ascii() abort
let ahead = gina#component#traffic#ahead()
let behind = gina#component#traffic#behind()
let ahead = empty(ahead) ? '' : ('^' . ahead)
let behind = empty(behind) ? '' : ('v' . behind)
return join(filter([ahead, behind], '!empty(v:val)'))
endfunction
function! s:preset_fancy() abort
let ahead = gina#component#traffic#ahead()
let behind = gina#component#traffic#behind()
let ahead = empty(ahead) ? '' : ('↑' . ahead)
let behind = empty(behind) ? '' : ('↓' . behind)
return join(filter([ahead, behind], '!empty(v:val)'))
endfunction

View File

@ -0,0 +1,175 @@
let s:Cache = vital#gina#import('System.Cache.Memory')
let s:Git = vital#gina#import('Git')
let s:registry = s:Cache.new()
let s:reference = s:Cache.new()
let s:CACHE_NEVER = 'never' " No cache. Search .git always.
let s:CACHE_TRUTH = 'truth' " Use a cache when a git repository is found.
let s:CACHE_ALWAYS = 'always' " Use a cache always.
function! gina#core#get_or_fail(...) abort
let options = extend({
\ 'expr': '%',
\ 'cache': s:CACHE_TRUTH,
\}, get(a:000, 0, {})
\)
let git = gina#core#get(options)
if !empty(git)
return git
endif
throw gina#core#revelator#warning(printf(
\ 'No git repository for a buffer "%s" is found.',
\ expand(options.expr)
\))
endfunction
function! gina#core#get(...) abort
let options = extend({
\ 'expr': '%',
\ 'cache': s:CACHE_ALWAYS,
\}, get(a:000, 0, {})
\)
if options.cache !=# s:CACHE_NEVER
let cached = s:get_cached_instance(options.expr)
if !empty(cached)
call gina#core#console#debug(printf(
\ 'A cached git instanse "%s" is used for "%s"',
\ get(cached, 'refname', ''),
\ expand(options.expr),
\))
return cached
elseif options.cache ==# s:CACHE_TRUTH && cached isnot# v:null
call gina#core#console#debug(printf(
\ 'An empty cached git instanse is used for "%s"',
\ expand(options.expr),
\))
return cached
endif
endif
let params = gina#core#buffer#parse(options.expr)
if empty(params)
let git = {}
let params.path = expand(options.expr)
else
let git = s:get_from_cache(params.repo)
endif
if empty(git)
if s:is_file_buffer(options.expr)
let git = s:get_from_bufname(params.path)
else
let git = s:get_from_cwd(bufnr(options.expr))
endif
endif
call s:set_cached_instance(options.expr, git)
return git
endfunction
function! gina#core#git_version() abort
if exists('s:git_version')
return s:git_version
endif
let r = gina#process#call(gina#core#get(), ['--version'])
let s:git_version = matchstr(join(r.stdout, "\n"), '\%(\d\+\.\)\+\d')
return s:git_version
endfunction
" Private --------------------------------------------------------------------
function! s:is_file_buffer(expr) abort
return getbufvar(a:expr, '&buftype', '') =~# '^\%(\|nowrite\|acwrite\)$'
endfunction
function! s:get_cached_instance(expr) abort
let refinfo = getbufvar(a:expr, 'gina', {})
if empty(refinfo)
return v:null
endif
" Check if the refinfo is fresh enough
if refinfo.bufname !=# simplify(bufname(a:expr))
return v:null
elseif refinfo.buftype !=# getbufvar(a:expr, '&buftype', '')
return v:null
elseif refinfo.cwd !=# simplify(getcwd())
return v:null
endif
" refinfo is fresh enough, use a cached git instance
return s:get_from_cache(refinfo.refname)
endfunction
function! s:set_cached_instance(expr, git) abort
if bufexists(bufnr(a:expr))
call setbufvar(a:expr, 'gina', {
\ 'refname': get(a:git, 'refname', ''),
\ 'bufname': simplify(bufname(a:expr)),
\ 'buftype': getbufvar(a:expr, '&buftype', ''),
\ 'cwd': simplify(getcwd()),
\})
endif
endfunction
function! s:get_available_refname(refname, git) abort
let refname = a:refname
let ref = s:reference.get(refname, '')
let index = 1
while !empty(ref) && ref !=# a:git.worktree
let refname = a:refname . '~' . index
let ref = s:reference.get(refname, '')
let index += 1
endwhile
return refname
endfunction
function! s:get_from_cache(reference) abort
if s:registry.has(a:reference)
return s:registry.get(a:reference)
elseif s:reference.has(a:reference)
return s:registry.get(s:reference.get(a:reference), {})
endif
return {}
endfunction
function! s:get_from_path(path) abort
let path = simplify(fnamemodify(a:path, ':p'))
let git = s:Git.new(path)
if empty(git)
return {}
endif
let git.refname = s:get_available_refname(
\ fnamemodify(git.worktree, ':t'),
\ git,
\)
call s:registry.set(git.worktree, git)
call s:reference.set(path, git.worktree)
call s:reference.set(git.refname, git.worktree)
return git
endfunction
function! s:get_from_bufname(path) abort
let git = s:get_from_path(a:path)
if !empty(git)
return git
endif
" Resolve symbol link
let sympath = simplify(resolve(a:path))
if sympath !=# a:path
let git = s:get_from_path(sympath)
if !empty(git)
return git
endif
endif
" Not found
return {}
endfunction
function! s:get_from_cwd(bufnr) abort
let winnr = bufwinnr(a:bufnr)
let cwdpath = winnr == -1
\ ? simplify(getcwd())
\ : simplify(getcwd(winnr))
return s:get_from_path(cwdpath)
endfunction

View File

@ -0,0 +1,162 @@
let s:Argument = vital#gina#import('Argument')
let s:WORKTREE = '@@'
function! gina#core#args#is_worktree(rev) abort
return s:WORKTREE ==# a:rev
endfunction
function! gina#core#args#raw(rargs) abort
return s:Argument.new(a:rargs)
endfunction
function! gina#core#args#new(rargs) abort
let args = gina#core#args#raw(a:rargs)
for preference in gina#custom#preferences(args.get(0))
" Assign default options
for [query, value, remover] in preference.command.options
if !empty(remover) && args.has(remover)
call args.pop(remover)
call args.pop(query)
elseif !args.has(query)
call args.set(query, value)
endif
endfor
" Assign alias
if preference.command.raw
call args.set(0, ['_raw', preference.command.origin])
else
call args.set(0, preference.command.origin)
endif
endfor
" Expand residuals to allow '%'
let pathlist = args.residual()
if !empty(pathlist)
call args.residual(map(pathlist, 'gina#core#path#expand(v:val)'))
endif
" Assig global params
let args.params = {}
let args.params.scheme = substitute(args.get(0, ''), '!$', '', '')
let cmd = args.pop('^+')
let cmdarg = []
while !empty(cmd)
call add(cmdarg, cmd)
let cmd = args.pop('^+')
endwhile
let args.params.cmdarg = empty(cmdarg) ? '' : (join(cmdarg) . ' ')
return args
endfunction
function! gina#core#args#extend_path(git, args, path) abort
if a:path is# v:null
let a:args.params.path = v:null
else
let path = empty(a:path)
\ ? gina#core#path#expand('%')
\ : gina#core#path#expand(a:path)
let path = gina#core#repo#relpath(a:git, path)
let a:args.params.path = path
endif
endfunction
function! gina#core#args#extend_diff(git, args, treeish) abort
let [rev1, rev2] = gina#core#treeish#split(a:treeish)
if get(a:args.params, 'cached')
let rev1 = empty(rev1) ? 'HEAD' : rev1
let rev2 = empty(rev2) ? ':0' : rev2
else
let rev1 = empty(rev1) ? ':0' : rev1
let rev2 = empty(rev2) ? s:WORKTREE : rev2
endif
if get(a:args.params, 'R')
let [rev2, rev1] = [rev1, rev2]
endif
" Validate if all requirements exist
" if !empty(get(a:args.params, 'path'))
" if rev1 != s:WORKTREE
" call gina#core#treeish#validate(a:git, rev1, a:args.params.path)
" endif
" if rev2 != s:WORKTREE
" call gina#core#treeish#validate(a:git, rev2, a:args.params.path)
" endif
" endif
call extend(a:args.params, {
\ 'rev1': rev1,
\ 'rev2': rev2,
\})
endfunction
function! gina#core#args#extend_treeish(git, args, treeish) abort
if a:treeish is# v:null
let rev = v:null
let path = v:null
else
let [rev, path] = gina#core#treeish#parse(a:treeish)
" Guess a revision from the current buffer name if necessary
if empty(rev)
let rev = gina#core#buffer#param('%', 'rev')
endif
" Guess a path from the current buffer name if necessary
" and make sure that the path is a relative path from rep root
if path isnot# v:null
let path = empty(path) ? gina#core#path#expand('%:p') : path
let path = gina#core#repo#relpath(a:git, path)
endif
endif
call extend(a:args.params, {
\ 'rev': rev,
\ 'path': path,
\ 'treeish': gina#core#treeish#build(rev, path),
\})
endfunction
function! gina#core#args#extend_line(git, args, line) abort
if a:line is# v:null || !empty(a:line)
let a:args.params.line = a:line
return
endif
let scheme = gina#core#buffer#param('%', 'scheme')
if scheme !~# '^\%(\|show\|blame\)$'
let a:args.params.line = v:null
return
elseif empty(scheme)
let treeish1 = gina#core#repo#relpath(a:git, expand('%:p'))
let treeish1 = ':' . treeish1
else
let treeish1 = gina#core#buffer#param('%', 'treeish')
let treeish1 = substitute(treeish1, '^:0', '', '')
endif
let treeish2 = gina#core#treeish#build(
\ gina#util#get(a:args.params, 'rev', v:null),
\ gina#util#get(a:args.params, 'path', v:null),
\)
let a:args.params.line = treeish1 ==# treeish2
\ ? line('.')
\ : v:null
endfunction
function! gina#core#args#extend_col(git, args, col) abort
if a:col is# v:null || !empty(a:col)
let a:args.params.col = a:col
return
endif
let scheme = gina#core#buffer#param('%', 'scheme')
if scheme !~# '^\%(\|show\|blame\)$'
let a:args.params.col = v:null
return
elseif empty(scheme)
let treeish1 = gina#core#repo#relpath(a:git, expand('%'))
let treeish1 = ':' . treeish1
else
let treeish1 = gina#core#buffer#param('%', 'treeish')
let treeish1 = substitute(treeish1, '^:0', '', '')
endif
let treeish2 = gina#core#treeish#build(
\ gina#util#get(a:args.params, 'rev', v:null),
\ gina#util#get(a:args.params, 'path', v:null),
\)
let a:args.params.col = treeish1 ==# treeish2
\ ? col('.')
\ : v:null
endfunction

View File

@ -0,0 +1,70 @@
let s:Path = vital#gina#import('System.Filepath')
let s:is_windows = has('win32') || has('win64')
let s:is_darwin = has('mac') || has('macunix')
let s:repository_root = expand('<sfile>:p:h:h:h:h')
let s:askpass_script = s:Path.join(s:repository_root, 'scripts', 'askpass')
if s:is_windows
" Using an official credential helper 'wincred' helps.
" See https://github.com/lambdalisue/gina.vim/pull/11#issuecomment-279541140
let s:askpass_script = ''
elseif s:is_darwin
" AFAI, no usable GUI ssh-askpass exist
let s:askpass_script .= '.mac'
else
if executable('zenity')
let s:askpass_script .= '.zenity'
else
" ssh-askpass-gnome would help in this case
let s:askpass_script = ''
endif
endif
if s:is_windows
" While Windows has an official credential which raise a GUI prompt, gina
" won't touch askpass for Windows
function! gina#core#askpass#wrap(git, args) abort
return a:args
endfunction
else
function! gina#core#askpass#wrap(git, args) abort
if empty(a:git)
return a:args
endif
let prefix = ['env', 'GIT_TERMINAL_PROMPT=0']
let askpass = s:askpass(a:git)
if !empty(askpass)
" NOTE:
" '$GIT_ASKPASS' has a higest priority so use this instead of
" '-c core.askpass=...' in Mac/Linux environment while 'env'
" is available.
let prefix += ['GIT_ASKPASS=' . askpass]
endif
return extend(a:args, prefix, 0)
endfunction
endif
function! s:askpass(git) abort
let config = gina#core#repo#config(a:git)
let askpass = get(config, 'core.askpass', '')
if !empty(g:gina#core#askpass#askpass_program)
return g:gina#core#askpas#askpass_program
elseif g:gina#core#askpass#force_internal
return s:askpass_script
elseif exists('$GIT_ASKPASS')
return $GIT_ASKPASS
elseif !empty(askpass)
return askpass
elseif exists('$SSH_ASKPASS')
return $SSH_ASKPASS
endif
return s:askpass_script
endfunction
call gina#config(expand('<sfile>'), {
\ 'askpass_program': '',
\ 'force_internal': 0,
\})

View File

@ -0,0 +1,223 @@
let s:Buffer = vital#gina#import('Vim.Buffer')
let s:String = vital#gina#import('Data.String')
let s:Guard = vital#gina#import('Vim.Guard')
let s:Opener = vital#gina#import('Vim.Buffer.Opener')
let s:Path = vital#gina#import('System.Filepath')
let s:Window = vital#gina#import('Vim.Window')
let s:NOAUTOCMD_SUFFIX = ':$'
let s:DEFAULT_PARAMS_ATTRIBUTES = {
\ 'repo': '',
\ 'scheme': '',
\ 'params': [],
\ 'rev': '',
\ 'path': '',
\ 'treeish': '',
\}
function! gina#core#buffer#bufname(git, scheme, ...) abort
let options = get(a:000, 0, {})
let params = filter(gina#util#get(options, 'params', []), '!empty(v:val)')
let treeish = gina#util#get(options, 'treeish', printf('%s:%s',
\ gina#util#get(options, 'rev'),
\ gina#util#get(options, 'path'),
\))
return s:normalize_bufname(printf(
\ 'gina://%s:%s%s/%s%s',
\ a:git.refname,
\ a:scheme,
\ empty(params) ? '' : ':' . join(params, ':'),
\ s:Path.unixpath(substitute(treeish, '^:0', '', '')),
\ gina#util#get(options, 'noautocmd', 0) ? s:NOAUTOCMD_SUFFIX : '',
\))
endfunction
function! gina#core#buffer#parse(expr) abort
let path = expand(a:expr)
let m = matchlist(path, printf(
\ '\v^gina://([^:]+):([^:\/]+)([^\/]*)[\/]?(%%(:[0-3]|[^:]*)%%(:[^:]*)?)%%(\m%s\v)?$',
\ s:String.escape_pattern(s:NOAUTOCMD_SUFFIX),
\))
if empty(m)
return {}
endif
let treeish = m[4]
let [rev, path] = gina#core#treeish#parse(treeish)
let params = {
\ 'repo': m[1],
\ 'scheme': m[2],
\ 'params': filter(split(m[3], ':'), '!empty(v:val)'),
\ 'rev': rev,
\ 'treeish': treeish,
\}
if path isnot# v:null
let params.path = substitute(
\ path,
\ s:String.escape_pattern(s:NOAUTOCMD_SUFFIX) . '$',
\ '',
\ '',
\)
endif
return params
endfunction
function! gina#core#buffer#param(expr, attr, ...) abort
if !has_key(s:DEFAULT_PARAMS_ATTRIBUTES, a:attr)
throw gina#core#revelator#critical(printf(
\ 'Unknown attribute "%s" has specified',
\ a:attr,
\))
endif
let default = get(a:000, 0, s:DEFAULT_PARAMS_ATTRIBUTES[a:attr])
let params = gina#core#buffer#parse(a:expr)
return gina#util#get(params, a:attr, default)
endfunction
function! gina#core#buffer#open(bufname, ...) abort
let options = extend({
\ 'mods': '',
\ 'group': '',
\ 'opener': '',
\ 'origin': v:true,
\ 'range': 'tabpage',
\ 'cmdarg': '',
\ 'width': v:null,
\ 'height': v:null,
\ 'line': v:null,
\ 'col': v:null,
\ 'callback': v:null,
\}, get(a:000, 0, {}),
\)
let bufname = s:normalize_bufname(a:bufname)
" Move focus to an origin window if necessary
if options.origin isnot# v:null && s:is_locator_available(options.opener)
let origin = options.origin is# v:true ? winnr() : options.origin
call gina#core#locator#focus(origin)
endif
" Open a buffer
if options.callback is# v:null
let context = s:open_without_callback(bufname, options)
else
let context = s:open_with_callback(bufname, options)
endif
" Resize width/height if necessary
if options.width && options.width != winwidth(0)
execute printf('vertical resize %d', options.width)
endif
if options.height && options.height != winheight(0)
execute printf('resize %d', options.height)
endif
" Move cursor if necessary
call setpos('.', [
\ 0,
\ options.line is# v:null ? line('.') : options.line,
\ options.col is# v:null ? col('.') : options.col,
\ 0,
\])
normal! zvzz
" Finalize
call context.end()
return context
endfunction
function! gina#core#buffer#focus(expr) abort
return s:Window.focus_buffer(a:expr)
endfunction
function! gina#core#buffer#assign_cmdarg(...) abort
let guard = s:Guard.store(['&l:modifiable'])
try
setlocal modifiable
let cmdarg = a:0 == 0 ? v:cmdarg : a:1
let fileencoding = matchstr(cmdarg, '++enc=\zs\S\+')
if !empty(fileencoding)
let &l:fileencoding = fileencoding
endif
let fileformat = matchstr(cmdarg, '++ff=\zs\S\+')
if !empty(fileformat)
let &l:fileformat = fileformat
endif
finally
call guard.restore()
endtry
endfunction
" Private --------------------------------------------------------------------
function! s:normalize_bufname(bufname) abort
" The {bufname}
" 1. Could not be started/ended with whitespaces
" 2. Could not ends with ':' in Windows
" 3. Should not ends with '/' in Vim 8 (opening a buffer fail randomly)
let oldname = ''
let newname = a:bufname
while oldname !=# newname
let oldname = newname
let newname = substitute(newname, '\%(^\s\+\|\s\+$\)', '', 'g')
let newname = substitute(newname, '\:\+$', '', '')
let newname = substitute(newname, '[\\/]$', '', '')
endwhile
return newname
endfunction
function! s:open_without_callback(bufname, options) abort
let context = s:Opener.open(a:bufname, {
\ 'mods': a:options.mods,
\ 'cmdarg': a:options.cmdarg,
\ 'group': a:options.group,
\ 'opener': a:options.opener,
\ 'range': a:options.range,
\})
return context
endfunction
function! s:open_with_callback(bufname, options) abort
" Open a buffer without BufReadCmd
let guard = s:Guard.store(['&eventignore'])
try
set eventignore+=BufReadCmd
let context = s:Opener.open(a:bufname, {
\ 'mods': a:options.mods,
\ 'group': a:options.group,
\ 'opener': a:options.opener,
\ 'range': a:options.range,
\})
finally
call guard.restore()
endtry
" NOTE:
" The content of the buffer MUST NOT be modified by callback while 'edit'
" command will be called to override the content later.
let content = getline(1, '$')
call call(
\ a:options.callback.fn,
\ get(a:options.callback, 'args', []),
\ a:options.callback
\)
if content != getline(1, '$')
throw gina#core#revelator#critical(
\ 'A buffer content could not be modified by callback'
\)
endif
" Update content
if !&modified
execute 'keepjumps edit' a:options.cmdarg
endif
return context
endfunction
function! s:is_locator_available(opener) abort
if a:opener =~# '\<p\%[tag]!\?\>'
return 0
elseif a:opener =~# '\<ped\%[it]!\?\>'
return 0
elseif a:opener =~# '\<ps\%[earch]!\?\>'
return 0
elseif a:opener =~# '\<\%(tabe\%[dit]\|tabnew\)\>'
return 0
elseif a:opener =~# '\<tabf\%[ind]\>'
return 0
endif
return 1
endfunction

View File

@ -0,0 +1,89 @@
let s:Console = vital#gina#import('Vim.Console')
let s:Console.prefix = '[gina] '
if has('nvim')
function! gina#core#console#message(msg) abort
return s:message(a:msg)
endfunction
else
" NOTE:
" Vim 8.0.0329 will not echo entire message which was invoked in timer/job.
" While echo pipe is used to inform the result of the process to a user, it
" is kind critical so use autocmd to forcedly invoke message.
let s:message_queue = []
function! gina#core#console#message(msg) abort
augroup gina_core_console_message_internal
autocmd! *
autocmd CursorMoved * call s:message_callback()
autocmd CursorHold * call s:message_callback()
autocmd InsertEnter * call s:message_callback()
augroup END
call add(s:message_queue, a:msg)
endfunction
function! s:message_callback() abort
while !empty(s:message_queue)
let msg = remove(s:message_queue, 0)
call s:message(msg)
endwhile
augroup gina_core_console_message_internal
autocmd! *
augroup END
endfunction
endif
function! gina#core#console#echo(...) abort
return call(s:Console.echo, a:000, s:Console)
endfunction
function! gina#core#console#echon(...) abort
return call(s:Console.echon, a:000, s:Console)
endfunction
function! gina#core#console#echomsg(...) abort
return call(s:Console.echomsg, a:000, s:Console)
endfunction
function! gina#core#console#debug(...) abort
return call(s:Console.debug, a:000, s:Console)
endfunction
function! gina#core#console#info(...) abort
return call(s:Console.info, a:000, s:Console)
endfunction
function! gina#core#console#warn(...) abort
return call(s:Console.warn, a:000, s:Console)
endfunction
function! gina#core#console#error(...) abort
return call(s:Console.error, a:000, s:Console)
endfunction
function! gina#core#console#ask(...) abort
return call(s:Console.ask, a:000, s:Console)
endfunction
function! gina#core#console#confirm(...) abort
return call(s:Console.confirm, a:000, s:Console)
endfunction
function! gina#core#console#ask_or_cancel(...) abort
let result = call(s:Console.ask, a:000, s:Console)
if empty(result)
throw gina#core#revelator#info('Canceled')
endif
return result
endfunction
function! s:message(msg) abort
if g:gina#core#console#enable_message_history
return gina#core#console#echomsg(a:msg)
endif
return gina#core#console#echo(a:msg)
endfunction
call gina#config(expand('<sfile>'), {
\ 'enable_message_history': 0,
\})

View File

@ -0,0 +1,117 @@
function! s:find_path_and_lnum(git, src_prefix, dst_prefix) abort
if getline('.') =~# '^-'
return s:find_path_and_lnum_lhs(a:git, a:src_prefix)
elseif getline('.') =~# '^[ +]'
return s:find_path_and_lnum_rhs(a:git, a:dst_prefix)
else
return v:null
endif
endfunction
function! s:find_path_and_lnum_lhs(git, prefix) abort
let lnum = search('^@@', 'bnW')
let m = matchlist(
\ getline(lnum),
\ '^@@ -\(\d\+\)\%(,\(\d\+\)\)\? +\(\d\+\),\(\d\+\) @@\(.*\)$'
\)
if empty(m)
return v:null
endif
let path = matchstr(
\ getline(search('^--- ', 'bnW')),
\ printf('^--- %s\zs.*$', a:prefix),
\)
if empty(path)
return v:null
endif
let n = len(filter(
\ map(range(lnum, line('.')), { _, v -> getline(v) }),
\ { _, v -> v !~# '^+' }
\))
return {'path': path, 'lnum': m[1] + n - 2, 'side': 0}
endfunction
function! s:find_path_and_lnum_rhs(git, prefix) abort
if getline('.') !~# '^[ -+]'
return v:null
endif
let lnum = search('^@@', 'bnW')
let m = matchlist(
\ getline(lnum),
\ '^@@ -\(\d\+\)\%(,\(\d\+\)\)\? +\(\d\+\),\(\d\+\) @@\(.*\)$'
\)
if empty(m)
return v:null
endif
let path = matchstr(
\ getline(search('^+++ ', 'bnW')),
\ printf('^+++ %s\zs.*$', a:prefix)
\)
if empty(path)
return v:null
endif
let n = len(filter(
\ map(range(lnum, line('.')), { _, v -> getline(v) }),
\ { _, v -> v !~# '^-' }
\))
return {'path': path, 'lnum': m[3] + n - 2, 'side': 1}
endfunction
function! s:jump(opener) abort
let git = gina#core#get_or_fail()
let args = gina#core#meta#get_or_fail('args')
let config = gina#core#repo#config(git)
if get(config, 'diff.noprefix', '') =~? 'true' || args.get('--no-prefix')
let src_prefix = ''
let dst_prefix = ''
else
let src_prefix = args.get('--src-prefix', '[aic]/')
let dst_prefix = args.get('--dst-prefix', '[bwi]/')
endif
let pathinfo = s:find_path_and_lnum(git, src_prefix, dst_prefix)
if pathinfo is v:null
return 1
endif
let rev = pathinfo.side ? args.params.rev2 : args.params.rev1
if gina#core#args#is_worktree(rev) && pathinfo.side == 1
call gina#core#console#debug(printf(
\ 'Gina edit --line=%d --opener=%s %s',
\ pathinfo.lnum,
\ a:opener,
\ pathinfo.path,
\))
execute printf(
\ 'Gina edit --line=%d --opener=%s %s',
\ pathinfo.lnum,
\ a:opener,
\ pathinfo.path,
\)
else
call gina#core#console#debug(printf(
\ 'Gina show --line=%d --opener=%s %s:%s',
\ pathinfo.lnum,
\ a:opener,
\ rev,
\ pathinfo.path,
\))
execute printf(
\ 'Gina show --line=%d --opener=%s %s:%s',
\ pathinfo.lnum,
\ a:opener,
\ rev,
\ pathinfo.path,
\)
endif
endfunction
function! gina#core#diffjump#jump(...) abort
let opener = a:0 ? a:1 : ''
let opener = empty(opener) ? 'edit' : opener
return gina#core#revelator#call(
\ function('s:jump'),
\ [opener],
\)
endfunction

View File

@ -0,0 +1,77 @@
let s:Emitter = vital#gina#import('App.Emitter')
let s:modified_timer = v:null
function! gina#core#emitter#emit(name, ...) abort
call call(s:Emitter.emit, [a:name] + a:000, s:Emitter)
endfunction
function! gina#core#emitter#subscribe(name, listener, ...) abort
call call(s:Emitter.subscribe, [a:name, a:listener] + a:000, s:Emitter)
endfunction
function! gina#core#emitter#unsubscribe(name, listener, ...) abort
call call(s:Emitter.unsubscribe, [a:name, a:listener] + a:000, s:Emitter)
endfunction
function! gina#core#emitter#add_middleware(middleware) abort
call call(s:Emitter.add_middleware, [a:middleware] + a:000, s:Emitter)
endfunction
function! gina#core#emitter#remove_middleware(...) abort
call call(s:Emitter.remove_middleware, a:000, s:Emitter)
endfunction
" Subscribe ------------------------------------------------------------------
function! s:on_modified(...) abort
if !empty(gina#process#runnings())
" DO NOT update if there are some running process
call gina#core#emitter#emit('modified:delay')
return
endif
let winid_saved = win_getid()
for winnr in range(1, winnr('$'))
let bufnr = winbufnr(winnr)
if !getbufvar(bufnr, '&modified')
\ && getbufvar(bufnr, '&autoread')
\ && bufname(bufnr) =~# '^gina://'
call win_gotoid(bufwinid(bufnr))
edit
endif
endfor
call win_gotoid(winid_saved)
endfunction
function! s:on_modified_delay() abort
if s:modified_timer isnot# v:null
" Do not emit 'modified' for previous 'modified:delay'
silent! call timer_stop(s:modified_timer)
endif
let s:modified_timer = timer_start(
\ g:gina#core#emitter#modified_delay,
\ function('s:emit_modified')
\)
endfunction
function! s:emit_modified(...) abort
call gina#core#emitter#emit('modified')
endfunction
if !exists('s:subscribed')
let s:subscribed = 1
call gina#core#emitter#subscribe(
\ 'modified',
\ function('s:on_modified')
\)
call gina#core#emitter#subscribe(
\ 'modified:delay',
\ function('s:on_modified_delay')
\)
endif
call gina#config(expand('<sfile>'), {
\ 'modified_delay': 10,
\})

View File

@ -0,0 +1,70 @@
function! gina#core#locator#find(origin) abort
let nwinnr = winnr('$')
if nwinnr == 1
return 1
endif
let origin = a:origin == 0 ? winnr() : a:origin
let former = range(origin, winnr('$'))
let latter = reverse(range(1, origin - 1))
for winnr in (former + latter)
if gina#core#locator#is_suitable(winnr)
return winnr
endif
endfor
return 0
endfunction
function! gina#core#locator#focus(origin) abort
let winnr = gina#core#locator#find(a:origin)
if winnr == 0 || winnr == winnr()
return 1
endif
call win_gotoid(win_getid(winnr))
endfunction
function! gina#core#locator#attach() abort
augroup gina_core_locator_local_internal
autocmd! * <buffer>
autocmd WinLeave <buffer> call s:on_WinLeave()
augroup END
endfunction
function! gina#core#locator#detach() abort
augroup gina_core_locator_local_internal
autocmd! * <buffer>
augroup END
endfunction
function! gina#core#locator#is_suitable(winnr) abort
if getbufvar(winbufnr(a:winnr), '&previewwindow')
\ || winwidth(a:winnr) < g:gina#core#locator#winwidth_threshold
\ || winheight(a:winnr) < g:gina#core#locator#winheight_threshold
return 0
endif
return 1
endfunction
function! s:on_WinLeave() abort
let s:info = {
\ 'nwin': winnr('$'),
\ 'previous': win_getid(winnr('#'))
\}
endfunction
function! s:on_WinEnter() abort
if exists('s:info') && winnr('$') < s:info.nwin
call gina#core#locator#focus(win_id2win(s:info.previous) || winnr())
endif
silent! unlet! s:info
endfunction
augroup gina_core_locator_internal
autocmd! *
autocmd WinEnter * nested call s:on_WinEnter()
augroup END
call gina#config(expand('<sfile>'), {
\ 'winwidth_threshold': &columns / 4,
\ 'winheight_threshold': &lines / 3,
\})

View File

@ -0,0 +1,55 @@
let s:Cache = vital#gina#import('System.Cache.Memory')
function! gina#core#meta#get(...) abort
let meta = s:meta('%')
return call(meta.get, a:000, meta)
endfunction
function! gina#core#meta#set(...) abort
let meta = s:meta('%')
return call(meta.set, a:000, meta)
endfunction
function! gina#core#meta#has(...) abort
let meta = s:meta('%')
return call(meta.has, a:000, meta)
endfunction
function! gina#core#meta#remove(...) abort
let meta = s:meta('%')
return call(meta.remove, a:000, meta)
endfunction
function! gina#core#meta#clear(...) abort
let meta = s:meta('%')
return call(meta.clear, a:000, meta)
endfunction
function! gina#core#meta#get_or_fail(name) abort
let meta = s:meta('%')
if !meta.has(a:name)
throw gina#core#revelator#critical(printf(
\ 'A required meta value "%s" does not exist on "%s"',
\ a:name,
\ bufname('%'),
\))
endif
return meta.get(a:name)
endfunction
" Private --------------------------------------------------------------------
function! s:meta(expr) abort
let bufnr = bufnr(a:expr)
if !bufexists(bufnr)
" Always return a fresh cache instance
return s:Cache.new()
endif
let meta = getbufvar(bufnr, 'gina_meta', {})
if empty(meta)
let meta = s:Cache.new()
call setbufvar(bufnr, 'gina_meta', meta)
endif
return meta
endfunction

View File

@ -0,0 +1,12 @@
let s:Options = vital#gina#import('Options')
function! gina#core#options#new() abort
return s:Options.new()
endfunction
function! gina#core#options#help_if_necessary(args, options) abort
if a:args.get('-h|--help')
call a:options.help()
throw gina#core#revelator#cancel()
endif
endfunction

View File

@ -0,0 +1,76 @@
let s:Git = vital#gina#import('Git')
let s:Path = vital#gina#import('System.Filepath')
let s:String = vital#gina#import('Data.String')
let s:is_windows = has('win32') || has('win64')
if s:is_windows
function! gina#core#path#expand(expr) abort
return s:Path.unixpath(s:expand(s:realpath(a:expr)))
endfunction
function! gina#core#path#abspath(path, ...) abort
let path = s:realpath(a:path)
return s:Path.unixpath(call('s:abspath', [path] + a:000))
endfunction
function! gina#core#path#relpath(path, ...) abort
let path = s:realpath(a:path)
return s:Path.unixpath(call('s:relpath', [path] + a:000))
endfunction
function! s:realpath(path) abort
if a:path =~# '^\w\+://'
return s:Path.unixpath(a:path)
endif
return s:Path.realpath(a:path)
endfunction
else
function! gina#core#path#expand(expr) abort
return s:expand(a:expr)
endfunction
function! gina#core#path#abspath(path, ...) abort
return call('s:abspath', [a:path] + a:000)
endfunction
function! gina#core#path#relpath(path, ...) abort
return call('s:relpath', [a:path] + a:000)
endfunction
endif
function! s:expand(expr) abort
if empty(a:expr) || a:expr[0] ==# ':'
return a:expr
elseif a:expr[:6] ==# 'gina://'
let git = gina#core#get_or_fail({'expr': a:expr})
let path = gina#core#buffer#param(a:expr, 'path')
return empty(path) ? '' : s:Path.join(git.worktree, path)
elseif a:expr[0] =~# '[%#<]'
let m = matchlist(a:expr, '^\([%#]\|<\w\+>\)\(.*\)')
let expr = expand(m[1])
let modifiers = m[2]
return fnamemodify(s:expand(expr), modifiers)
endif
return a:expr
endfunction
function! s:abspath(path, ...) abort
if s:Path.is_absolute(a:path) || a:path[0] ==# ':'
return a:path
endif
let root = s:Path.remove_last_separator(a:0 == 0 ? getcwd() : a:1)
return s:Path.join(root, a:path)
endfunction
function! s:relpath(path, ...) abort
if s:Path.is_relative(a:path) || a:path[0] ==# ':'
return a:path
endif
let root = s:Path.remove_last_separator(a:0 == 0 ? getcwd() : a:1)
let pattern = s:String.escape_pattern(root . s:Path.separator())
return a:path =~# '^' . pattern
\ ? matchstr(a:path, '^' . pattern . '\zs.*')
\ : a:path
endfunction

View File

@ -0,0 +1,49 @@
let s:Git = vital#gina#import('Git')
let s:Path = vital#gina#import('System.Filepath')
let s:Store = vital#gina#import('System.Store')
function! gina#core#repo#abspath(git, expr) abort
return gina#core#path#abspath(a:expr, a:git.worktree)
endfunction
function! gina#core#repo#relpath(git, expr) abort
let path = gina#core#path#expand(a:expr)
if s:Path.is_relative(s:Path.realpath(path))
return path
endif
let path = gina#core#path#relpath(path, a:git.worktree)
if path ==# path && path !=# resolve(path)
return gina#core#path#relpath(resolve(path), a:git.worktree)
endif
return path
endfunction
function! gina#core#repo#config(git) abort
let slug = eval(s:Store.get_slug_expr())
let store = s:Store.of(s:Git.resolve(a:git, 'config'))
let config = store.get(slug, {})
if !empty(config)
return config
endif
let result = gina#process#call(a:git, ['config', '--list'])
if result.status
throw gina#process#errormsg(result)
endif
let config = {}
for record in filter(copy(result.stdout), '!empty(v:val)')
call s:extend_config(config, record)
endfor
call store.set(slug, config)
return config
endfunction
" Private --------------------------------------------------------------------
function! s:extend_config(config, record) abort
let m = matchlist(a:record, '^\(.\+\)=\(.*\)$')
if empty(m)
return
endif
let a:config[tolower(m[1])] = m[2]
endfunction

View File

@ -0,0 +1,57 @@
let s:Revelator = vital#gina#import('App.Revelator')
function! gina#core#revelator#cancel() abort
return call(s:Revelator.message, ['CANCEL', ''], s:Revelator)
endfunction
function! gina#core#revelator#info(msg) abort
return call(s:Revelator.info, [a:msg], s:Revelator)
endfunction
function! gina#core#revelator#warning(msg) abort
return call(s:Revelator.warning, [a:msg], s:Revelator)
endfunction
function! gina#core#revelator#error(msg) abort
return call(s:Revelator.error, [a:msg], s:Revelator)
endfunction
function! gina#core#revelator#critical(msg) abort
return call(s:Revelator.critical, [a:msg], s:Revelator)
endfunction
function! gina#core#revelator#call(funcref, args, ...) abort
return call(s:Revelator.call, [a:funcref, a:args] + a:000, s:Revelator)
endfunction
" Private --------------------------------------------------------------------
function! s:receiver(revelation) abort
if a:revelation.category ==# 'CANCEL'
return 1
elseif a:revelation.category ==# 'INFO'
redraw
call gina#core#console#info(a:revelation.message)
call gina#core#console#debug(a:revelation.throwpoint)
return 1
elseif a:revelation.category ==# 'WARNING'
redraw
call gina#core#console#warn(a:revelation.message)
call gina#core#console#debug(a:revelation.throwpoint)
return 1
elseif a:revelation.category ==# 'ERROR'
redraw
call gina#core#console#error(a:revelation.message)
call gina#core#console#debug(a:revelation.throwpoint)
return 1
elseif a:revelation.category ==# 'CRITICAL'
redraw
call gina#core#console#error(a:revelation.message)
call gina#core#console#error(a:revelation.throwpoint)
return 1
endif
endfunction
call s:Revelator.unregister(s:Revelator.get_default_receiver())
call s:Revelator.register(function('s:receiver'))

View File

@ -0,0 +1,108 @@
scriptencoding utf-8
if $LANG ==# 'C'
let s:frames = ['-', '\', '|', '/']
else
let s:frames = ['⣾', '⣽', '⣻', '⢿', '⡿', '⣟', '⣯', '⣷']
endif
function! gina#core#spinner#new(expr, ...) abort
let options = extend({
\ 'frames': g:gina#core#spinner#frames,
\ 'message': g:gina#core#spinner#message,
\ 'delaytime': g:gina#core#spinner#delaytime,
\ 'updatetime': g:gina#core#spinner#updatetime,
\}, a:0 ? a:1 : {})
let spinner = deepcopy(s:spinner)
let spinner._bufnr = bufnr(a:expr)
let spinner._frames = options.frames
let spinner._message = options.message
let spinner._delaytime = options.delaytime
let spinner._updatetime = options.updatetime
return spinner
endfunction
function! gina#core#spinner#start(expr, ...) abort
let spinner = gina#core#spinner#new(a:expr, a:0 ? a:1 : {})
call spinner.start_delay()
return spinner
endfunction
function! s:_spinner_next() abort dict
let index = self._index + 1
let self._index = index >= len(self._frames) ? 0 : index
return self._index
endfunction
function! s:_spinner_text() abort dict
let face = self._frames[self._index]
return ' ' . face . ' ' . self._message
endfunction
function! s:_spinner_start_delay() abort dict
if self._timer isnot# v:null || self._timer_delay isnot# v:null || self._delaytime < 0
return
endif
let self._timer_delay = timer_start(
\ self._delaytime,
\ self.start,
\)
endfunction
function! s:_spinner_start(...) abort dict
if self._timer isnot# v:null || self._delaytime < 0
return
endif
silent! call timer_stop(self._timer_delay)
let self._statusline = getbufvar(self._bufnr, '&statusline')
let self._timer = timer_start(
\ self._updatetime,
\ function('s:update_spinner', [self]),
\ { 'repeat': -1 }
\)
let self._timer_delay = v:null
endfunction
function! s:_spinner_stop() abort dict
if self._timer_delay isnot# v:null
call timer_stop(self._timer_delay)
let self._timer_delay = v:null
endif
if self._timer is# v:null
return
endif
call timer_stop(self._timer)
call setbufvar(self._bufnr, '&statusline', self._statusline)
let self._timer = v:null
endfunction
function! s:update_spinner(spinner, timer) abort
if !bufexists(a:spinner._bufnr)
call a:spinner.stop()
elseif bufwinnr(a:spinner._bufnr) >= 0
call a:spinner.next()
call setbufvar(a:spinner._bufnr, '&statusline', a:spinner.text())
endif
endfunction
let s:spinner = {
\ '_timer': v:null,
\ '_timer_delay': v:null,
\ '_bufnr': 0,
\ '_index': 0,
\ 'next': function('s:_spinner_next'),
\ 'text': function('s:_spinner_text'),
\ 'start': function('s:_spinner_start'),
\ 'start_delay': function('s:_spinner_start_delay'),
\ 'stop': function('s:_spinner_stop'),
\}
call gina#config(expand('<sfile>'), {
\ 'frames': s:frames,
\ 'message': 'Loading ...',
\ 'delaytime': 500,
\ 'updatetime': 100,
\})

View File

@ -0,0 +1,59 @@
let s:DateTime = vital#gina#import('DateTime')
function! gina#core#timestamper#new(...) abort
let timestamper = extend({
\ 'now': s:DateTime.now(),
\ 'months': 3,
\ 'format1': '%d %b',
\ 'format2': '%d %b, %Y',
\}, get(a:000, 0, {})
\)
let timestamper = extend(timestamper, s:timestamper, 'keep')
let timestamper._cache_timezone = {}
let timestamper._cache_datetime = {}
let timestamper._cache_timestamp = {}
return timestamper
endfunction
" Timestamper ----------------------------------------------------------------
let s:timestamper = {}
function! s:timestamper.timezone(timezone) abort
if has_key(self._cache_timezone, a:timezone)
return self._cache_timezone[a:timezone]
endif
let timezone = s:DateTime.timezone(a:timezone)
let self._cache_timezone[a:timezone] = timezone
return timezone
endfunction
function! s:timestamper.datetime(epoch, timezone) abort
let cname = a:epoch . a:timezone
if has_key(self._cache_datetime, cname)
return self._cache_datetime[cname]
endif
let timezone = self.timezone(a:timezone)
let datetime = s:DateTime.from_unix_time(a:epoch, timezone)
let self._cache_datetime[cname] = datetime
return datetime
endfunction
function! s:timestamper.format(epoch, timezone) abort
let cname = a:epoch . a:timezone
if has_key(self._cache_timestamp, cname)
return self._cache_timestamp[cname]
endif
let datetime = self.datetime(a:epoch, a:timezone)
let timedelta = datetime.delta(self.now)
if timedelta.duration().months() < self.months
let timestamp = timedelta.about()
elseif datetime.year() == self.now.year()
let timestamp = datetime.strftime(self.format1)
else
let timestamp = datetime.strftime(self.format2)
endif
let self._cache_timestamp[cname] = timestamp
return timestamp
endfunction

View File

@ -0,0 +1,66 @@
function! gina#core#tracker#track(git, path, lnum, ...) abort
let options = extend({
\ 'lhs': '',
\ 'rhs': 'HEAD',
\ 'cache': 0,
\}, a:0 ? a:1 : {}
\)
let result = s:investigate(
\ a:git, a:path,
\ options.lhs,
\ options.rhs,
\ options.cache
\)
let offset = 0
for ref in result.refs
if a:lnum > ref.ll && a:lnum < (ref.ll + ref.ls)
let offset = a:lnum - ref.ll
return offset <= ref.rs ? ref.rl + offset : ref.rl + ref.rs
elseif a:lnum > ref.ll
let offset += (ref.rs - ref.ls)
else
break
endif
endfor
return a:lnum + offset
endfunction
function! s:investigate(git, path, lhs, rhs, cache) abort
let result = gina#process#call_or_fail(a:git, filter([
\ 'diff',
\ '--unified=0',
\ '--dst-prefix=',
\ '--find-renames',
\ '--inter-hunk-context=0',
\ a:cache ? '--cache' : '',
\ printf('%s..%s', a:lhs, a:rhs),
\ '--',
\ a:path,
\], '!empty(v:val)'))
let path = get(filter(copy(result.stdout), 'v:val =~# ''^+++'''), 0, a:path)
let path = substitute(path, '^+++ ', '', '')
let refs = map(
\ filter(copy(result.stdout), 'v:val =~# ''^@@'''),
\ 's:extract_ranges(v:val)'
\)
return {
\ 'path': path,
\ 'refs': refs,
\}
endfunction
function! s:extract_ranges(record) abort
let m = matchlist(
\ a:record,
\ '^@@ -\(\d\+\%(,\d\+\)\?\) +\(\d\+\%(,\d\+\)\?\) @@',
\)
let lhs = split(m[1], ',')
let rhs = split(m[2], ',')
return {
\ 'll': abs(0 + lhs[0]),
\ 'ls': 0 + get(lhs, 1, 1),
\ 'rl': abs(0 + rhs[0]),
\ 'rs': 0 + get(rhs, 1, 1),
\}
endfunction

View File

@ -0,0 +1,89 @@
let s:Path = vital#gina#import('System.Filepath')
let s:Git = vital#gina#import('Git')
function! gina#core#treeish#parse(treeish) abort
" Ref: https://git-scm.com/docs/gitrevisions
if a:treeish =~# '^:/' || a:treeish =~# '^[^:]*^{/' || a:treeish !~# ':'
return [a:treeish, v:null]
endif
let m = matchlist(a:treeish, '^\(:[0-3]\|[^:]*\)\%(:\(.*\)\)\?$')
return [m[1], m[2]]
endfunction
function! gina#core#treeish#build(rev, path) abort
let rev = a:rev is# v:null ? ':0' : a:rev
if a:path is# v:null
return rev
endif
return printf('%s:%s', rev, s:Path.unixpath(a:path))
endfunction
function! gina#core#treeish#split(rev) abort
if a:rev =~# '^.\{-}\.\.\..*$'
let [lhs, rhs] = matchlist(a:rev, '^\(.\{-}\)\.\.\.\(.*\)$')[1 : 2]
let lhs = empty(lhs) ? 'HEAD' : lhs
let rhs = empty(rhs) ? 'HEAD' : rhs
return [lhs . '...' . rhs, rhs]
elseif a:rev =~# '^.\{-}\.\..*$'
let [lhs, rhs] = matchlist(a:rev, '^\(.\{-}\)\.\.\(.*\)$')[1 : 2]
let lhs = empty(lhs) ? 'HEAD' : lhs
let rhs = empty(rhs) ? 'HEAD' : rhs
return [lhs, rhs]
else
return [a:rev, '']
endif
endfunction
function! gina#core#treeish#sha1(git, rev) abort
let ref = s:Git.ref(a:git, a:rev)
if !empty(ref)
return ref.hash
endif
" Fallback to rev-parse (e.g. HEAD@{2.days.ago})
let result = gina#process#call_or_fail(a:git, ['rev-parse', a:rev])
return get(result.stdout, 0, '')
endfunction
function! gina#core#treeish#resolve(git, rev, ...) abort
let aggressive = a:0 ? a:1 : 0
if a:rev =~# '^.\{-}\.\.\..*$'
let [lhs, rhs] = matchlist(a:rev, '^\(.\{-}\)\.\.\.\(.*\)$')[1 : 2]
let lhs = empty(lhs) ? 'HEAD' : lhs
let rhs = empty(rhs) ? 'HEAD' : rhs
return s:find_common_ancestor(a:git, lhs, rhs)
elseif a:rev =~# '^.\{-}\.\..*$'
let [lhs, _] = matchlist(a:rev, '^\(.\{-}\)\.\.\(.*\)$')[1 : 2]
let lhs = empty(lhs) ? 'HEAD' : lhs
if aggressive
let ref = s:Git.ref(a:git, lhs)
return get(ref, 'name', lhs)
else
return lhs
endif
elseif aggressive
let ref = s:Git.ref(a:git, a:rev)
return get(ref, 'name', a:rev)
else
return a:rev
endif
endfunction
function! gina#core#treeish#validate(git, rev, path, ...) abort
let treeish = gina#core#treeish#build(a:rev, a:path)
let result = gina#process#call(a:git, ['rev-parse', treeish])
if result.status
throw gina#core#revelator#warning(a:0 ? a:1 : join(result.stderr, "\n"))
endif
endfunction
" Private --------------------------------------------------------------------
function! s:find_common_ancestor(git, rev1, rev2) abort
let lhs = empty(a:rev1) ? 'HEAD' : a:rev1
let rhs = empty(a:rev2) ? 'HEAD' : a:rev2
let result = gina#process#call_or_fail(a:git, [
\ 'merge-base', lhs, rhs
\])
return get(result.stdout, 0, '')
endfunction

View File

@ -0,0 +1,18 @@
let s:Writer = vital#gina#import('Vim.Buffer.Writer')
function! gina#core#writer#new(...) abort
let options = extend({
\ 'updatetime': g:gina#core#writer#updatetime,
\}, a:0 ? a:1 : {},
\)
return s:Writer.new(options)
endfunction
function! gina#core#writer#replace(...) abort
return call(s:Writer.replace, a:000)
endfunction
call gina#config(expand('<sfile>'), {
\ 'updatetime': 100,
\})

View File

@ -0,0 +1,76 @@
let s:preferences = {}
let s:pattern_preferences = {}
function! gina#custom#preference(scheme, ...) abort
let readonly = a:0 ? a:1 : 1
let preferences = a:scheme =~# '^/'
\ ? s:pattern_preferences
\ : s:preferences
let preferences[a:scheme] = get(preferences, a:scheme, {})
let preference = extend(preferences[a:scheme], {
\ 'action': {
\ 'aliases': [],
\ 'shortens': [],
\ },
\ 'mapping': {
\ 'mappings': [],
\ },
\ 'command': {
\ 'options': [],
\ 'origin': a:scheme,
\ 'raw': 0,
\ },
\ 'executes': [],
\}, 'keep'
\)
return readonly ? deepcopy(preference) : preference
endfunction
function! gina#custom#preferences(scheme) abort
let preferences = []
for [pattern, preference] in items(s:pattern_preferences)
if a:scheme =~# pattern[1:]
call add(preferences, preference)
endif
endfor
return extend(
\ deepcopy(preferences),
\ [gina#custom#preference(a:scheme)]
\)
endfunction
function! gina#custom#clear() abort
let s:preferences = {}
let s:pattern_preferences = {}
endfunction
function! gina#custom#execute(scheme, expr) abort
let value = get(a:000, 0, 1)
let preference = gina#custom#preference(a:scheme, 0)
call add(preference.executes, [a:expr])
endfunction
" Private --------------------------------------------------------------------
function! s:apply_preference(preference) abort
for [expr] in a:preference.executes
execute expr
endfor
endfunction
function! s:FileType() abort
let scheme = gina#core#buffer#param('%', 'scheme')
if empty(scheme)
return
endif
for preference in gina#custom#preferences(scheme)
call s:apply_preference(preference)
endfor
endfunction
" Autocmd --------------------------------------------------------------------
augroup gina_custom_internal
autocmd! *
autocmd FileType * call s:FileType()
augroup END

View File

@ -0,0 +1,38 @@
function! gina#custom#action#alias(scheme, alias, origin) abort
let preference = gina#custom#preference(a:scheme, 0)
call add(preference.action.aliases, [a:alias, a:origin])
endfunction
function! gina#custom#action#shorten(scheme, action_scheme, ...) abort
let excludes = get(a:000, 0, [])
let preference = gina#custom#preference(a:scheme, 0)
call add(preference.action.shortens, [a:action_scheme, excludes])
endfunction
" Private --------------------------------------------------------------------
function! s:apply_preference(preference) abort
for [alias, origin] in a:preference.action.aliases
call gina#action#alias(alias, origin)
endfor
for [action_scheme, excludes] in a:preference.action.shortens
call gina#action#shorten(action_scheme, excludes)
endfor
endfunction
function! s:FileType() abort
let scheme = gina#core#buffer#param('%', 'scheme')
if empty(scheme)
return
endif
for preference in gina#custom#preferences(scheme)
call s:apply_preference(preference)
endfor
endfunction
" Autocmd --------------------------------------------------------------------
augroup gina_custom_action_internal
autocmd! *
autocmd FileType * call s:FileType()
augroup END

View File

@ -0,0 +1,36 @@
let s:t_number = type(0)
function! gina#custom#command#option(scheme, query, ...) abort
if a:query !~# '^--\?\S\+\%(|--\?\S\+\)*$'
throw gina#core#revelator#error(
\ 'Invalid query. See :h gina#custom#command#option'
\)
endif
let value = get(a:000, 0, 1)
let remover = type(value) == s:t_number ? s:build_remover(a:query) : ''
let preference = gina#custom#preference(a:scheme, 0)
call add(preference.command.options, [a:query, value, remover])
endfunction
function! gina#custom#command#alias(scheme, alias, ...) abort
if a:scheme =~# '^/'
throw gina#core#revelator#error(
\ '/{pattern} cannot be used to define a command alias'
\)
endif
let preference = gina#custom#preference(a:alias, 0)
let preference.command.origin = a:scheme
let preference.command.raw = get(a:000, 0, 0)
endfunction
" Private --------------------------------------------------------------------
function! s:build_remover(query) abort
let terms = split(a:query, '|')
let names = map(copy(terms), 'matchstr(v:val, ''^--\?\zs\S\+'')')
let remover = map(
\ range(len(terms)),
\ '(terms[v:val] =~# ''^--'' ? ''--no-'' : ''-!'') . names[v:val]'
\)
return join(remover, '|')
endfunction

View File

@ -0,0 +1,48 @@
function! gina#custom#mapping#map(scheme, lhs, rhs, ...) abort
let options = get(a:000, 0, {})
let preference = gina#custom#preference(a:scheme, 0)
call add(preference.mapping.mappings, [a:lhs, a:rhs, options])
endfunction
function! gina#custom#mapping#nmap(scheme, lhs, rhs, ...) abort
let options = get(a:000, 0, {})
let options.mode = 'n'
call gina#custom#mapping#map(a:scheme, a:lhs, a:rhs, options)
endfunction
function! gina#custom#mapping#vmap(scheme, lhs, rhs, ...) abort
let options = get(a:000, 0, {})
let options.mode = 'v'
call gina#custom#mapping#map(a:scheme, a:lhs, a:rhs, options)
endfunction
function! gina#custom#mapping#imap(scheme, lhs, rhs, ...) abort
let options = get(a:000, 0, {})
let options.mode = 'i'
call gina#custom#mapping#map(a:scheme, a:lhs, a:rhs, options)
endfunction
" Private --------------------------------------------------------------------
function! s:apply_preference(preference) abort
for [lhs, rhs, options] in a:preference.mapping.mappings
call gina#util#map(lhs, rhs, options)
endfor
endfunction
function! s:FileType() abort
let scheme = gina#core#buffer#param('%', 'scheme')
if empty(scheme)
return
endif
for preference in gina#custom#preferences(scheme)
call s:apply_preference(preference)
endfor
endfunction
" Autocmd --------------------------------------------------------------------
augroup gina_custom_mapping_internal
autocmd! *
autocmd FileType * call s:FileType()
augroup END

View File

@ -0,0 +1,157 @@
let s:Job = vital#gina#import('System.Job')
let s:Guard = vital#gina#import('Vim.Guard')
let s:String = vital#gina#import('Data.String')
let s:t_dict = type({})
let s:no_askpass_commands = [
\ 'init',
\ 'config',
\]
let s:runnings = {}
function! gina#process#runnings() abort
return items(s:runnings)
endfunction
function! gina#process#register(job, ...) abort
if get(a:000, 0, 0)
let s:runnings['pseudo:' . a:job] = a:job
call gina#core#emitter#emit('process:registered:pseudo', a:job)
else
let s:runnings['job:' . a:job.pid()] = a:job
call gina#core#emitter#emit('process:registered', a:job.pid(), a:job.params.scheme, a:job.args)
endif
endfunction
function! gina#process#unregister(job, ...) abort
if get(a:000, 0, 0)
silent! unlet s:runnings['pseudo:' . a:job]
call gina#core#emitter#emit('process:unregistered:pseudo', a:job)
else
silent! unlet s:runnings['job:' . a:job.pid()]
call gina#core#emitter#emit('process:unregistered', a:job.pid(), a:job.params.scheme, a:job.args)
endif
endfunction
function! gina#process#wait(...) abort
let timeout = get(a:000, 0, v:null)
let timeout = timeout is# v:null ? v:null : timeout / 1000.0
let start_time = reltime()
let updatetime = g:gina#process#updatetime . 'm'
call gina#core#emitter#emit('wait:start')
while timeout is# v:null || timeout > reltimefloat(reltime(start_time))
if empty(s:runnings)
call gina#core#emitter#emit('wait:end')
return
endif
execute 'sleep' updatetime
endwhile
call gina#core#emitter#emit('wait:timeout')
return -1
endfunction
function! gina#process#open(git, args, ...) abort
let args = type(a:args) == s:t_dict ? a:args : gina#core#args#raw(a:args)
let pipe = extend(gina#process#pipe#default(), get(a:000, 0, {}))
let pipe.params = get(args, 'params', {})
let pipe.params.scheme = get(pipe.params, 'scheme', args.get(0, ''))
let LC_ALL_saved = exists('$LC_ALL') ? $LC_ALL : v:null
let GIT_EDITOR_saved = exists('$GIT_EDITOR') ? $GIT_EDITOR : v:null
try
let $LC_ALL = 'C'
unlet $GIT_EDITOR
let job = s:Job.start(s:build_raw_args(a:git, args), pipe)
finally
if LC_ALL_saved is# v:null
unlet $LC_ALL
else
let $LC_ALL = LC_ALL_saved
endif
if GIT_EDITOR_saved is# v:null
unlet $GIT_EDITOR
else
let $GIT_EDITOR = GIT_EDITOR_saved
endif
endtry
call job.on_start()
call gina#core#console#debug(printf('process: %s', join(job.args)))
return job
endfunction
function! gina#process#call(git, args, ...) abort
let options = extend({
\ 'timeout': g:gina#process#timeout,
\}, get(a:000, 0, {})
\)
let pipe = gina#process#pipe#store()
let job = gina#process#open(a:git, a:args, pipe)
let status = job.wait(options.timeout)
return {
\ 'args': job.args,
\ 'status': status,
\ 'stdout': pipe.stdout,
\ 'stderr': pipe.stderr,
\ 'content': pipe.content,
\}
endfunction
function! gina#process#call_or_fail(git, args, ...) abort
let result = call('gina#process#call', [a:git, a:args] + a:000)
if result.status
throw gina#process#errormsg(result)
endif
return result
endfunction
function! gina#process#inform(result) abort
redraw | echo
if a:result.status
call gina#core#console#warn('Fail: ' . join(a:result.args))
endif
call gina#core#console#echo(s:String.remove_ansi_sequences(
\ join(a:result.content, "\n"))
\)
endfunction
function! gina#process#errormsg(result) abort
return gina#core#revelator#error(printf(
\ "Fail: %s\n%s",
\ join(a:result.args),
\ join(a:result.content, "\n")
\))
endfunction
function! gina#process#build_raw_args(git, args) abort
return s:build_raw_args(a:git, a:args)
endfunction
" Private --------------------------------------------------------------------
function! s:build_raw_args(git, args) abort
let args = gina#core#args#raw(g:gina#process#command).raw
if !empty(a:git) && isdirectory(a:git.worktree)
call extend(args, ['-C', a:git.worktree])
endif
call extend(args, a:args.raw)
call filter(map(args, 's:expand(v:val)'), '!empty(v:val)')
" Assign env GIT_TERMINAL_PROMPT/GIT_ASKPASS if necessary
if index(s:no_askpass_commands, a:args.get(0)) == -1
call gina#core#askpass#wrap(a:git, args)
endif
return args
endfunction
function! s:expand(value) abort
if a:value =~# '^\%([%#]\|<\w\+>\)\%(:[p8~.htreS]\|:g\?s?\S\+?\S\+?\)*$'
return gina#core#path#expand(a:value)
endif
return a:value
endfunction
call gina#config(expand('<sfile>'), {
\ 'command': 'git --no-pager -c core.editor=false -c color.status=always',
\ 'updatetime': 100,
\ 'timeout': 10000,
\})

View File

@ -0,0 +1,202 @@
let s:Guard = vital#gina#import('Vim.Guard')
let s:String = vital#gina#import('Data.String')
function! s:extend_content(content, data) abort
unlockvar 1 a:content
let a:content[-1] .= a:data[0]
call extend(a:content, a:data[1:])
lockvar 1 a:content
endfunction
" Default pipe -------------------------------------------------------------
function! gina#process#pipe#default() abort
let pipe = deepcopy(s:default_pipe)
return pipe
endfunction
function! s:_default_pipe_on_start() abort dict
call gina#process#register(self)
endfunction
function! s:_default_pipe_on_exit(exitval) abort dict
call gina#process#unregister(self)
endfunction
let s:default_pipe = {
\ 'on_start': function('s:_default_pipe_on_start'),
\ 'on_exit': function('s:_default_pipe_on_exit'),
\}
" Store pipe ---------------------------------------------------------------
function! gina#process#pipe#store() abort
let pipe = deepcopy(s:store_pipe)
let pipe.stdout = ['']
let pipe.stderr = ['']
let pipe.content = ['']
lockvar 1 pipe.stdout
lockvar 1 pipe.stderr
lockvar 1 pipe.content
return pipe
endfunction
function! s:_store_pipe_on_receive(event, data) abort dict
call map(a:data, 'v:val[-1:] ==# "\r" ? v:val[:-2] : v:val')
call s:extend_content(self[a:event], a:data)
call s:extend_content(self.content, a:data)
endfunction
function! s:_store_pipe_on_exit(exitval) abort dict
" Content may has an extra empty line (POSIX text) so remove it
if has_key(self, 'stdout') && empty(self.stdout[-1])
unlockvar 1 self.stdout
call remove(self.stdout, -1)
lockvar 1 self.stdout
endif
if has_key(self, 'stderr') && empty(self.stderr[-1])
unlockvar 1 self.stderr
call remove(self.stderr, -1)
lockvar 1 self.stderr
endif
if has_key(self, 'content') && empty(self.content[-1])
unlockvar 1 self.content
call remove(self.content, -1)
lockvar 1 self.content
endif
call call('s:_default_pipe_on_exit', [a:exitval], self)
endfunction
let s:store_pipe = {
\ 'on_start': function('s:_default_pipe_on_start'),
\ 'on_stdout': function('s:_store_pipe_on_receive', ['stdout']),
\ 'on_stderr': function('s:_store_pipe_on_receive', ['stderr']),
\ 'on_exit': function('s:_store_pipe_on_exit'),
\}
" Echo pipe ----------------------------------------------------------------
function! gina#process#pipe#echo() abort
let pipe = deepcopy(s:echo_pipe)
let pipe.stdout = ['']
let pipe.stderr = ['']
let pipe.content = ['']
lockvar 1 pipe.stdout
lockvar 1 pipe.stderr
lockvar 1 pipe.content
return pipe
endfunction
function! s:_echo_pipe_on_exit(exitval) abort dict
if len(self.content)
call gina#core#console#message(
\ s:String.remove_ansi_sequences(join(self.content, "\n")),
\)
endif
call call('s:_store_pipe_on_exit', [a:exitval], self)
endfunction
let s:echo_pipe = {
\ 'on_start': function('s:_default_pipe_on_start'),
\ 'on_stdout': function('s:_store_pipe_on_receive', ['stdout']),
\ 'on_stderr': function('s:_store_pipe_on_receive', ['stderr']),
\ 'on_exit': function('s:_echo_pipe_on_exit'),
\}
" Stream pipe --------------------------------------------------------------
function! gina#process#pipe#stream(...) abort
let pipe = deepcopy(s:stream_pipe)
let pipe.writer = gina#core#writer#new(a:0 ? a:1 : s:stream_pipe_writer)
let pipe.stderr = ['']
let pipe.content = ['']
lockvar 1 pipe.stderr
lockvar 1 pipe.content
return pipe
endfunction
function! s:_stream_pipe_on_start() abort dict
call call('s:_default_pipe_on_start', [], self)
let self.writer._job = self
call self.writer.start()
endfunction
function! s:_stream_pipe_on_receive(data) abort dict
call map(a:data, 'v:val[-1:] ==# "\r" ? v:val[:-2] : v:val')
call self.writer.write(a:data)
endfunction
function! s:_stream_pipe_on_exit(data) abort dict
call self.writer.stop()
call call('s:_echo_pipe_on_exit', [a:data], self)
endfunction
let s:stream_pipe = {
\ 'on_start': function('s:_stream_pipe_on_start'),
\ 'on_stdout': function('s:_stream_pipe_on_receive'),
\ 'on_stderr': function('s:_store_pipe_on_receive', ['stderr']),
\ 'on_exit': function('s:_stream_pipe_on_exit'),
\}
" Stream pipe writer -------------------------------------------------------
function! gina#process#pipe#stream_writer() abort
return deepcopy(s:stream_pipe_writer)
endfunction
function! s:_discard_winview() abort
augroup gina_process_pipe_stream_pipe_writer_internal
autocmd! * <buffer=abuf>
autocmd CursorMoved <buffer=abuf>
\ silent! unlet! b:gina_winview |
\ autocmd! gina_process_pipe_stream_pipe_writer_internal * <buffer=abuf>
augroup END
endfunction
function! s:_stream_pipe_writer_on_start() abort dict
let self._spinner = gina#core#spinner#start(self.bufnr)
call gina#process#register('writer:' . self.bufnr, 1)
call gina#core#emitter#emit('writer:started', self.bufnr)
" When user moves cursor, remove 'b:gina_winview' to prevent unwilling
" cursor change after completion
augroup gina_process_pipe_stream_pipe_writer_internal
execute printf('autocmd! * <buffer=%d>', self.bufnr)
execute printf(
\ 'autocmd CursorMoved <buffer=%d> call s:_discard_winview()',
\ self.bufnr
\)
augroup END
endfunction
function! s:_stream_pipe_writer_on_exit() abort dict
call self._job.stop()
call self._spinner.stop()
let focus = gina#core#buffer#focus(self.bufnr)
if empty(focus) || bufnr('%') != self.bufnr
call gina#core#emitter#emit('writer:stopped', self.bufnr)
call gina#process#unregister('writer:' . self.bufnr, 1)
return
endif
try
if exists('b:gina_winview')
silent! call winrestview(b:gina_winview)
endif
finally
call focus.restore()
call gina#core#emitter#emit('writer:stopped', self.bufnr)
call gina#process#unregister('writer:' . self.bufnr, 1)
endtry
endfunction
let s:stream_pipe_writer = {
\ 'on_start': function('s:_stream_pipe_writer_on_start'),
\ 'on_exit': function('s:_stream_pipe_writer_on_exit'),
\}
" Save winview on BufUnload while winsaveview() returns unwilling value
" on BufReadCmd
augroup gina_process_pipe_internal
autocmd! *
autocmd BufUnload gina://* let b:gina_winview = winsaveview()
augroup END

View File

@ -0,0 +1,251 @@
let s:Dict = vital#gina#import('Data.Dict')
let s:File = vital#gina#import('System.File')
let s:String = vital#gina#import('Data.String')
let s:DIRECTION_PATTERN = printf('\<\%%(%s\)\>', join([
\ 'lefta\%[bove]',
\ 'abo\%[veleft]',
\ 'rightb\%[elow]',
\ 'bel\%[owright]',
\ 'to\%[pleft]',
\ 'bo\%[tright]',
\], '\|')
\)
let s:t_list = type([])
let s:timer_syncbind = v:null
let s:timer_diffupdate = v:null
function! gina#util#contain_direction(mods) abort
return a:mods =~# s:DIRECTION_PATTERN
endfunction
function! gina#util#get(obj, key, ...) abort
let val = get(a:obj, a:key, v:null)
return val is# v:null ? get(a:000, 0, '') : val
endfunction
function! gina#util#map(lhs, rhs, ...) abort
let options = extend({
\ 'mode': '',
\ 'noremap': 0,
\ 'buffer': 1,
\ 'nowait': 0,
\ 'silent': 0,
\ 'special': 0,
\ 'script': 0,
\ 'unique': 0,
\ 'expr': 0,
\}, get(a:000, 0, {})
\)
let command = join([
\ options.mode . (options.noremap ? 'noremap' : 'map'),
\ options.buffer ? '<buffer>' : '',
\ options.nowait ? '<nowait>' : '',
\ options.silent ? '<silent>' : '',
\ options.special ? '<special>' : '',
\ options.script ? '<script>' : '',
\ options.unique ? '<unique>' : '',
\ options.expr ? '<expr>' : '',
\ a:lhs, a:rhs
\])
execute command
endfunction
function! gina#util#yank(value) abort
call setreg(v:register, a:value)
endfunction
function! gina#util#open(uri) abort
call s:File.open(a:uri)
endfunction
function! gina#util#filter(arglead, candidates, ...) abort
let hidden_pattern = get(a:000, 0, '')
let pattern = '^' . s:String.escape_pattern(a:arglead)
let candidates = copy(a:candidates)
if empty(a:arglead) && !empty(hidden_pattern)
call filter(candidates, 'v:val !~# hidden_pattern')
endif
call filter(candidates, 'v:val =~# pattern')
return candidates
endfunction
function! gina#util#shellescape(value, ...) abort
if empty(a:value)
return ''
endif
let value = type(a:value) == s:t_list
\ ? join(map(copy(a:value), 'shellescape(v:val)'))
\ : shellescape(a:value)
let prefix = get(a:000, 0, '')
return prefix . value
endfunction
function! gina#util#fnameescape(value, ...) abort
if empty(a:value)
return ''
endif
let value = type(a:value) == s:t_list
\ ? join(map(copy(a:value), 'fnameescape(v:val)'))
\ : fnameescape(a:value)
let prefix = get(a:000, 0, '')
return prefix . value
endfunction
function! gina#util#windo(expr) abort
let winid = win_getid()
try
execute printf('windo %s', a:expr)
finally
call win_gotoid(winid)
endtry
endfunction
function! gina#util#bufdo(expr, ...) abort
let bang = a:0 ? '!' : ''
let winid = win_getid()
try
execute printf('bufdo%s %s', bang, a:expr)
finally
call win_gotoid(winid)
endtry
endfunction
function! gina#util#doautocmd(name, ...) abort
let pattern = get(a:000, 0, '')
let expr = '#' . a:name
let eis = split(&eventignore, ',')
if index(eis, a:name) != -1 || index(eis, 'all') != -1 || !exists(expr)
" the specified event is ignored or does not exists
return
endif
let is_pseudo_required = empty(pattern) && !exists('#' . a:name . '#*')
if is_pseudo_required
" NOTE:
" autocmd XXXXX <pattern> exists but not sure if the current buffer name
" match with the <pattern> so register an empty autocmd to prevent
" 'No matching autocommands' warning
augroup gina_internal_util_doautocmd
autocmd! *
execute printf('autocmd %s * :', a:name)
augroup END
endif
let nomodeline = has('patch-7.4.438') && a:name ==# 'User'
\ ? '<nomodeline> '
\ : ''
try
execute printf('doautocmd %s %s %s', nomodeline, a:name, pattern)
finally
if is_pseudo_required
augroup gina_internal_util_doautocmd
autocmd! *
augroup END
endif
endtry
endfunction
function! gina#util#winwidth(winnr) abort
let width = winwidth(a:winnr)
let width -= &foldcolumn
let width -= s:is_sign_visible(winbufnr(a:winnr)) ? 2 : 0
let width -= (&number || &relativenumber)
\ ? len(string(line('$'))) + 1
\ : 0
return width
endfunction
function! gina#util#syncbind() abort
" NOTE:
" 'syncbind' does not work just after a buffer has opened
" so use timer to delay the command.
silent! call timer_stop(s:timer_syncbind)
let s:timer_syncbind = timer_start(50, function('s:syncbind'))
endfunction
function! gina#util#diffthis() abort
diffthis
augroup gina_internal_util_diffthis_local
autocmd! * <buffer>
autocmd BufReadPost <buffer>
\ if &diff && &foldmethod !=# 'diff' |
\ setlocal foldmethod=diff |
\ endif
augroup END
endfunction
function! gina#util#diffupdate() abort
" NOTE:
" 'diffupdate' does not work just after a buffer has opened
" so use timer to delay the command.
silent! call timer_stop(s:timer_diffupdate)
let s:timer_diffupdate = timer_start(100, function('s:diffupdate', [bufnr('%')]))
endfunction
" Private --------------------------------------------------------------------
function! s:syncbind(...) abort
syncbind
silent! wincmd p
silent! wincmd p
endfunction
function! s:diffoff(update) abort
augroup gina_internal_util_diffthis
autocmd! * <buffer>
augroup END
diffoff
if a:update
call s:diffupdate(bufnr('%'))
endif
endfunction
function! s:diffoff_all() abort
let winid = win_getid()
for winnr in range(1, winnr('$'))
if getwinvar(winnr, '&diff')
call win_gotoid(win_getid(winnr))
call s:diffoff(0)
endif
endfor
call win_gotoid(winid)
call s:diffupdate(bufnr('%'))
endfunction
function! s:diffupdate(bufnr, ...) abort
let winid = bufwinid(a:bufnr)
if winid == -1
return
endif
let winid_saved = win_getid()
try
if winid != winid_saved
call win_gotoid(winid)
endif
diffupdate
syncbind
finally
call win_gotoid(winid_saved)
endtry
endfunction
function! s:diffcount() abort
let indicators = map(
\ range(1, winnr('$')),
\ 'getwinvar(v:val, ''&diff'')'
\)
let indicators = filter(indicators, 'v:val')
return len(indicators)
endfunction
function! s:call_super(cls, method, ...) abort dict
return call(a:cls.__super[a:method], a:000, self)
endfunction
function! s:is_sign_visible(bufnr) abort
if !exists('&signcolumn') || &signcolumn ==# 'auto'
return len(split(execute('sign place buffer=' . a:bufnr), '\r\?\n')) > 1
else
return &signcolumn ==# 'yes'
endif
endfunction

View File

@ -0,0 +1,573 @@
let s:t_funcref = type(function('tr'))
let s:t_list = type([])
let s:PREFIX = '_vital_action_binder_'
let s:UNIQUE = sha256(expand('<sfile>:p'))
let s:MODIFIERS = [
\ 'aboveleft',
\ 'belowright',
\ 'botright',
\ 'browse',
\ 'confirm',
\ 'hide',
\ 'keepalt',
\ 'keepjumps',
\ 'keepmarks',
\ 'keeppatterns',
\ 'lockmarks',
\ 'noswapfile',
\ 'silent',
\ 'tab',
\ 'topleft',
\ 'verbose',
\ 'vertical',
\]
function! s:_vital_loaded(V) abort
let s:Revelator = a:V.import('App.Revelator')
endfunction
function! s:_vital_depends() abort
return ['App.Revelator']
endfunction
function! s:_vital_created(module) abort
let a:module.name = 'action'
let a:module.mark_sign_text = '*'
endfunction
function! s:get() abort
return get(b:, s:PREFIX . s:UNIQUE, v:null)
endfunction
function! s:attach(candidates, ...) abort dict
let binder = copy(s:binder)
call extend(binder, {
\ 'name': substitute(self.name, '\W', '-', 'g'),
\ '_candidates': a:candidates,
\ 'actions': {},
\ 'aliases': {},
\ 'markable': 0,
\})
call extend(binder, get(a:000, 0, {}))
" Lock methods
lockvar binder._get_candidates
lockvar binder._get_marked_candidates
lockvar binder.define
lockvar binder.alias
lockvar binder.action
lockvar binder.call
" Define builtin actions
call binder.define('builtin:echo', function('s:_action_echo'), {
\ 'hidden': 1,
\ 'description': 'Echo candidates',
\ 'clear_marks': 0,
\})
call binder.define('builtin:help', function('s:_action_help'), {
\ 'description': 'Show a help of actions',
\ 'mapping_mode': 'n',
\ 'repeatable': 0,
\ 'use_marks': 0,
\ 'clear_marks': 0,
\})
call binder.define('builtin:help:all', function('s:_action_help'), {
\ 'description': 'Show a help of actions including hidden actions',
\ 'mapping_mode': 'n',
\ 'options': { 'all': 1 },
\ 'repeatable': 0,
\ 'use_marks': 0,
\ 'clear_marks': 0,
\})
call binder.define('builtin:choice', function('s:_action_choice'), {
\ 'description': 'Select an action to perform',
\ 'mapping_mode': 'inv',
\ 'repeatable': 0,
\ 'clear_marks': 0,
\})
call binder.define('builtin:repeat', function('s:_action_repeat'), {
\ 'description': 'Repeat a previous repeatable action',
\ 'mapping_mode': 'inv',
\ 'repeatable': 0,
\})
call binder.alias('echo', 'builtin:echo')
call binder.alias('help', 'builtin:help')
call binder.alias('help:all', 'builtin:help:all')
execute printf('nmap <buffer> ? <Plug>(%s-builtin-help)', binder.name)
execute printf('nmap <buffer> a <Plug>(%s-builtin-choice)', binder.name)
execute printf('vmap <buffer> a <Plug>(%s-builtin-choice)', binder.name)
execute printf('imap <buffer> a <Plug>(%s-builtin-choice)', binder.name)
execute printf('nmap <buffer> . <Plug>(%s-builtin-repeat)', binder.name)
execute printf('vmap <buffer> . <Plug>(%s-builtin-repeat)', binder.name)
execute printf('imap <buffer> . <Plug>(%s-builtin-repeat)', binder.name)
if binder.markable
execute printf(
\ 'sign define %s linehl=%s texthl=%s text=%s',
\ 'VitalActionMarkSelectedSign',
\ 'VitalActionMarkSelectedLine',
\ 'VitalActionMarkSelected',
\ self.mark_sign_text,
\)
call binder.define('builtin:mark', function('s:_action_mark'), {
\ 'description': 'Mark/Unmark selected candidates',
\ 'mapping_mode': 'nv',
\ 'requirements': ['__lnum'],
\ 'use_marks': 0,
\ 'clear_marks': 0,
\})
call binder.define('builtin:mark:set', function('s:_action_mark_set'), {
\ 'hidden': 1,
\ 'description': 'Mark selected candidates',
\ 'mapping_mode': 'nv',
\ 'requirements': ['__lnum'],
\ 'use_marks': 0,
\ 'clear_marks': 0,
\})
call binder.define('builtin:mark:unset', function('s:_action_mark_unset'), {
\ 'hidden': 1,
\ 'description': 'Unmark selected candidates',
\ 'mapping_mode': 'nv',
\ 'requirements': ['__lnum'],
\ 'use_marks': 0,
\ 'clear_marks': 0,
\})
call binder.define('builtin:mark:unall', function('s:_action_mark_unall'), {
\ 'hidden': 1,
\ 'description': 'Unmark all candidates',
\ 'mapping_mode': 'n',
\ 'use_marks': 0,
\ 'clear_marks': 0,
\})
call binder.alias('mark', 'builtin:mark')
call binder.alias('mark:unall', 'builtin:mark:unall')
execute printf('nmap <buffer> mm <Plug>(%s-builtin-mark)', binder.name)
execute printf('vmap <buffer> mm <Plug>(%s-builtin-mark)', binder.name)
execute printf('nmap <buffer> m+ <Plug>(%s-builtin-mark-set)', binder.name)
execute printf('vmap <buffer> m+ <Plug>(%s-builtin-mark-set)', binder.name)
execute printf('nmap <buffer> m- <Plug>(%s-builtin-mark-unset)', binder.name)
execute printf('vmap <buffer> m- <Plug>(%s-builtin-mark-unset)', binder.name)
execute printf('nmap <buffer> m* <Plug>(%s-builtin-mark-unall)', binder.name)
execute printf('nmap <buffer> <C-j> <Plug>(%s-builtin-mark)j', binder.name)
execute printf('nmap <buffer> <C-k> k<Plug>(%s-builtin-mark)', binder.name)
endif
let b:{s:PREFIX . s:UNIQUE} = binder
return binder
endfunction
" Instance -------------------------------------------------------------------
let s:binder = {}
function! s:binder._get_candidates(...) abort
let fline = get(a:000, 0, line('.'))
let lline = get(a:000, 1, fline)
if type(self._candidates) == s:t_funcref
let candidates = self._candidates(fline, lline)
else
let candidates = self._candidates[fline-1 : lline-1]
endif
return map(
\ deepcopy(candidates),
\ 'extend(v:val, {''__lnum'': fline + v:key})'
\)
endfunction
function! s:binder._get_marked_candidates() abort
if !self.markable
throw 'vital: Action: The action binder is not markable'
endif
let signs = s:_get_signs()
let lnums = map(signs, 'v:val.line')
let candidates = []
call map(lnums, 'extend(candidates, self._get_candidates(v:val, v:val))')
return candidates
endfunction
function! s:binder.define(name, callback, ...) abort
let action = extend({
\ 'callback': a:callback,
\ 'name': a:name,
\ 'description': '',
\ 'mapping': '',
\ 'mapping_mode': '',
\ 'requirements': [],
\ 'options': {},
\ 'hidden': 0,
\ 'repeatable': 1,
\ 'use_marks': 1,
\ 'clear_marks': 1,
\}, get(a:000, 0, {}),
\)
if empty(action.mapping)
let action.mapping = printf(
\ '<Plug>(%s-%s)',
\ substitute(self.name, '\W', '-', 'g'),
\ substitute(action.name, '\W', '-', 'g'),
\)
endif
for mode in split(action.mapping_mode, '\zs')
execute printf(
\ '%snoremap <buffer><silent> %s %s:%scall <SID>_call_for_mapping(''%s'')<CR>',
\ mode,
\ action.mapping,
\ mode =~# '[i]' ? '<Esc>' : '',
\ mode =~# '[ni]' ? '<C-u>' : '',
\ a:name,
\)
endfor
let self.actions[action.name] = action
endfunction
function! s:binder.alias(alias, expr, ...) abort
let alias = extend({
\ 'name': matchstr(a:expr, '\S\+$'),
\ 'expr': a:expr,
\ 'alias': a:alias,
\}, get(a:000, 0, {}),
\)
let self.aliases[a:alias] = alias
endfunction
function! s:binder.action(expr) abort
let mods = matchstr(a:expr, '^\%(.* \)\?\ze\S\+$')
let name = matchstr(a:expr, '\S\+$')
if has_key(self.aliases, name)
let alias = self.aliases[name]
return self.action(mods . alias.expr)
elseif has_key(self.actions, name)
return [mods, self.actions[name]]
endif
" If only one action/alias could be determine, use it.
let candidates = filter(
\ keys(self.aliases) + keys(self.actions),
\ 'v:val =~# ''^'' . name'
\)
" Shorter to Longer
call sort(candidates, { a, b -> len(a) - len(b) })
if empty(candidates)
throw s:Revelator.warning(printf(
\ 'No corresponding action has found for "%s"',
\ a:expr
\))
else
" Use the first match
return self.action(mods . candidates[0])
endif
endfunction
function! s:binder.call(expr, ...) abort range
let [mods, action] = self.action(a:expr)
if a:0 == 1 && type(a:1) == s:t_list
let candidates = a:1
elseif self.markable && action.use_marks
let candidates = self._get_marked_candidates()
let candidates = empty(candidates)
\ ? call(self._get_candidates, a:000, self)
\ : candidates
else
let candidates = call(self._get_candidates, a:000, self)
endif
if !empty(action.requirements)
call filter(
\ candidates,
\ 's:_is_satisfied(v:val, action.requirements)',
\)
endif
let options = extend({
\ 'mods': mods,
\}, action.options
\)
if self.markable && action.clear_marks
call self.call('builtin:mark:unall')
endif
call call(action.callback, [candidates, options], self)
return [mods, action]
endfunction
" Actions --------------------------------------------------------------------
function! s:_action_echo(candidates, options) abort
for candidate in a:candidates
echo string(candidate)
endfor
endfunction
function! s:_action_help(candidates, options) abort dict
let mappings = s:_find_mappings(self)
let actions = values(self.actions)
if !get(a:options, 'all')
call filter(actions, '!v:val.hidden')
endif
let rows = []
let longest1 = 0
let longest2 = 0
let longest3 = 0
for action in actions
let mapping = get(mappings, action.mapping, {})
let lhs = !empty(action.mapping) && !empty(mapping) ? mapping.lhs : ''
let alias = s:_find_alias(self, action)
let identifier = empty(alias)
\ ? action.name
\ : printf('%s [%s]', action.name, alias)
let hidden = action.hidden ? '*' : ' '
let description = action.description
let mapping = printf('%s [%s]', action.mapping, action.mapping_mode)
call add(rows, [
\ lhs,
\ identifier,
\ hidden,
\ description,
\ mapping,
\])
let longest1 = len(lhs) > longest1 ? len(lhs) : longest1
let longest2 = len(identifier) > longest2 ? len(identifier) : longest2
let longest3 = len(description) > longest3 ? len(description) : longest3
endfor
let content = []
let pattern = printf(
\ '%%-%ds %%-%ds %%s %%-%ds %%s',
\ longest1,
\ longest2,
\ longest3,
\)
for params in sort(rows, 's:_compare')
call add(content, call('printf', [pattern] + params))
endfor
redraw | echo join(content, "\n")
endfunction
function! s:_action_choice(candidates, options) abort dict
let s:_binder = self
call inputsave()
try
echohl Question
redraw | echo
let fname = s:_get_function_name(function('s:_complete_action_aliases'))
let aname = input(
\ 'action: ', '',
\ printf('customlist,%s', fname),
\)
redraw | echo
finally
echohl None
call inputrestore()
endtry
if empty(aname)
return
endif
let [mods, action] = self.call(aname, a:candidates)
if action.repeatable
let self._previous_action = [mods, action]
endif
endfunction
function! s:_action_repeat(candidates, options) abort dict
let [mods, action] = get(self, '_previous_action', ['', {}])
if empty(action)
return
endif
return self.call(join([mods, action.name]), a:candidates)
endfunction
function! s:_action_mark(candidates, options) abort dict
let set_candidates = []
let unset_candidates = []
let signmap = s:_get_signmap()
for candidate in a:candidates
if has_key(signmap, string(candidate.__lnum))
call add(unset_candidates, candidate)
else
call add(set_candidates, candidate)
endif
endfor
call self.call('builtin:mark:set', set_candidates)
call self.call('builtin:mark:unset', unset_candidates)
endfunction
function! s:_action_mark_set(candidates, options) abort dict
let options = extend({}, a:options)
let lnums = map(a:candidates, 'v:val.__lnum')
let bufnr = bufnr('%')
for lnum in lnums
execute printf(
\ 'sign place %d line=%d name=%s buffer=%d',
\ lnum, lnum,
\ 'VitalActionMarkSelectedSign',
\ bufnr,
\)
endfor
endfunction
function! s:_action_mark_unset(candidates, options) abort dict
let options = extend({}, a:options)
let lnums = map(a:candidates, 'v:val.__lnum')
let bufnr = bufnr('%')
for lnum in lnums
execute printf(
\ 'sign unplace %d buffer=%d',
\ lnum, bufnr,
\)
endfor
endfunction
function! s:_action_mark_unall(candidates, options) abort dict
let options = extend({}, a:options)
let bufnr = bufnr('%')
execute printf('sign unplace * buffer=%d', bufnr)
endfunction
" Privates -------------------------------------------------------------------
function! s:_is_satisfied(candidate, requirements) abort
for requirement in a:requirements
if !has_key(a:candidate, requirement)
return 0
endif
endfor
return 1
endfunction
function! s:_compare(i1, i2) abort
return a:i1[1] == a:i2[1] ? 0 : a:i1[1] > a:i2[1] ? 1 : -1
endfunction
function! s:_compare_length(w1, w2) abort
let l1 = len(a:w1)
let l2 = len(a:w2)
return l1 == l2 ? 0 : l1 > l2 ? 1 : -1
endfunction
function! s:_find_mappings(binder) abort
let content = s:_execute('map')
let rhss = filter(
\ map(values(a:binder.actions), 'v:val.mapping'),
\ '!empty(v:val)'
\)
let rhsp = printf('\%%(%s\)', join(map(rhss, 'escape(v:val, ''\'')'), '\|'))
let rows = filter(split(content, '\r\?\n'), 'v:val =~# ''@.*'' . rhsp')
let pattern = '\(...\)\(\S\+\)'
let mappings = {}
for row in rows
let [mode, lhs] = matchlist(row, pattern)[1 : 2]
let rhs = matchstr(row, rhsp)
let mappings[rhs] = {
\ 'mode': mode,
\ 'lhs': lhs,
\ 'rhs': rhs,
\}
endfor
return mappings
endfunction
function! s:_find_alias(binder, action) abort
let aliases = filter(
\ values(a:binder.aliases),
\ 'v:val.name ==# a:action.name',
\)
" Remove aliases with mods
call filter(aliases, 'v:val.name ==# v:val.expr')
" Prefer shorter name
let candidates = sort(
\ map(aliases, 'v:val.alias'),
\ function('s:_compare_length')
\)
return get(candidates, 0, '')
endfunction
function! s:_complete_action_aliases(arglead, cmdline, cursorpos) abort
let terms = split(a:arglead, ' ', 1)
" Build modifier candidates
let modifiers = terms[:-2]
let candidates = map(
\ filter(copy(s:MODIFIERS), 'index(modifiers, v:val) == -1'),
\ 'v:val . '' '''
\)
" Build action/alias candidates
let arglead = terms[-1]
let binder = s:_binder
let actions = values(binder.actions)
if empty(arglead)
call filter(actions, '!v:val.hidden')
endif
call extend(candidates, sort(extend(
\ map(actions, 'v:val.name'),
\ keys(binder.aliases),
\)), 0)
call filter(uniq(candidates), 'v:val =~# ''^'' . arglead')
call map(candidates, 'join(modifiers + [v:val])')
return candidates
endfunction
function! s:_call_for_mapping(name) abort range
let binder = s:get()
return s:Revelator.call(binder.call, [a:name, a:firstline, a:lastline], binder)
endfunction
function! s:_get_signs() abort
let content = split(s:_execute('sign place buffer=' . bufnr('%')), '\r\?\n')
let signs = map(
\ filter(content, 'v:val =~# ''^\s\{4}'''),
\ 's:_parse_sign(v:val)',
\)
return filter(signs, 'v:val.name ==# ''VitalActionMarkSelectedSign''')
endfunction
function! s:_get_signmap() abort
let signmap = {}
for sign in s:_get_signs()
let signmap[sign.line] = sign
endfor
return signmap
endfunction
function! s:_parse_sign(sign) abort
let terms = split(a:sign)
let sign = {}
for term in terms
let m = split(term, '=')
let sign[m[0]] = m[1]
endfor
return sign
endfunction
" Compatibility --------------------------------------------------------------
if has('patch-7.4.1842')
function! s:_get_function_name(fn) abort
return get(a:fn, 'name')
endfunction
else
function! s:_get_function_name(fn) abort
return matchstr(string(a:fn), 'function(''\zs.*\ze''')
endfunction
endif
if exists('*execute')
let s:_execute = function('execute')
else
function! s:_execute(command) abort
try
redir => content
silent execute a:command
finally
redir END
endtry
return content
endfunction
endif
" Highlight ------------------------------------------------------------------
function! s:_define_mark_highlights() abort
highlight default link VitalActionMarkSelectedLine Search
highlight default link VitalActionMarkSelected Search
endfunction
augroup vital_action_internal
autocmd! *
autocmd ColorScheme * call s:_define_mark_highlights()
augroup END
call s:_define_mark_highlights()

View File

@ -0,0 +1,323 @@
let s:t_number = type(0)
let s:t_string = type('')
function! s:_vital_loaded(V) abort
let s:Guard = a:V.import('Vim.Guard')
let s:List = a:V.import('Data.List')
let s:String = a:V.import('Data.String')
endfunction
function! s:_vital_depends() abort
return ['Vim.Guard', 'Data.List', 'Data.String']
endfunction
function! s:_vital_created(module) abort
" build pattern for parsing arguments
let single_quote = '''\zs[^'']\+\ze'''
let double_quote = '"\zs[^"]\+\ze"'
let bare_strings = '\%(\\\s\|[^ ''"]\)\+'
let s:PARSE_PATTERN = printf(
\ '\%%(%s\)*\zs\%%(\s\+\|$\)\ze',
\ join([single_quote, double_quote, bare_strings], '\|')
\)
let s:NORM_PATTERN = '^\%("\zs.*\ze"\|''\zs.*\ze''\|.*\)$'
endfunction
function! s:parse(cmdline) abort
return s:norm(split(a:cmdline, s:PARSE_PATTERN))
endfunction
function! s:norm(terms) abort
return map(copy(a:terms), 's:_norm_term(v:val)')
endfunction
function! s:new(...) abort
if a:0 > 0
let init = type(a:1) == s:t_string ? s:parse(a:1) : s:norm(a:1)
else
let init = []
endif
let args = copy(s:args)
let args.raw = init
return args
endfunction
" Private --------------------------------------------------------------------
function! s:_norm_term(term) abort
let m = matchlist(a:term, '^\(-\w\|--\S\+=\)\(.\+\)')
if empty(m)
return matchstr(a:term, s:NORM_PATTERN)
endif
return m[1] . matchstr(m[2], s:NORM_PATTERN)
endfunction
function! s:_parse_term(term) abort
let m = matchlist(a:term, '^\(-\w\|--\S\+=\)\(.\+\)')
if empty(m)
return a:term =~# '^--\?\S\+' ? [a:term, 1] : ['', a:term]
else
return [substitute(m[1], '=$', '', ''), m[2]]
endif
endfunction
function! s:_build_term(key, value) abort
if type(a:value) == s:t_number
return a:value == 0 ? '' : a:key
elseif empty(a:key) || a:key =~# '^-\w$'
return a:key . a:value
else
return a:key . '=' . a:value
endif
endfunction
function! s:_build_pattern(query) abort
let patterns = split(a:query, '|')
call map(patterns, 's:String.escape_pattern(v:val)')
call map(patterns, 'v:val =~# ''^--\S\+'' ? v:val . ''\%(=\|$\)'' : v:val')
return printf('^\%%(%s\)', join(patterns, '\|'))
endfunction
function! s:_is_query(query) abort
return a:query =~# '^--\?\S\+\%(|--\?\S\+\)*$'
endfunction
" Instance -------------------------------------------------------------------
let s:args = {}
function! s:_index(pattern) abort dict
let guard = s:Guard.store(['&l:iskeyword'])
try
setlocal iskeyword&
let indices = range(0, len(self.raw)-1)
for index in indices
let term = self.raw[index]
if term ==# '--'
return -1
elseif term =~# a:pattern
return index
endif
endfor
return -1
finally
call guard.restore()
endtry
endfunction
function! s:_has(pattern) abort dict
return call('s:_index', [a:pattern], self) != -1
endfunction
function! s:_get(pattern, default) abort dict
let index = call('s:_index', [a:pattern], self)
if index == -1
return a:default
endif
return self.raw[index]
endfunction
function! s:_pop(pattern, default) abort dict
let index = call('s:_index', [a:pattern], self)
if index == -1
return a:default
endif
return remove(self.raw, index)
endfunction
function! s:_set(pattern, term) abort dict
let index = call('s:_index', [a:pattern], self)
if index == -1
let tail = index(self.raw, '--')
let tail = tail == -1 ? len(self.raw) : tail
call insert(self.raw, a:term, tail)
else
let self.raw[index] = a:term
endif
return self
endfunction
function! s:_index_o(query) abort dict
return call('s:_index', [s:_build_pattern(a:query)], self)
endfunction
function! s:_has_o(query) abort dict
return call('s:_has', [s:_build_pattern(a:query)], self)
endfunction
function! s:_get_o(query, default) abort dict
let index = call('s:_index_o', [a:query], self)
if index == -1
return a:default
endif
return s:_parse_term(self.raw[index])[1]
endfunction
function! s:_pop_o(query, default) abort dict
let index = call('s:_index_o', [a:query], self)
if index == -1
return a:default
endif
return s:_parse_term(remove(self.raw, index))[1]
endfunction
function! s:_set_o(query, value) abort dict
let index = call('s:_index_o', [a:query], self)
if index == -1
let tail = index(self.raw, '--')
let tail = tail == -1 ? len(self.raw) : tail
let term = s:_build_term(split(a:query, '|')[-1], a:value)
call insert(self.raw, term, tail)
else
let term = s:_build_term(s:_parse_term(self.raw[index])[0], a:value)
let self.raw[index] = term
endif
return self
endfunction
function! s:_index_p(query) abort dict
if a:query < 0
throw 'vital: Argument: {query} (n-th) requires to be positive.'
endif
let indices = range(0, len(self.raw)-1)
let pattern = '^--\?\w\+'
let counter = 0
for index in indices
let term = self.raw[index]
if term ==# '--'
return -1
elseif term !~# pattern
let counter += 1
if counter == a:query + 1
return index
endif
endif
endfor
return -1
endfunction
function! s:_has_p(query) abort dict
return call('s:_index_p', [a:query], self) != -1
endfunction
function! s:_get_p(query, ...) abort dict
let default = get(a:000, 0, '')
let index = call('s:_index_p', [a:query], self)
if index == -1
return default
endif
return self.raw[index]
endfunction
function! s:_pop_p(query, ...) abort dict
let default = get(a:000, 0, '')
let index = call('s:_index_p', [a:query], self)
if index == -1
return default
endif
return remove(self.raw, index)
endfunction
function! s:_set_p(query, value) abort dict
let index = call('s:_index_p', [a:query], self)
if index == -1
let tail = index(self.raw, '--')
let tail = tail == -1 ? len(self.raw) : tail
let n = (a:query + 1) - len(filter(
\ self.raw[:tail-1],
\ 'v:val !~# ''^--\?\w\+'''
\))
call extend(self.raw, repeat([''], n), tail)
let self.raw[tail - 1 + n] = a:value
else
let self.raw[index] = a:value
endif
let self.raw = s:List.flatten(self.raw)
return self
endfunction
function! s:args.hash() abort
return sha256(string(self.raw))
endfunction
function! s:args.lock() abort
lockvar self
return self
endfunction
function! s:args.clone() abort
let args = deepcopy(self)
lockvar 1 args
return args
endfunction
function! s:args.index(query) abort
return type(a:query) == s:t_string
\ ? s:_is_query(a:query)
\ ? call('s:_index_o', [a:query], self)
\ : call('s:_index', [a:query], self)
\ : call('s:_index_p', [a:query], self)
endfunction
function! s:args.has(query, ...) abort
return type(a:query) == s:t_string
\ ? s:_is_query(a:query)
\ ? call('s:_has_o', [a:query], self)
\ : call('s:_has', [a:query], self)
\ : call('s:_has_p', [a:query], self)
endfunction
function! s:args.get(query, ...) abort
return type(a:query) == s:t_string
\ ? s:_is_query(a:query)
\ ? call('s:_get_o', [a:query, get(a:000, 0, 0)], self)
\ : call('s:_get', [a:query, get(a:000, 0, '')], self)
\ : call('s:_get_p', [a:query, get(a:000, 0, '')], self)
endfunction
function! s:args.pop(query, ...) abort
return type(a:query) == s:t_string
\ ? s:_is_query(a:query)
\ ? call('s:_pop_o', [a:query, get(a:000, 0, 0)], self)
\ : call('s:_pop', [a:query, get(a:000, 0, '')], self)
\ : call('s:_pop_p', [a:query, get(a:000, 0, '')], self)
endfunction
function! s:args.set(query, value) abort
return type(a:query) == s:t_string
\ ? s:_is_query(a:query)
\ ? call('s:_set_o', [a:query, a:value], self)
\ : call('s:_set', [a:query, a:value], self)
\ : call('s:_set_p', [a:query, a:value], self)
endfunction
function! s:args.split() abort
let tail = index(self.raw, '--')
if tail == -1
return [copy(self.raw), []]
elseif tail == 0
return [[], self.raw[1:]]
else
return [self.raw[:tail-1], self.raw[tail+1:]]
endif
endfunction
function! s:args.effective(...) abort
if a:0 == 0
return self.split()[0]
else
let residual = self.residual()
let self.raw = empty(residual) ? a:1 : a:1 + ['--'] + residual
return self
endif
endfunction
function! s:args.residual(...) abort
if a:0 == 0
return self.split()[1]
else
let effective = self.effective()
let self.raw = effective + ['--'] + a:1
return self
endif
endfunction

View File

@ -0,0 +1,181 @@
function! s:_vital_loaded(V) abort
let s:INI = a:V.import('Text.INI')
let s:Path = a:V.import('System.Filepath')
let s:String = a:V.import('Data.String')
let s:Store = a:V.import('System.Store')
endfunction
function! s:_vital_depends() abort
return [
\ 'Text.INI',
\ 'System.Filepath',
\ 'Data.String',
\ 'System.Store',
\]
endfunction
function! s:new(path) abort
let path = s:Path.remove_last_separator(expand(a:path))
let dirpath = isdirectory(path) ? path : fnamemodify(path, ':p:h')
let dirpath = simplify(s:Path.abspath(s:Path.realpath(path)))
" Find worktree
let dgit = finddir('.git', fnameescape(dirpath) . ';')
let dgit = empty(dgit) ? '' : fnamemodify(dgit, ':p:h')
let fgit = findfile('.git', fnameescape(dirpath) . ';')
let fgit = empty(fgit) ? '' : fnamemodify(fgit, ':p')
let worktree = len(dgit) > len(fgit) ? dgit : fgit
let worktree = empty(worktree) ? '' : fnamemodify(worktree, ':h')
if empty(worktree)
return {}
endif
" Find repository
let repository = s:Path.join(worktree, '.git')
if filereadable(repository)
" A '.git' may be a file which was created by '--separate-git-dir' option
let lines = readfile(repository)
if empty(lines)
throw printf(
\ 'vital: Git: An invalid .git file has found at "%s".',
\ repository,
\)
endif
let gitdir = matchstr(lines[0], '^gitdir:\s*\zs.\+$')
let is_abs = s:Path.is_absolute(gitdir)
let repository = is_abs ? gitdir : repository[:-5] . gitdir
let repository = empty(repository) ? '' : fnamemodify(repository, ':p:h')
endif
" Find commondir
let commondir = ''
if filereadable(s:Path.join(repository, 'commondir'))
let commondir = readfile(s:Path.join(repository, 'commondir'))[0]
let commondir = s:Path.join(repository, commondir)
endif
let git = {
\ 'worktree': simplify(s:Path.realpath(worktree)),
\ 'repository': simplify(s:Path.realpath(repository)),
\ 'commondir': simplify(s:Path.realpath(commondir)),
\}
lockvar git.worktree
lockvar git.repository
lockvar git.commondir
return git
endfunction
function! s:abspath(git, path) abort
let relpath = s:Path.realpath(expand(a:path))
if s:Path.is_absolute(relpath)
return relpath
endif
return s:Path.join(a:git.worktree, relpath)
endfunction
function! s:relpath(git, path) abort
let abspath = s:Path.realpath(expand(a:path))
if s:Path.is_relative(abspath)
return abspath
endif
let pattern = s:String.escape_pattern(a:git.worktree . s:Path.separator())
return abspath =~# '^' . pattern
\ ? matchstr(abspath, '^' . pattern . '\zs.*')
\ : abspath
endfunction
function! s:resolve(git, path) abort
let path = s:Path.realpath(a:path)
let path1 = s:Path.join(a:git.repository, path)
let path2 = empty(a:git.commondir)
\ ? ''
\ : s:Path.join(a:git.commondir, path)
return filereadable(path1) || isdirectory(path1)
\ ? path1
\ : filereadable(path2) || isdirectory(path2)
\ ? path2
\ : path1
endfunction
" The search paths are documented at
" https://git-scm.com/docs/git-rev-parse
function! s:ref(git, refname) abort
let refname = a:refname ==# '@' ? 'HEAD' : a:refname
let candidates = [
\ refname,
\ printf('refs/%s', refname),
\ printf('refs/tags/%s', refname),
\ printf('refs/heads/%s', refname),
\ printf('refs/remotes/%s', refname),
\ printf('refs/remotes/%s/HEAD', refname),
\]
let packed_refs = s:_get_packed_refs(a:git)
for candidate in candidates
let ref = s:_get_reference(a:git, candidate, packed_refs)
if !empty(ref)
return ref
endif
endfor
return {}
endfunction
" Private --------------------------------------------------------------------
function! s:_get_packed_refs(git) abort
let path = s:resolve(a:git, 'packed-refs')
if !filereadable(path)
return []
endif
let store = s:Store.of(path)
let packed_refs = store.get('packed-refs', [])
if !empty(packed_refs)
return packed_refs
endif
let packed_refs = readfile(path)
call store.set('packed-refs', packed_refs)
return packed_refs
endfunction
function! s:_get_reference(git, refname, packed_refs) abort
let ref = s:_get_reference_trad(a:git, a:refname, a:packed_refs)
if !empty(ref)
return ref
endif
return s:_get_reference_packed(a:git, a:refname, a:packed_refs)
endfunction
function! s:_get_reference_trad(git, refname, packed_refs) abort
let path = s:resolve(a:git, a:refname)
if !filereadable(path)
return {}
endif
let content = get(readfile(path), 0, '')
if content =~# '^ref:'
return s:_get_reference(
\ a:git,
\ matchstr(content, '^ref:\s\+\zs.\+'),
\ a:packed_refs,
\)
endif
let name = matchstr(a:refname, '^refs/\%(heads\|remotes\|tags\)/\zs.*')
return {
\ 'name': empty(name) ? content[:7] : name,
\ 'path': a:refname,
\ 'hash': content,
\}
endfunction
function! s:_get_reference_packed(git, refname, packed_refs) abort
let m = matchstr(a:packed_refs, '\s' . a:refname . '$')
if empty(m)
return {}
endif
let m = matchlist(m, '^\([0-9a-f]\+\)\s\+\(.*\)$')
let refname = m[2]
let name = matchstr(refname, '^refs/\%(heads\|remotes\|tags\)/\zs.*')
return {
\ 'name': name,
\ 'path': refname,
\ 'hash': m[1],
\}
endfunction

View File

@ -0,0 +1,104 @@
let s:t_list = type([])
let s:t_func = type(function('tr'))
function! s:_vital_loaded(V) abort
let s:String = a:V.import('Data.String')
endfunction
function! s:_vital_depends() abort
return ['Data.String']
endfunction
function! s:new() abort
let options = copy(s:options)
let options._options = {}
return options
endfunction
" Private --------------------------------------------------------------------
function! s:_filter(arglead, candidates) abort
let pattern = '^' . s:String.escape_pattern(a:arglead)
let candidates = copy(a:candidates)
call filter(candidates, 'v:val =~# pattern')
return candidates
endfunction
function! s:_complete_choice(arglead, cmdline, cursorpos, choices) abort
let leading = matchstr(a:arglead, '\%(-[^-]\|--\S\+=\)')
let candidates = map(
\ copy(a:choices),
\ 'leading . v:val'
\)
return s:_filter(a:arglead, candidates)
endfunction
function! s:_complete_callback(arglead, cmdline, cursorpos, callback) abort
let m = matchlist(a:arglead, '\(-[^-]\|--\S\+=\)\(.*\)')
let candidates = a:callback(m[2], a:cmdline, a:cursorpos)
return map(
\ copy(candidates),
\ 'm[1] . v:val'
\)
endfunction
function! s:_compare_options(lhs, rhs) abort
let lhs = a:lhs.names[-1]
let rhs = a:rhs.names[-1]
return lhs ==# rhs ? 0 : (lhs < rhs ? -1 : 1)
endfunction
" Options --------------------------------------------------------------------
let s:options = {}
function! s:options.define(query, description, ...) abort
let self._options[a:query] = extend(copy(s:option), {
\ 'names': split(a:query, '|'),
\ 'value': a:0 ? a:1 : v:null,
\ 'description': a:description,
\})
endfunction
function! s:options.help(...) abort
let lwidth = max(map(keys(self._options), 'len(v:val)')) + 3
let rwidth = get(a:000, 0, 80) - lwidth
let text = ['options:']
call map(
\ sort(values(self._options), function('s:_compare_options')),
\ 'extend(text, v:val.help(lwidth, rwidth))'
\)
redraw | echo join(text, "\n")
endfunction
function! s:options.complete(arglead, cmdline, cursorpos) abort
let leading = matchstr(a:arglead, '^\%(-[^-]\|--\S\+=\)')
let candidates = []
for option in values(self._options)
if index(option.names, leading) != -1
return option.complete(a:arglead, a:cmdline, a:cursorpos)
endif
call extend(candidates, option.names)
endfor
return s:_filter(a:arglead, candidates)
endfunction
" Option ---------------------------------------------------------------------
let s:option = {}
function! s:option.help(lwidth, rwidth) abort
let rhs = s:String.wrap(self.description, a:rwidth)
let lhs = [s:String.pad_right(join(self.names), a:lwidth)]
let lhs += repeat([repeat(' ', a:lwidth)], len(rhs) - 1)
return map(range(len(lhs)), 'lhs[v:val] . rhs[v:val]')
endfunction
function! s:option.complete(arglead, cmdline, cursorpos) abort
if type(self.value) == s:t_list
return s:_complete_choice(a:arglead, a:cmdline, a:cursorpos, self.value)
elseif type(self.value) == s:t_func
return s:_complete_callback(a:arglead, a:cmdline, a:cursorpos, self.value)
endif
return []
endfunction

View File

@ -0,0 +1,87 @@
let s:t_list = type([])
let s:store_cache = {}
function! s:hash(pathlist) abort
return sha256(join(sort(a:pathlist)))
endfunction
function! s:get_slug_expr() abort
return 'matchstr(expand(''<sfile>''), ''\zs[^. ]\+$'')'
endfunction
function! s:of(pathlist) abort
let pathlist = type(a:pathlist) == s:t_list
\ ? a:pathlist
\ : [a:pathlist]
let hash = s:hash(pathlist)
if has_key(s:store_cache, hash)
return s:store_cache[hash]
endif
let store = copy(s:store)
let store.caches = {}
let store.pathlist = copy(pathlist)
lockvar store.pathlist
let s:store_cache[hash] = store
return store
endfunction
function! s:remove(pathlist) abort
let pathlist = type(a:pathlist) == s:t_list
\ ? a:pathlist
\ : [a:pathlist]
let hash = s:hash(pathlist)
silent! unlet s:store_cache[hash]
endfunction
" Store instance -------------------------------------------------------------
let s:store = {}
function! s:store.is_expired(name) abort
let cache = get(self.caches, a:name, {})
if empty(cache)
return 1
endif
for i in range(len(self.pathlist))
let uptime1 = getftime(self.pathlist[i])
let uptime2 = cache.uptimes[i]
if uptime1 != uptime2 && (uptime1 == -1 || uptime2 == -1)
return 1
elseif uptime1 > uptime2
return 1
endif
endfor
return 0
endfunction
function! s:store.has(name) abort
return has_key(self.caches, a:name)
endfunction
function! s:store.get(name, ...) abort
let default = get(a:000, 0)
if self.is_expired(a:name)
return default
endif
return self.caches[a:name].cache
endfunction
function! s:store.set(name, value) abort
let uptimes = map(copy(self.pathlist), 'getftime(v:val)')
let cache = {
\ 'cache': a:value,
\ 'uptimes': uptimes
\}
let self.caches[a:name] = cache
return self
endfunction
function! s:store.remove(name) abort
silent! unlet self.caches[a:name]
return self
endfunction
function! s:store.clear() abort
let self.caches = {}
return self
endfunction

View File

@ -0,0 +1,9 @@
let s:_plugin_name = expand('<sfile>:t:r')
function! vital#{s:_plugin_name}#new() abort
return vital#{s:_plugin_name[1:]}#new()
endfunction
function! vital#{s:_plugin_name}#function(funcname) abort
silent! return function(a:funcname)
endfunction

View File

@ -0,0 +1,78 @@
" ___vital___
" NOTE: lines between '" ___vital___' is generated by :Vitalize.
" Do not modify the code nor insert new lines before '" ___vital___'
function! s:_SID() abort
return matchstr(expand('<sfile>'), '<SNR>\zs\d\+\ze__SID$')
endfunction
execute join(['function! vital#_gina#App#Emitter#import() abort', printf("return map({'subscribe': '', 'unsubscribe': '', 'emit': '', 'add_middleware': '', 'remove_middleware': ''}, \"vital#_gina#function('<SNR>%s_' . v:key)\")", s:_SID()), 'endfunction'], "\n")
delfunction s:_SID
" ___vital___
let s:listeners = {}
let s:middlewares = []
function! s:subscribe(name, listener, ...) abort
let dict = get(a:000, 0, v:null)
let s:listeners[a:name] = get(s:listeners, a:name, [])
call add(s:listeners[a:name], [a:listener, dict])
endfunction
function! s:unsubscribe(...) abort
if a:0 == 0
let s:listeners = {}
elseif a:0 == 1
let s:listeners[a:1] = []
else
let dict = a:0 == 3 ? a:3 : v:null
let s:listeners[a:1] = get(s:listeners, a:1, [])
let index = index(s:listeners[a:1], [a:2, dict])
if index != -1
call remove(s:listeners[a:1], index)
endif
endif
endfunction
function! s:add_middleware(middleware) abort
call add(s:middlewares, a:middleware)
endfunction
function! s:remove_middleware(...) abort
if a:0 == 0
let s:middlewares = []
else
let index = index(s:middlewares, a:1)
if index != -1
call remove(s:middlewares, index)
endif
endif
endfunction
function! s:emit(name, ...) abort
let attrs = copy(a:000)
let listeners = copy(get(s:listeners, a:name, []))
let middlewares = map(s:middlewares, 'extend(copy(s:middleware), v:val)')
for middleware in middlewares
call call(middleware.on_emit_pre, [a:name, listeners, attrs], middleware)
endfor
for [l:Listener, dict] in listeners
if empty(dict)
call call(Listener, attrs)
else
call call(Listener, attrs, dict)
endif
endfor
for middleware in middlewares
call call(middleware.on_emit_post, [a:name, listeners, attrs], middleware)
endfor
endfunction
" Middleware skeleton --------------------------------------------------------
let s:middleware = {}
function! s:middleware.on_emit_pre(name, listeners, attrs) abort
" Users can override this method
endfunction
function! s:middleware.on_emit_post(name, listeners, attrs) abort
" Users can override this method
endfunction

View File

@ -0,0 +1,126 @@
" ___vital___
" NOTE: lines between '" ___vital___' is generated by :Vitalize.
" Do not modify the code nor insert new lines before '" ___vital___'
function! s:_SID() abort
return matchstr(expand('<sfile>'), '<SNR>\zs\d\+\ze__SID$')
endfunction
execute join(['function! vital#_gina#App#Revelator#import() abort', printf("return map({'info': '', '_vital_depends': '', 'unregister': '', '_vital_created': '', 'register': '', 'call': '', 'message': '', 'warning': '', 'critical': '', 'get_default_receiver': '', 'error': '', '_vital_loaded': ''}, \"vital#_gina#function('<SNR>%s_' . v:key)\")", s:_SID()), 'endfunction'], "\n")
delfunction s:_SID
" ___vital___
let s:t_number = type(0)
let s:receivers = []
function! s:_vital_loaded(V) abort
let s:Console = a:V.import('Vim.Console')
endfunction
function! s:_vital_depends() abort
return ['Vim.Console']
endfunction
function! s:_vital_created(module) abort
call a:module.register(s:get_default_receiver())
endfunction
function! s:message(category, msg) abort
return printf(
\ 'vital: App.Revelator: %s: %s',
\ a:category,
\ a:msg,
\)
endfunction
function! s:info(msg) abort
let v:statusmsg = a:msg
return s:message('INFO', a:msg)
endfunction
function! s:warning(msg) abort
let v:warningmsg = a:msg
return s:message('WARNING', a:msg)
endfunction
function! s:error(msg) abort
let v:errmsg = a:msg
return s:message('ERROR', a:msg)
endfunction
function! s:critical(msg) abort
let v:errmsg = a:msg
return s:message('CRITICAL', a:msg)
endfunction
function! s:call(func, arglist, ...) abort
let receivers_saved = copy(s:receivers)
let dict = a:0 ? a:1 : 0
try
return type(dict) == s:t_number
\ ? call(a:func, a:arglist)
\ : call(a:func, a:arglist, dict)
catch /^vital: App\.Revelator: /
call s:_receive(v:exception, v:throwpoint)
finally
let s:receivers = receivers_saved
endtry
endfunction
function! s:register(receiver) abort
call add(s:receivers, a:receiver)
endfunction
function! s:unregister(receiver) abort
let index = index(s:receivers, a:receiver)
if index != -1
call remove(s:receivers, index)
endif
endfunction
function! s:get_default_receiver() abort
return function('s:_default_receiver')
endfunction
function! s:_receive(exception, throwpoint) abort
let m = matchlist(a:exception, '^vital: App\.Revelator: \(.\{-}\): \(.*\)$')
if len(m)
let category = m[1]
let message = m[2]
let revelation = {
\ 'category': m[1],
\ 'message': m[2],
\ 'exception': a:exception,
\ 'throwpoint': a:throwpoint,
\}
for l:Receiver in reverse(copy(s:receivers))
if call(Receiver, [revelation])
return
endif
endfor
endif
throw a:exception . "\n" . a:throwpoint
endfunction
function! s:_default_receiver(revelation) abort
if a:revelation.category ==# 'INFO'
redraw
call s:Console.info(a:revelation.message)
call s:Console.debug(a:revelation.throwpoint)
return 1
elseif a:revelation.category ==# 'WARNING'
redraw
call s:Console.warn(a:revelation.message)
call s:Console.debug(a:revelation.throwpoint)
return 1
elseif a:revelation.category ==# 'ERROR'
redraw
call s:Console.error(a:revelation.message)
call s:Console.debug(a:revelation.throwpoint)
return 1
elseif a:revelation.category ==# 'CRITICAL'
redraw
call s:Console.error(a:revelation.message)
call s:Console.error(a:revelation.throwpoint)
return 1
endif
endfunction

View File

@ -0,0 +1,186 @@
" ___vital___
" NOTE: lines between '" ___vital___' is generated by :Vitalize.
" Do not modify the code nor insert new lines before '" ___vital___'
function! s:_SID() abort
return matchstr(expand('<sfile>'), '<SNR>\zs\d\+\ze__SID$')
endfunction
execute join(['function! vital#_gina#Bitwise#import() abort', printf("return map({'uint8': '', 'rotate16l': '', 'uint16': '', 'rotate8l': '', 'rotate16r': '', 'uint32': '', 'rotate8r': '', '_vital_loaded': '', 'rshift': '', 'lshift': '', 'lshift32': '', 'rotate32l': '', 'rshift32': '', 'rotate32r': '', 'sign_extension': '', '_vital_created': '', 'rotate64l': '', 'compare': '', 'uint64': '', 'rotate64r': ''}, \"vital#_gina#function('<SNR>%s_' . v:key)\")", s:_SID()), 'endfunction'], "\n")
delfunction s:_SID
" ___vital___
" bitwise operators
" moved from github.com/ynkdir/vim-funlib
let s:save_cpo = &cpo
set cpo&vim
" inner utility
let s:bits = has('num64') ? 64 : 32
let s:mask = s:bits - 1
let s:mask32 = 32 - 1
let s:pow2 = [1]
for s:i in range(s:mask)
call add(s:pow2, s:pow2[-1] * 2)
endfor
unlet s:i
let s:min = s:pow2[-1]
" 32bit/64bit common method
function! s:_throw(msg) abort
throw 'vital: Bitwise: ' . a:msg
endfunction
" compare as unsigned int
function! s:compare(a, b) abort
if (a:a >= 0 && a:b >= 0) || (a:a < 0 && a:b < 0)
return a:a < a:b ? -1 : a:a > a:b ? 1 : 0
else
return a:a < 0 ? 1 : -1
endif
endfunction
function! s:lshift(a, n) abort
return a:a * s:pow2[and(a:n, s:mask)]
endfunction
function! s:rshift(a, n) abort
let n = and(a:n, s:mask)
return n == 0 ? a:a :
\ a:a < 0 ? (a:a - s:min) / s:pow2[n] + s:pow2[-2] / s:pow2[n - 1]
\ : a:a / s:pow2[n]
endfunction
" 32bit or 64bit specific method
" define sign_extension
" lshift32/rshift32 64bit only implementation.
if has('num64')
" NOTE:
" An int literal larger than or equal to 0x8000000000000000 will be rounded
" to 0x7FFFFFFFFFFFFFFF after Vim 8.0.0219, so create it without literal
let s:xFFFFFFFF00000000 = 0xFFFFFFFF * s:pow2[and(32, s:mask)]
function! s:sign_extension(n) abort
if and(a:n, 0x80000000)
return or(a:n, s:xFFFFFFFF00000000)
else
return and(a:n, 0xFFFFFFFF)
endif
endfunction
function! s:lshift32(a, n) abort
return and(s:lshift(a:a, and(a:n, s:mask32)), 0xFFFFFFFF)
endfunction
function! s:rshift32(a, n) abort
return s:rshift(and(a:a, 0xFFFFFFFF), and(a:n, s:mask32))
endfunction
else
function! s:sign_extension(n) abort
return a:n
endfunction
endif
" 32bit or 64bit specific method
" builtin and funcref setup at module creation time.
" define and/or/xor/invert built-in
" lshift32/rshift32 32bit only altnative define.
function! s:_vital_created(module) abort
for op in ['and', 'or', 'xor', 'invert']
let a:module[op] = function(op)
let s:[op] = a:module[op]
endfor
if !has('num64')
let a:module.lshift32 = a:module.lshift
let a:module.rshift32 = a:module.rshift
endif
endfunction
" setup at module loaded time.
" define inner utility part2 : use defined method
function! s:_vital_loaded(V) abort
if has('num64')
let s:mask32bit = 0xFFFFFFFF
let s:mask64bit = or(
\ s:lshift(s:mask32bit, 32),
\ s:mask32bit
\)
else
let s:mask32bit = or(
\ s:lshift(0xFFFF, 16),
\ 0xFFFF
\)
endif
endfunction
" 32bit/64bit common method part2 : use defined method
function! s:uint8(value) abort
return and(a:value, 0xFF)
endfunction
function! s:uint16(value) abort
return and(a:value, 0xFFFF)
endfunction
function! s:uint32(value) abort
return and(a:value, s:mask32bit)
endfunction
function! s:rotate8l(data, bits) abort
let data = s:uint8(a:data)
return s:uint8(or(s:lshift(data, a:bits),
\ s:rshift(data, 8 - a:bits)))
endfunction
function! s:rotate8r(data, bits) abort
return s:rotate8l(a:data, 8 - a:bits)
endfunction
function! s:rotate16l(data, bits) abort
let data = s:uint16(a:data)
return s:uint16(or(s:lshift(data, a:bits),
\ s:rshift(data, 16 - a:bits)))
endfunction
function! s:rotate16r(data, bits) abort
return s:rotate16l(a:data, 16 - a:bits)
endfunction
function! s:rotate32l(data, bits) abort
let data = s:uint32(a:data)
return s:uint32(or(s:lshift(data, a:bits),
\ s:rshift(data, 32 - a:bits)))
endfunction
function! s:rotate32r(data, bits) abort
return s:rotate32l(a:data, 32 - a:bits)
endfunction
" 32bit or 64bit specific method part2 : use defined method
" define uint64/rotate64l 64bit only implementation.
" 32bit throw exception.
if has('num64')
function! s:uint64(value) abort
return and(a:value, s:mask64bit)
endfunction
function! s:rotate64l(data, bits) abort
let data = s:uint64(a:data)
return s:uint64(or(s:lshift(data, a:bits),
\ s:rshift(data, 64 - a:bits)))
endfunction
else
function! s:uint64(value) abort
call s:_throw('64bit unsupport.')
endfunction
function! s:rotate64l(data, bits) abort
call s:_throw('64bit unsupport.')
endfunction
endif
" When 32bit throw exception.
function! s:rotate64r(data, bits) abort
return s:rotate64l(a:data, 64 - a:bits)
endfunction
let &cpo = s:save_cpo
unlet s:save_cpo
" vim:set et ts=2 sts=2 sw=2 tw=0:

View File

@ -0,0 +1,44 @@
" ___vital___
" NOTE: lines between '" ___vital___' is generated by :Vitalize.
" Do not modify the code nor insert new lines before '" ___vital___'
function! s:_SID() abort
return matchstr(expand('<sfile>'), '<SNR>\zs\d\+\ze__SID$')
endfunction
execute join(['function! vital#_gina#Config#import() abort', printf("return map({'define': '', 'translate': '', 'config': ''}, \"vital#_gina#function('<SNR>%s_' . v:key)\")", s:_SID()), 'endfunction'], "\n")
delfunction s:_SID
" ___vital___
let s:plugin_name = expand('<sfile>:p:h:t')
let s:plugin_name = s:plugin_name =~# '^__.\+__$'
\ ? s:plugin_name[2:-3]
\ : s:plugin_name =~# '^_.\+$'
\ ? s:plugin_name[1:]
\ : s:plugin_name
function! s:define(prefix, default) abort
let prefix = a:prefix =~# '^g:' ? a:prefix : 'g:' . a:prefix
for [key, Value] in items(a:default)
let name = prefix . '#' . key
if !exists(name)
execute 'let ' . name . ' = ' . string(Value)
endif
unlet Value
endfor
endfunction
function! s:config(scriptfile, default) abort
let prefix = s:translate(a:scriptfile)
call s:define(prefix, a:default)
endfunction
function! s:translate(scriptfile) abort
let path = fnamemodify(a:scriptfile, ':gs?\\?/?')
let name = matchstr(path, printf(
\ 'autoload/\zs\%%(%s\.vim\|%s/.*\)$',
\ s:plugin_name,
\ s:plugin_name,
\))
let name = substitute(name, '\.vim$', '', '')
let name = substitute(name, '/', '#', 'g')
let name = substitute(name, '\%(^#\|#$\)', '', 'g')
return 'g:' . name
endfunction

Some files were not shown because too many files have changed in this diff Show More