├── .github └── workflows │ └── test.yml ├── .gitignore ├── .gitmessage ├── LICENSE ├── README.md ├── autoload └── gin │ ├── component │ ├── branch.vim │ ├── traffic.vim │ └── worktree.vim │ ├── indicator │ └── echo.vim │ ├── internal │ ├── component.vim │ ├── proxy.vim │ ├── util.vim │ └── util │ │ └── ansi_escape_code.vim │ └── util.vim ├── deno.jsonc ├── denops └── gin │ ├── action │ ├── add.ts │ ├── branch_delete.ts │ ├── branch_move.ts │ ├── branch_new.ts │ ├── browse.ts │ ├── chaperon.ts │ ├── cherry_pick.ts │ ├── core.ts │ ├── diff.ts │ ├── diff_smart.ts │ ├── echo.ts │ ├── edit.ts │ ├── fixup.ts │ ├── log.ts │ ├── merge.ts │ ├── patch.ts │ ├── rebase.ts │ ├── reset.ts │ ├── reset_file.ts │ ├── restore.ts │ ├── revert.ts │ ├── rm.ts │ ├── show.ts │ ├── stage.ts │ ├── stash.ts │ ├── switch.ts │ ├── tag.ts │ └── yank.ts │ ├── command │ ├── bare │ │ ├── command.ts │ │ └── main.ts │ ├── branch │ │ ├── __snapshots__ │ │ │ └── parser_test.ts.snap │ │ ├── command.ts │ │ ├── edit.ts │ │ ├── main.ts │ │ ├── parser.ts │ │ ├── parser_test.ts │ │ └── testdata │ │ │ ├── branch-with-worktree.txt │ │ │ └── branch.txt │ ├── browse │ │ ├── command.ts │ │ └── main.ts │ ├── buffer │ │ ├── command.ts │ │ ├── edit.ts │ │ ├── main.ts │ │ └── read.ts │ ├── chaperon │ │ ├── command.ts │ │ ├── main.ts │ │ ├── util.ts │ │ └── util_test.ts │ ├── diff │ │ ├── command.ts │ │ ├── commitish.ts │ │ ├── commitish_test.ts │ │ ├── edit.ts │ │ ├── jump.ts │ │ ├── jump_test.diff │ │ ├── jump_test.ts │ │ ├── main.ts │ │ └── read.ts │ ├── edit │ │ ├── command.ts │ │ ├── edit.ts │ │ ├── main.ts │ │ ├── read.ts │ │ ├── util.ts │ │ └── write.ts │ ├── log │ │ ├── command.ts │ │ ├── edit.ts │ │ ├── main.ts │ │ ├── parser.ts │ │ ├── parser_test.ts │ │ └── read.ts │ ├── patch │ │ ├── command.ts │ │ └── main.ts │ └── status │ │ ├── command.ts │ │ ├── edit.ts │ │ ├── main.ts │ │ ├── parser.ts │ │ └── parser_test.ts │ ├── component │ ├── branch.ts │ ├── traffic.ts │ └── worktree.ts │ ├── git │ ├── executor.ts │ ├── finder.ts │ ├── finder_test.ts │ ├── process.ts │ ├── process_test.ts │ └── worktree.ts │ ├── global.ts │ ├── main.ts │ ├── proxy │ ├── askpass.ts │ ├── editor.ts │ ├── main.ts │ └── server.ts │ └── util │ ├── __snapshots__ │ └── ansi_escape_code_test.ts.snap │ ├── ansi_escape_code.ts │ ├── ansi_escape_code_test.ts │ ├── cmd.ts │ ├── cmd_test.ts │ ├── ensure_path.ts │ ├── expand.ts │ ├── indicator_stream.ts │ ├── main.ts │ ├── testdata │ └── ansi_escape_code_content.txt │ ├── testutil.ts │ ├── text.ts │ ├── text_test.ts │ ├── yank.ts │ └── yank_test.ts ├── doc ├── .gitignore └── gin.txt ├── ftplugin ├── gin-branch.vim ├── gin-diff.vim ├── gin-log.vim ├── gin-status.vim ├── gitcommit │ └── gin.vim └── gitrebase │ └── gin.vim ├── lefthook.yml └── plugin ├── gin-branch.vim ├── gin-browse.vim ├── gin-cd.vim ├── gin-chaperon.vim ├── gin-diff.vim ├── gin-edit.vim ├── gin-log.vim ├── gin-patch.vim ├── gin-status.vim └── gin.vim /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Test 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | pull_request: 8 | paths: 9 | - "**.md" 10 | - "**.ts" 11 | - "deno.jsonc" 12 | - ".github/workflows/test.yml" 13 | workflow_dispatch: 14 | inputs: 15 | denops_branch: 16 | description: "Denops revision to test" 17 | required: false 18 | default: "main" 19 | 20 | defaults: 21 | run: 22 | shell: bash --noprofile --norc -eo pipefail {0} 23 | 24 | env: 25 | DENOPS_BRANCH: ${{ github.event.inputs.denops_branch || 'main' }} 26 | 27 | jobs: 28 | check: 29 | strategy: 30 | matrix: 31 | runner: 32 | - ubuntu-latest 33 | deno_version: 34 | - "1.x" 35 | runs-on: ${{ matrix.runner }} 36 | steps: 37 | - run: git config --global core.autocrlf false 38 | if: runner.os == 'Windows' 39 | - uses: actions/checkout@v4 40 | - uses: denoland/setup-deno@v1.1.4 41 | with: 42 | deno-version: "${{ matrix.deno_version }}" 43 | - uses: actions/cache@v4 44 | with: 45 | key: deno-${{ hashFiles('**/*') }} 46 | restore-keys: deno- 47 | path: | 48 | /home/runner/.cache/deno/deps/https/deno.land 49 | - name: Lint check 50 | run: deno lint 51 | - name: Format check 52 | run: deno fmt --check 53 | - name: Type check 54 | run: deno task check 55 | 56 | test: 57 | strategy: 58 | matrix: 59 | runner: 60 | - windows-latest 61 | - macos-latest 62 | - ubuntu-latest 63 | deno_version: 64 | - "1.45.0" 65 | - "1.x" 66 | host_version: 67 | - vim: "v9.1.0448" 68 | nvim: "v0.10.0" 69 | runs-on: ${{ matrix.runner }} 70 | timeout-minutes: 15 71 | steps: 72 | - run: git config --global core.autocrlf false 73 | if: runner.os == 'Windows' 74 | 75 | - run: | 76 | git config --global user.email "github-action@example.com" 77 | git config --global user.name "GitHub Action" 78 | git version 79 | 80 | - uses: actions/checkout@v4 81 | 82 | - uses: denoland/setup-deno@v1.1.4 83 | with: 84 | deno-version: "${{ matrix.deno_version }}" 85 | 86 | - name: Get denops 87 | run: | 88 | git clone https://github.com/vim-denops/denops.vim /tmp/denops.vim 89 | echo "DENOPS_TEST_DENOPS_PATH=/tmp/denops.vim" >> "$GITHUB_ENV" 90 | 91 | - name: Try switching denops branch 92 | run: | 93 | git -C /tmp/denops.vim switch ${{ env.DENOPS_BRANCH }} || true 94 | git -C /tmp/denops.vim branch 95 | 96 | - uses: rhysd/action-setup-vim@v1 97 | id: vim 98 | with: 99 | version: "${{ matrix.host_version.vim }}" 100 | 101 | - uses: rhysd/action-setup-vim@v1 102 | id: nvim 103 | with: 104 | neovim: true 105 | version: "${{ matrix.host_version.nvim }}" 106 | 107 | - name: Export executables 108 | run: | 109 | echo "DENOPS_TEST_VIM_EXECUTABLE=${{ steps.vim.outputs.executable }}" >> "$GITHUB_ENV" 110 | echo "DENOPS_TEST_NVIM_EXECUTABLE=${{ steps.nvim.outputs.executable }}" >> "$GITHUB_ENV" 111 | 112 | - name: Check versions 113 | run: | 114 | deno --version 115 | ${DENOPS_TEST_VIM_EXECUTABLE} --version 116 | ${DENOPS_TEST_NVIM_EXECUTABLE} --version 117 | 118 | - name: Perform pre-cache 119 | run: | 120 | deno cache ${DENOPS_TEST_DENOPS_PATH}/denops/@denops-private/mod.ts ./denops/gin/main.ts 121 | 122 | - name: Test 123 | run: deno task test:coverage 124 | timeout-minutes: 15 125 | 126 | - run: | 127 | deno task coverage --lcov > coverage.lcov 128 | 129 | - uses: codecov/codecov-action@v4 130 | with: 131 | os: ${{ runner.os }} 132 | files: ./coverage.lcov 133 | token: ${{ secrets.CODECOV_TOKEN }} 134 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /.tools 2 | .tool-versions 3 | .coverage 4 | .deps 5 | deno.lock 6 | -------------------------------------------------------------------------------- /.gitmessage: -------------------------------------------------------------------------------- 1 | 2 | 3 | # Guide (v1.0) 4 | # 5 | # 👍 :+1: Apply changes. 6 | # 7 | # 🌿 :herb: Add or update things for tests. 8 | # ☕ :coffee: Add or update things for developments. 9 | # 📦 :package: Add or update dependencies. 10 | # 📝 :memo: Add or update documentations. 11 | # 12 | # 🐛 :bug: Bugfixes. 13 | # 💋 :kiss: Critical hotfixes. 14 | # 🚿 :shower: Remove features, codes, or files. 15 | # 16 | # 🚀 :rocket: Improve performance. 17 | # 💪 :muscle: Refactor codes. 18 | # 💥 :boom: Breaking changes. 19 | # 💩 :poop: Bad codes needs to be improved. 20 | # 21 | # How to use: 22 | # git config commit.template .gitmessage 23 | # 24 | # Reference: 25 | # https://github.com/lambdalisue/emojiprefix 26 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2021 Alisue 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | SOFTWARE. 20 | 21 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # 🥃 Gin 2 | 3 | [![MIT License](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE) 4 | [![vim help](https://img.shields.io/badge/vim-%3Ah%20gin-orange.svg)](doc/gin.txt) 5 | [![test](https://github.com/lambdalisue/vim-gin/actions/workflows/test.yml/badge.svg)](https://github.com/lambdalisue/vim-gin/actions/workflows/test.yml) 6 | 7 | Gin (_vim-gin_) is a plugin to handle git repository from Vim/Neovim. 8 | 9 | **Alpha version. Any changes, including backward incompatible ones, are applied 10 | without announcements.** 11 | 12 | ## Features 13 | 14 | - Proxy the prompts/editor used by git commands to Vim 15 | - Components to show information on `statusline` and/or `tabline` 16 | - `Gin` to call a raw git command and echo the result 17 | - `GinBuffer` to call a raw git command and open a result buffer 18 | - `GinBranch` to see `git branch` of a repository 19 | - `GinBrowse` to visit the hosting service webpage of a repository (powered by 20 | [git-browse](https://deno.land/x/git_browse)) 21 | - `GinCd/GinLcd/GinTcd` to invoke `cd/lcd/tcd` to the repository root 22 | - `GinChaperon` to solve git conflicts (like `git mergetool`) 23 | - `GinDiff` to see `git diff` of a file 24 | - `GinEdit` to see `git show` of a file 25 | - `GinLog` to see `git log` of a repository/file 26 | - `GinPatch` to stage changes partially (like `git add -p`) 27 | - `GinStatus` to see `git status` of a repository 28 | 29 | See [Features](https://github.com/lambdalisue/vim-gin/wiki/Features) in Wiki for 30 | detail about each features. 31 | 32 | ## Requirements 33 | 34 | Gin is written in denops thus users need to install [Deno](https://deno.land) 35 | and denops.vim 36 | 37 | - [vim-denops/denops.vim][vim-denops/denops.vim]
An ecosystem for writing 38 | Vim/Neovim plugin in Deno. 39 | 40 | [vim-denops/denops.vim]: https://github.com/vim-denops/denops.vim 41 | 42 | ## Installation 43 | 44 | Install [Deno](https://deno.land) then use 45 | [vim-plug](https://github.com/junegunn/vim-plug) to install like: 46 | 47 | ```vim 48 | Plug 'vim-denops/denops.vim' 49 | Plug 'lambdalisue/vim-gin' 50 | ``` 51 | 52 | Or see 53 | [How to install](https://github.com/lambdalisue/vim-gin/wiki#how-to-install) 54 | section in Wiki for other Vim plugin managers. 55 | 56 | ## Similar projects 57 | 58 | - [tpope/vim-fugitive](https://github.com/tpope/vim-fugitive)
A plugin that 59 | lead me to the development of gita.vim 60 | - [lambdalisue/vim-gita](https://github.com/lambdalisue/vim-gita)
First git 61 | manipulation plugin that I made, works on Vim 7.4 62 | - [lambdalisue/vim-gina](https://github.com/lambdalisue/vim-gina)
Second git 63 | manipulation plugin that I made, works asynchronously on Vim 8.1 64 | 65 | ## License 66 | 67 | The code in this repository follows MIT license, texted in [LICENSE](./LICENSE). 68 | Contributors need to agree that any modifications sent in this repository follow 69 | the license. 70 | -------------------------------------------------------------------------------- /autoload/gin/component/branch.vim: -------------------------------------------------------------------------------- 1 | function! gin#component#branch#ascii() abort 2 | let component = 'component:branch:ascii' 3 | call gin#internal#component#init(component) 4 | return gin#internal#component#get(component) 5 | endfunction 6 | 7 | function! gin#component#branch#unicode() abort 8 | let component = 'component:branch:unicode' 9 | call gin#internal#component#init(component) 10 | return gin#internal#component#get(component) 11 | endfunction 12 | -------------------------------------------------------------------------------- /autoload/gin/component/traffic.vim: -------------------------------------------------------------------------------- 1 | function! gin#component#traffic#ascii() abort 2 | let component = 'component:traffic:ascii' 3 | call gin#internal#component#init(component) 4 | return gin#internal#component#get(component) 5 | endfunction 6 | 7 | function! gin#component#traffic#unicode() abort 8 | let component = 'component:traffic:unicode' 9 | call gin#internal#component#init(component) 10 | return gin#internal#component#get(component) 11 | endfunction 12 | -------------------------------------------------------------------------------- /autoload/gin/component/worktree.vim: -------------------------------------------------------------------------------- 1 | function! gin#component#worktree#name() abort 2 | let component = 'component:worktree:name' 3 | call gin#internal#component#init(component) 4 | return gin#internal#component#get(component) 5 | endfunction 6 | 7 | function! gin#component#worktree#full() abort 8 | let component = 'component:worktree:full' 9 | call gin#internal#component#init(component) 10 | return gin#internal#component#get(component) 11 | endfunction 12 | -------------------------------------------------------------------------------- /autoload/gin/indicator/echo.vim: -------------------------------------------------------------------------------- 1 | let s:saved = v:null 2 | 3 | function gin#indicator#echo#open(id) abort 4 | if s:saved isnot v:null 5 | let s:saved.refcount += 1 6 | return 7 | endif 8 | let s:saved = #{ 9 | \ more: &more, 10 | \ cmdheight: &cmdheight, 11 | \ refcount: 1, 12 | \} 13 | endfunction 14 | 15 | function gin#indicator#echo#write(id, chunk) abort 16 | if s:saved is v:null || len(a:chunk) is# 0 17 | return 18 | endif 19 | let &cmdheight = max([&cmdheight, len(a:chunk)]) 20 | redraw | echo join(a:chunk, "\n") 21 | call gin#internal#util#debounce(printf('let &cmdheight = %d', s:saved.cmdheight), 500) 22 | endfunction 23 | 24 | function gin#indicator#echo#close(id) abort 25 | if s:saved is v:null 26 | return 27 | endif 28 | let s:saved.refcount -= 1 29 | if s:saved.refcount is# 0 30 | let &more = s:saved.more 31 | let &cmdheight = s:saved.cmdheight 32 | let s:saved = v:null 33 | endif 34 | endfunction 35 | -------------------------------------------------------------------------------- /autoload/gin/internal/component.vim: -------------------------------------------------------------------------------- 1 | let s:cache = {} 2 | let s:init = {} 3 | 4 | function! gin#internal#component#init(component) abort 5 | if has_key(s:init, a:component) 6 | return 7 | endif 8 | let s:init[a:component] = 1 9 | execute printf('augroup gin_internal_%s', substitute(a:component, '\W', '_', 'g')) 10 | autocmd! 11 | execute printf('autocmd BufEnter * call gin#internal#component#update("%s")', a:component) 12 | execute printf('autocmd User GinCommandPost call gin#internal#component#update("%s")', a:component) 13 | execute printf('autocmd User DenopsPluginPost:gin call gin#internal#component#update("%s")', a:component) 14 | augroup END 15 | endfunction 16 | 17 | function! gin#internal#component#get(component) abort 18 | return get(s:cache, a:component, '') 19 | endfunction 20 | 21 | function! gin#internal#component#update(component) abort 22 | if !denops#plugin#is_loaded('gin') 23 | return 24 | endif 25 | let previous = get(s:cache, a:component, '') 26 | call denops#request_async( 27 | \ 'gin', 28 | \ a:component, 29 | \ [], 30 | \ { v -> s:update_success(a:component, previous, v) }, 31 | \ { e -> s:update_fail(e) }, 32 | \) 33 | endfunction 34 | 35 | function! s:update_success(component, previous, value) abort 36 | let s:cache[a:component] = a:value 37 | if a:value !=# a:previous 38 | call gin#internal#util#debounce('doautocmd User GinComponentPost', 100) 39 | endif 40 | endfunction 41 | 42 | function! s:update_fail(err) abort 43 | if &verbose 44 | echoerr a:err 45 | endif 46 | endfunction 47 | -------------------------------------------------------------------------------- /autoload/gin/internal/proxy.vim: -------------------------------------------------------------------------------- 1 | function gin#internal#proxy#init(waiter) abort 2 | let b:gin_internal_proxy_waiter = a:waiter 3 | 4 | setlocal bufhidden=hide 5 | setlocal noswapfile 6 | 7 | command! -buffer -nargs=0 Apply call s:apply() 8 | command! -buffer -nargs=0 Cancel call s:cancel() 9 | augroup gin_internal_proxy_init 10 | autocmd! * 11 | autocmd QuitPre ++nested ++once call s:confirm(expand('')) 12 | augroup END 13 | endfunction 14 | 15 | function s:apply() abort 16 | let l:waiter = b:gin_internal_proxy_waiter 17 | call s:reset() 18 | write 19 | call denops#request('gin', l:waiter, [v:true]) 20 | bwipeout 21 | endfunction 22 | 23 | function s:cancel() abort 24 | let l:waiter = b:gin_internal_proxy_waiter 25 | call s:reset() 26 | call denops#request('gin', l:waiter, [v:false]) 27 | bwipeout! 28 | endfunction 29 | 30 | function s:reset() abort 31 | augroup gin_internal_proxy_init 32 | autocmd! * 33 | augroup END 34 | endfunction 35 | 36 | function s:confirm(bufnr) abort 37 | if get(g:, 'gin_proxy_apply_without_confirm', 0) 38 | call s:apply() 39 | return 40 | endif 41 | echohl Comment 42 | echo 'Hint: Use `:Apply` or `:Cancel` to apply or cancel changes directly' 43 | echo 'Hint: Use `let g:gin_proxy_apply_without_confirm = 1` to apply changes without confirmation' 44 | echohl Title 45 | try 46 | let l:result = gin#internal#util#input(#{ 47 | \ prompt: 'Do you want to apply changes? [Y/n] ', 48 | \ cancelreturn: 'no', 49 | \}) 50 | finally 51 | echohl None 52 | endtry 53 | redraw 54 | if l:result ==# '' || l:result =~? '^y\%[es]$' 55 | call s:apply() 56 | else 57 | call s:cancel() 58 | endif 59 | endfunction 60 | -------------------------------------------------------------------------------- /autoload/gin/internal/util.vim: -------------------------------------------------------------------------------- 1 | let s:debounce_timers = {} 2 | 3 | function! gin#internal#util#debounce(expr, delay) abort 4 | let timer = get(s:debounce_timers, a:expr, 0) 5 | silent! call timer_stop(timer) 6 | let s:debounce_timers[a:expr] = timer_start(a:delay, { -> execute(a:expr) }) 7 | endfunction 8 | 9 | function gin#internal#util#input(opts) abort 10 | return s:input(a:opts) 11 | endfunction 12 | 13 | if has('nvim') 14 | let s:input = function('input') 15 | else 16 | function s:input(opts) abort 17 | let l:unique = sha256(reltimestr(reltime())) 18 | let l:opts = extend(#{ 19 | \ prompt: '', 20 | \ default: '', 21 | \ completion: v:null, 22 | \ cancelreturn: '', 23 | \}, a:opts) 24 | try 25 | execute printf('cnoremap %s', l:unique) 26 | let l:result = l:opts.completion is# v:null 27 | \ ? input(l:opts.prompt, l:opts.default) 28 | \ : input(l:opts.prompt, l:opts.default, l:opts.completion) 29 | if l:result ==# l:unique 30 | return l:opts.cancelreturn 31 | endif 32 | return l:result 33 | finally 34 | silent! cunmap 35 | endtry 36 | endfunction 37 | endif 38 | -------------------------------------------------------------------------------- /autoload/gin/internal/util/ansi_escape_code.vim: -------------------------------------------------------------------------------- 1 | " Ref: https://github.com/w0ng/vim-hybrid 2 | let s:terminal_ansi_colors = [ 3 | \ '#282a2e', 4 | \ '#a54242', 5 | \ '#8c9440', 6 | \ '#de935f', 7 | \ '#5f819d', 8 | \ '#85678f', 9 | \ '#5e8d87', 10 | \ '#707880', 11 | \ '#373b41', 12 | \ '#cc6666', 13 | \ '#b5bd68', 14 | \ '#f0c674', 15 | \ '#81a2be', 16 | \ '#b294bb', 17 | \ '#8abeb7', 18 | \ '#c5c8c6', 19 | \] 20 | 21 | function! gin#internal#util#ansi_escape_code#colors() abort 22 | return s:list_colors() 23 | endfunction 24 | 25 | if has('nvim') 26 | function! s:list_colors() abort 27 | return map( 28 | \ range(16), 29 | \ { _, v -> get(g:, printf('terminal_color_%d', v), s:terminal_ansi_colors[v]) }, 30 | \) 31 | endfunction 32 | else 33 | function! s:list_colors() abort 34 | return get(g:, 'terminal_ansi_colors', s:terminal_ansi_colors) 35 | endfunction 36 | endif 37 | -------------------------------------------------------------------------------- /autoload/gin/util.vim: -------------------------------------------------------------------------------- 1 | let s:debounce_timers = {} 2 | 3 | function! gin#util#reload(...) abort 4 | let bufnr = a:0 ? a:1 : bufnr() 5 | call denops#plugin#wait_async('gin', { -> denops#notify('gin','util:reload', [bufnr]) }) 6 | endfunction 7 | 8 | function! gin#util#expand(expr) abort 9 | call denops#plugin#wait('gin') 10 | return denops#request('gin', 'util:expand', [a:expr]) 11 | endfunction 12 | 13 | function! gin#util#worktree(...) abort 14 | call denops#plugin#wait('gin') 15 | if a:0 16 | return denops#request('gin', 'util:worktree', [a:1]) 17 | else 18 | return denops#request('gin', 'util:worktree', []) 19 | endif 20 | endfunction 21 | -------------------------------------------------------------------------------- /deno.jsonc: -------------------------------------------------------------------------------- 1 | { 2 | "lock": false, 3 | "exclude": [ 4 | "docs/**", 5 | ".coverage/**" 6 | ], 7 | "tasks": { 8 | "check": "deno check ./**/*.ts", 9 | "test": "deno test -A --shuffle --doc", 10 | "test:coverage": "deno task test --coverage=.coverage", 11 | "coverage": "deno coverage .coverage", 12 | "update": "deno run --allow-env --allow-read --allow-write --allow-run=git,deno --allow-net=jsr.io,registry.npmjs.org jsr:@molt/cli **/*.ts", 13 | "update:write": "deno task -q update --write", 14 | "update:commit": "deno task -q update --commit --prefix :package: --pre-commit=fmt,lint" 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /denops/gin/action/add.ts: -------------------------------------------------------------------------------- 1 | import type { Denops } from "jsr:@denops/std@^7.0.0"; 2 | import * as batch from "jsr:@denops/std@^7.0.0/batch"; 3 | import { define, GatherCandidates, Range } from "./core.ts"; 4 | 5 | export type Candidate = { path: string }; 6 | 7 | export async function init( 8 | denops: Denops, 9 | bufnr: number, 10 | gatherCandidates: GatherCandidates, 11 | ): Promise { 12 | await batch.batch(denops, async (denops) => { 13 | await define( 14 | denops, 15 | bufnr, 16 | "add", 17 | (denops, bufnr, range) => 18 | doAdd(denops, bufnr, range, [], gatherCandidates), 19 | ); 20 | await define( 21 | denops, 22 | bufnr, 23 | "add:intent-to-add", 24 | (denops, bufnr, range) => 25 | doAdd(denops, bufnr, range, ["--intent-to-add"], gatherCandidates), 26 | ); 27 | }); 28 | } 29 | 30 | async function doAdd( 31 | denops: Denops, 32 | bufnr: number, 33 | range: Range, 34 | extraArgs: string[], 35 | gatherCandidates: GatherCandidates, 36 | ): Promise { 37 | const xs = await gatherCandidates(denops, bufnr, range); 38 | await denops.dispatch("gin", "command", "silent", [ 39 | "add", 40 | "--ignore-errors", 41 | "--force", 42 | ...extraArgs, 43 | "--", 44 | ...xs.map((x) => x.path), 45 | ]); 46 | } 47 | -------------------------------------------------------------------------------- /denops/gin/action/branch_delete.ts: -------------------------------------------------------------------------------- 1 | import type { Denops } from "jsr:@denops/std@^7.0.0"; 2 | import * as batch from "jsr:@denops/std@^7.0.0/batch"; 3 | import { define, GatherCandidates, Range } from "./core.ts"; 4 | 5 | export type Candidate = 6 | | { kind: "remote"; branch: string; remote: string } 7 | | { kind?: "alias" | "local"; branch: string }; 8 | 9 | export async function init( 10 | denops: Denops, 11 | bufnr: number, 12 | gatherCandidates: GatherCandidates, 13 | ): Promise { 14 | await batch.batch(denops, async (denops) => { 15 | await define( 16 | denops, 17 | bufnr, 18 | "delete", 19 | (denops, bufnr, range) => 20 | doDelete(denops, bufnr, range, false, gatherCandidates), 21 | ); 22 | await define( 23 | denops, 24 | bufnr, 25 | "delete:force", 26 | (denops, bufnr, range) => 27 | doDelete(denops, bufnr, range, true, gatherCandidates), 28 | ); 29 | }); 30 | } 31 | 32 | async function doDelete( 33 | denops: Denops, 34 | bufnr: number, 35 | range: Range, 36 | force: boolean, 37 | gatherCandidates: GatherCandidates, 38 | ): Promise { 39 | const xs = await gatherCandidates(denops, bufnr, range); 40 | for (const x of xs) { 41 | switch (x.kind) { 42 | case "alias": 43 | continue; 44 | case "remote": 45 | await denops.dispatch("gin", "command", "", [ 46 | "push", 47 | "--delete", 48 | x.remote, 49 | x.branch, 50 | ]); 51 | break; 52 | default: 53 | await denops.dispatch("gin", "command", "", [ 54 | "branch", 55 | force ? "-D" : "-d", 56 | x.branch, 57 | ]); 58 | break; 59 | } 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /denops/gin/action/branch_move.ts: -------------------------------------------------------------------------------- 1 | import type { Denops } from "jsr:@denops/std@^7.0.0"; 2 | import * as batch from "jsr:@denops/std@^7.0.0/batch"; 3 | import * as helper from "jsr:@denops/std@^7.0.0/helper"; 4 | import { define, GatherCandidates, Range } from "./core.ts"; 5 | 6 | export type Candidate = { branch: string }; 7 | 8 | export async function init( 9 | denops: Denops, 10 | bufnr: number, 11 | gatherCandidates: GatherCandidates, 12 | ): Promise { 13 | await batch.batch(denops, async (denops) => { 14 | await define( 15 | denops, 16 | bufnr, 17 | "move", 18 | (denops, bufnr, range) => 19 | doMove(denops, bufnr, range, false, gatherCandidates), 20 | ); 21 | await define( 22 | denops, 23 | bufnr, 24 | "move:force", 25 | (denops, bufnr, range) => 26 | doMove(denops, bufnr, range, true, gatherCandidates), 27 | ); 28 | }); 29 | } 30 | 31 | async function doMove( 32 | denops: Denops, 33 | bufnr: number, 34 | range: Range, 35 | force: boolean, 36 | gatherCandidates: GatherCandidates, 37 | ): Promise { 38 | const xs = await gatherCandidates(denops, bufnr, range); 39 | const x = xs.at(0); 40 | if (!x) { 41 | return; 42 | } 43 | const from = x.branch; 44 | const name = await helper.input(denops, { 45 | prompt: `Rename (from ${from}): `, 46 | text: from, 47 | }); 48 | await denops.cmd('redraw | echo ""'); 49 | if (!name) { 50 | await helper.echoerr(denops, "Cancelled"); 51 | return; 52 | } 53 | await denops.dispatch("gin", "command", "", [ 54 | "branch", 55 | ...(force ? ["--force"] : []), 56 | "--move", 57 | from, 58 | name, 59 | ]); 60 | } 61 | -------------------------------------------------------------------------------- /denops/gin/action/branch_new.ts: -------------------------------------------------------------------------------- 1 | import type { Denops } from "jsr:@denops/std@^7.0.0"; 2 | import * as batch from "jsr:@denops/std@^7.0.0/batch"; 3 | import * as helper from "jsr:@denops/std@^7.0.0/helper"; 4 | import { define, GatherCandidates, Range } from "./core.ts"; 5 | 6 | export type Candidate = { target?: string }; 7 | 8 | export async function init( 9 | denops: Denops, 10 | bufnr: number, 11 | gatherCandidates: GatherCandidates, 12 | ): Promise { 13 | await batch.batch(denops, async (denops) => { 14 | await define( 15 | denops, 16 | bufnr, 17 | "new", 18 | (denops, bufnr, range) => 19 | doNew(denops, bufnr, range, false, gatherCandidates), 20 | ); 21 | await define( 22 | denops, 23 | bufnr, 24 | "new:force", 25 | (denops, bufnr, range) => 26 | doNew(denops, bufnr, range, true, gatherCandidates), 27 | ); 28 | await define( 29 | denops, 30 | bufnr, 31 | "new:orphan", 32 | (denops, bufnr, range) => 33 | doNewOrphan(denops, bufnr, range, gatherCandidates), 34 | ); 35 | }); 36 | } 37 | 38 | async function doNew( 39 | denops: Denops, 40 | bufnr: number, 41 | range: Range, 42 | force: boolean, 43 | gatherCandidates: GatherCandidates, 44 | ): Promise { 45 | const xs = await gatherCandidates(denops, bufnr, range); 46 | const x = xs.at(0); 47 | const from = x?.target ?? "HEAD"; 48 | const name = await helper.input(denops, { 49 | prompt: `New branch (from ${from}): `, 50 | }); 51 | await denops.cmd('redraw | echo ""'); 52 | if (!name) { 53 | await helper.echoerr(denops, "Cancelled"); 54 | return; 55 | } 56 | await denops.dispatch("gin", "command", "", [ 57 | "switch", 58 | force ? "-C" : "-c", 59 | name, 60 | from, 61 | ]); 62 | } 63 | 64 | async function doNewOrphan( 65 | denops: Denops, 66 | _bufnr: number, 67 | _range: Range, 68 | _gatherCandidates: GatherCandidates, 69 | ): Promise { 70 | const name = await helper.input(denops, { 71 | prompt: "New branch (orphan): ", 72 | }); 73 | await denops.cmd('redraw | echo ""'); 74 | if (!name) { 75 | await helper.echoerr(denops, "Cancelled"); 76 | return; 77 | } 78 | await denops.dispatch("gin", "command", "", ["switch", "--orphan", name]); 79 | } 80 | -------------------------------------------------------------------------------- /denops/gin/action/browse.ts: -------------------------------------------------------------------------------- 1 | import type { Denops } from "jsr:@denops/std@^7.0.0"; 2 | import * as batch from "jsr:@denops/std@^7.0.0/batch"; 3 | import { define, GatherCandidates, Range } from "./core.ts"; 4 | 5 | export type Candidate = { commit: string; path?: string }; 6 | 7 | export async function init( 8 | denops: Denops, 9 | bufnr: number, 10 | gatherCandidates: GatherCandidates, 11 | ): Promise { 12 | await batch.batch(denops, async (denops) => { 13 | await define( 14 | denops, 15 | bufnr, 16 | "browse", 17 | (denops, bufnr, range) => 18 | doBrowse(denops, bufnr, range, [], gatherCandidates), 19 | ); 20 | await define( 21 | denops, 22 | bufnr, 23 | "browse:yank", 24 | (denops, bufnr, range) => 25 | doBrowse(denops, bufnr, range, ["++yank"], gatherCandidates), 26 | ); 27 | 28 | await define( 29 | denops, 30 | bufnr, 31 | "browse:permalink", 32 | (denops, bufnr, range) => 33 | doBrowse(denops, bufnr, range, ["--permalink"], gatherCandidates), 34 | ); 35 | await define( 36 | denops, 37 | bufnr, 38 | "browse:permalink:yank", 39 | (denops, bufnr, range) => 40 | doBrowse( 41 | denops, 42 | bufnr, 43 | range, 44 | ["--permalink", "++yank"], 45 | gatherCandidates, 46 | ), 47 | ); 48 | 49 | await define( 50 | denops, 51 | bufnr, 52 | "browse:commit", 53 | (denops, bufnr, range) => 54 | doBrowse(denops, bufnr, range, ["--commit"], gatherCandidates), 55 | ); 56 | await define( 57 | denops, 58 | bufnr, 59 | "browse:commit:yank", 60 | (denops, bufnr, range) => 61 | doBrowse( 62 | denops, 63 | bufnr, 64 | range, 65 | ["--commit", "++yank"], 66 | gatherCandidates, 67 | ), 68 | ); 69 | 70 | await define( 71 | denops, 72 | bufnr, 73 | "browse:commit:permalink", 74 | (denops, bufnr, range) => 75 | doBrowse( 76 | denops, 77 | bufnr, 78 | range, 79 | ["--commit", "--permalink"], 80 | gatherCandidates, 81 | ), 82 | ); 83 | await define( 84 | denops, 85 | bufnr, 86 | "browse:commit:permalink:yank", 87 | (denops, bufnr, range) => 88 | doBrowse( 89 | denops, 90 | bufnr, 91 | range, 92 | ["--commit", "--permalink", "++yank"], 93 | gatherCandidates, 94 | ), 95 | ); 96 | 97 | await define( 98 | denops, 99 | bufnr, 100 | "browse:pr", 101 | (denops, bufnr, range) => 102 | doBrowse(denops, bufnr, range, ["--pr"], gatherCandidates), 103 | ); 104 | await define( 105 | denops, 106 | bufnr, 107 | "browse:pr:yank", 108 | (denops, bufnr, range) => 109 | doBrowse(denops, bufnr, range, ["--pr", "++yank"], gatherCandidates), 110 | ); 111 | }); 112 | } 113 | 114 | async function doBrowse( 115 | denops: Denops, 116 | bufnr: number, 117 | range: Range, 118 | extraArgs: string[], 119 | gatherCandidates: GatherCandidates, 120 | ): Promise { 121 | const xs = await gatherCandidates(denops, bufnr, range); 122 | for (const x of xs) { 123 | await denops.dispatch("gin", "browse:command", [ 124 | ...extraArgs, 125 | ...(x.path ? [] : ["++repository"]), 126 | x.commit, 127 | ...(x.path ? [x.path] : []), 128 | ]); 129 | } 130 | } 131 | -------------------------------------------------------------------------------- /denops/gin/action/chaperon.ts: -------------------------------------------------------------------------------- 1 | import type { Denops } from "jsr:@denops/std@^7.0.0"; 2 | import * as batch from "jsr:@denops/std@^7.0.0/batch"; 3 | import { alias, define, GatherCandidates, Range } from "./core.ts"; 4 | 5 | export type Candidate = { path: string }; 6 | 7 | export async function init( 8 | denops: Denops, 9 | bufnr: number, 10 | gatherCandidates: GatherCandidates, 11 | ): Promise { 12 | await batch.batch(denops, async (denops) => { 13 | await define( 14 | denops, 15 | bufnr, 16 | "chaperon:both", 17 | (denops, bufnr, range) => 18 | doChaperon(denops, bufnr, range, [], gatherCandidates), 19 | ); 20 | await define( 21 | denops, 22 | bufnr, 23 | "chaperon:theirs", 24 | (denops, bufnr, range) => 25 | doChaperon(denops, bufnr, range, ["++no-ours"], gatherCandidates), 26 | ); 27 | await define( 28 | denops, 29 | bufnr, 30 | "chaperon:ours", 31 | (denops, bufnr, range) => 32 | doChaperon(denops, bufnr, range, ["++no-theirs"], gatherCandidates), 33 | ); 34 | await alias( 35 | denops, 36 | bufnr, 37 | "chaperon", 38 | "chaperon:both", 39 | ); 40 | }); 41 | } 42 | 43 | async function doChaperon( 44 | denops: Denops, 45 | bufnr: number, 46 | range: Range, 47 | extraArgs: string[], 48 | gatherCandidates: GatherCandidates, 49 | ): Promise { 50 | const xs = await gatherCandidates(denops, bufnr, range); 51 | for (const x of xs) { 52 | await denops.dispatch("gin", "chaperon:command", "", "", [ 53 | "++opener=tabedit", 54 | ...extraArgs, 55 | x.path, 56 | ]); 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /denops/gin/action/cherry_pick.ts: -------------------------------------------------------------------------------- 1 | import type { Denops } from "jsr:@denops/std@^7.0.0"; 2 | import * as batch from "jsr:@denops/std@^7.0.0/batch"; 3 | import { define, GatherCandidates, Range } from "./core.ts"; 4 | 5 | export type Candidate = { commit: string }; 6 | 7 | export async function init( 8 | denops: Denops, 9 | bufnr: number, 10 | gatherCandidates: GatherCandidates, 11 | ): Promise { 12 | await batch.batch(denops, async (denops) => { 13 | await define( 14 | denops, 15 | bufnr, 16 | `cherry-pick`, 17 | (denops, bufnr, range) => 18 | doCherryPick(denops, bufnr, range, "", gatherCandidates), 19 | ); 20 | await define( 21 | denops, 22 | bufnr, 23 | `cherry-pick:1`, 24 | (denops, bufnr, range) => 25 | doCherryPick(denops, bufnr, range, "1", gatherCandidates), 26 | ); 27 | await define( 28 | denops, 29 | bufnr, 30 | `cherry-pick:2`, 31 | (denops, bufnr, range) => 32 | doCherryPick(denops, bufnr, range, "2", gatherCandidates), 33 | ); 34 | }); 35 | } 36 | 37 | async function doCherryPick( 38 | denops: Denops, 39 | bufnr: number, 40 | range: Range, 41 | mainline: "" | "1" | "2", 42 | gatherCandidates: GatherCandidates, 43 | ): Promise { 44 | const xs = await gatherCandidates(denops, bufnr, range); 45 | const x = xs.at(0); 46 | if (!x) { 47 | return; 48 | } 49 | await denops.dispatch("gin", "command", "", [ 50 | "cherry-pick", 51 | ...(mainline === "" ? [] : ["--mainline", mainline]), 52 | x.commit, 53 | ]); 54 | } 55 | -------------------------------------------------------------------------------- /denops/gin/action/diff.ts: -------------------------------------------------------------------------------- 1 | import type { Denops } from "jsr:@denops/std@^7.0.0"; 2 | import * as batch from "jsr:@denops/std@^7.0.0/batch"; 3 | import { alias, define, GatherCandidates, Range } from "./core.ts"; 4 | 5 | export type Candidate = { path: string }; 6 | 7 | export async function init( 8 | denops: Denops, 9 | bufnr: number, 10 | gatherCandidates: GatherCandidates, 11 | ): Promise { 12 | await batch.batch(denops, async (denops) => { 13 | const openers = [ 14 | "edit", 15 | "split", 16 | "vsplit", 17 | "tabedit", 18 | ]; 19 | for (const opener of openers) { 20 | await define( 21 | denops, 22 | bufnr, 23 | `diff:local:${opener}`, 24 | (denops, bufnr, range) => 25 | doDiff(denops, bufnr, range, opener, [], gatherCandidates), 26 | ); 27 | await define( 28 | denops, 29 | bufnr, 30 | `diff:cached:${opener}`, 31 | (denops, bufnr, range) => 32 | doDiff( 33 | denops, 34 | bufnr, 35 | range, 36 | opener, 37 | ["--cached"], 38 | gatherCandidates, 39 | ), 40 | ); 41 | await define( 42 | denops, 43 | bufnr, 44 | `diff:HEAD:${opener}`, 45 | (denops, bufnr, range) => 46 | doDiff( 47 | denops, 48 | bufnr, 49 | range, 50 | opener, 51 | ["HEAD"], 52 | gatherCandidates, 53 | ), 54 | ); 55 | } 56 | await alias( 57 | denops, 58 | bufnr, 59 | "diff:local", 60 | "diff:local:edit", 61 | ); 62 | await alias( 63 | denops, 64 | bufnr, 65 | "diff:cached", 66 | "diff:cached:edit", 67 | ); 68 | await alias( 69 | denops, 70 | bufnr, 71 | "diff:HEAD", 72 | "diff:HEAD:edit", 73 | ); 74 | await alias( 75 | denops, 76 | bufnr, 77 | "diff", 78 | "diff:local", 79 | ); 80 | }); 81 | } 82 | 83 | async function doDiff( 84 | denops: Denops, 85 | bufnr: number, 86 | range: Range, 87 | opener: string, 88 | extraArgs: string[], 89 | gatherCandidates: GatherCandidates, 90 | ): Promise { 91 | const xs = await gatherCandidates(denops, bufnr, range); 92 | for (const x of xs) { 93 | await denops.dispatch("gin", "diff:command", "", "", [ 94 | `++opener=${opener}`, 95 | ...extraArgs, 96 | "--", 97 | x.path, 98 | ]); 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /denops/gin/action/diff_smart.ts: -------------------------------------------------------------------------------- 1 | import type { Denops } from "jsr:@denops/std@^7.0.0"; 2 | import * as batch from "jsr:@denops/std@^7.0.0/batch"; 3 | import { alias, define, GatherCandidates, Range } from "./core.ts"; 4 | 5 | export type Candidate = { path: string; XY: string }; 6 | 7 | export async function init( 8 | denops: Denops, 9 | bufnr: number, 10 | gatherCandidates: GatherCandidates, 11 | ): Promise { 12 | await batch.batch(denops, async (denops) => { 13 | const openers = [ 14 | "edit", 15 | "split", 16 | "vsplit", 17 | "tabedit", 18 | ]; 19 | for (const opener of openers) { 20 | await define( 21 | denops, 22 | bufnr, 23 | `diff:smart:${opener}`, 24 | (denops, bufnr, range) => 25 | doDiffSmart(denops, bufnr, range, opener, gatherCandidates), 26 | ); 27 | } 28 | await alias( 29 | denops, 30 | bufnr, 31 | "diff:smart", 32 | "diff:smart:edit", 33 | ); 34 | await alias( 35 | denops, 36 | bufnr, 37 | "diff", 38 | "diff:smart", 39 | ); 40 | }); 41 | } 42 | 43 | async function doDiffSmart( 44 | denops: Denops, 45 | bufnr: number, 46 | range: Range, 47 | opener: string, 48 | gatherCandidates: GatherCandidates, 49 | ): Promise { 50 | const xs = await gatherCandidates(denops, bufnr, range); 51 | for (const x of xs) { 52 | if (x.XY.startsWith(".")) { 53 | await denops.dispatch("gin", "diff:command", "", "", [ 54 | `++opener=${opener}`, 55 | "--", 56 | x.path, 57 | ]); 58 | } else { 59 | await denops.dispatch("gin", "diff:command", "", "", [ 60 | `++opener=${opener}`, 61 | "--cached", 62 | "--", 63 | x.path, 64 | ]); 65 | } 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /denops/gin/action/echo.ts: -------------------------------------------------------------------------------- 1 | import type { Denops } from "jsr:@denops/std@^7.0.0"; 2 | import * as batch from "jsr:@denops/std@^7.0.0/batch"; 3 | import * as helper from "jsr:@denops/std@^7.0.0/helper"; 4 | import { define, GatherCandidates, Range } from "./core.ts"; 5 | 6 | export type Candidate = unknown; 7 | 8 | export async function init( 9 | denops: Denops, 10 | bufnr: number, 11 | gatherCandidates: GatherCandidates, 12 | ): Promise { 13 | await batch.batch(denops, async (denops) => { 14 | await define( 15 | denops, 16 | bufnr, 17 | "echo", 18 | (denops, bufnr, range) => doEcho(denops, bufnr, range, gatherCandidates), 19 | ); 20 | }); 21 | } 22 | 23 | async function doEcho( 24 | denops: Denops, 25 | bufnr: number, 26 | range: Range, 27 | gatherCandidates: GatherCandidates, 28 | ): Promise { 29 | const xs = await gatherCandidates(denops, bufnr, range); 30 | await helper.echo(denops, JSON.stringify(xs, null, 2)); 31 | } 32 | -------------------------------------------------------------------------------- /denops/gin/action/edit.ts: -------------------------------------------------------------------------------- 1 | import type { Denops } from "jsr:@denops/std@^7.0.0"; 2 | import * as batch from "jsr:@denops/std@^7.0.0/batch"; 3 | import * as buffer from "jsr:@denops/std@^7.0.0/buffer"; 4 | import { alias, define, GatherCandidates, Range } from "./core.ts"; 5 | 6 | export type Candidate = { path: string }; 7 | 8 | export async function init( 9 | denops: Denops, 10 | bufnr: number, 11 | gatherCandidates: GatherCandidates, 12 | ): Promise { 13 | await batch.batch(denops, async (denops) => { 14 | const openers = [ 15 | "edit", 16 | "split", 17 | "vsplit", 18 | "tabedit", 19 | ]; 20 | for (const opener of openers) { 21 | await define( 22 | denops, 23 | bufnr, 24 | `edit:local:${opener}`, 25 | (denops, bufnr, range) => 26 | doEditLocal(denops, bufnr, range, opener, gatherCandidates), 27 | ); 28 | await define( 29 | denops, 30 | bufnr, 31 | `edit:cached:${opener}`, 32 | (denops, bufnr, range) => 33 | doEdit(denops, bufnr, range, opener, [], gatherCandidates), 34 | ); 35 | await define( 36 | denops, 37 | bufnr, 38 | `edit:HEAD:${opener}`, 39 | (denops, bufnr, range) => 40 | doEdit( 41 | denops, 42 | bufnr, 43 | range, 44 | opener, 45 | ["HEAD"], 46 | gatherCandidates, 47 | ), 48 | ); 49 | } 50 | await alias( 51 | denops, 52 | bufnr, 53 | "edit:local", 54 | "edit:local:edit", 55 | ); 56 | await alias( 57 | denops, 58 | bufnr, 59 | "edit:cached", 60 | "edit:cached:edit", 61 | ); 62 | await alias( 63 | denops, 64 | bufnr, 65 | "edit:HEAD", 66 | "edit:HEAD:edit", 67 | ); 68 | await alias( 69 | denops, 70 | bufnr, 71 | "edit", 72 | "edit:local", 73 | ); 74 | }); 75 | } 76 | 77 | async function doEdit( 78 | denops: Denops, 79 | bufnr: number, 80 | range: Range, 81 | opener: string, 82 | extraArgs: string[], 83 | gatherCandidates: GatherCandidates, 84 | ): Promise { 85 | const xs = await gatherCandidates(denops, bufnr, range); 86 | for (const x of xs) { 87 | await denops.dispatch("gin", "edit:command", "", "", [ 88 | `++opener=${opener}`, 89 | ...extraArgs, 90 | x.path, 91 | ]); 92 | } 93 | } 94 | 95 | async function doEditLocal( 96 | denops: Denops, 97 | bufnr: number, 98 | range: Range, 99 | opener: string, 100 | gatherCandidates: GatherCandidates, 101 | ): Promise { 102 | const xs = await gatherCandidates(denops, bufnr, range); 103 | for (const x of xs) { 104 | await buffer.open(denops, x.path, { 105 | opener: opener, 106 | }); 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /denops/gin/action/fixup.ts: -------------------------------------------------------------------------------- 1 | import type { Denops } from "jsr:@denops/std@^7.0.0"; 2 | import * as batch from "jsr:@denops/std@^7.0.0/batch"; 3 | import { alias, define, GatherCandidates, Range } from "./core.ts"; 4 | 5 | export type Candidate = { commit: string }; 6 | 7 | export async function init( 8 | denops: Denops, 9 | bufnr: number, 10 | gatherCandidates: GatherCandidates, 11 | ): Promise { 12 | await batch.batch(denops, async (denops) => { 13 | await define( 14 | denops, 15 | bufnr, 16 | "fixup:fixup", 17 | (denops, bufnr, range) => 18 | doFixup(denops, bufnr, range, gatherCandidates, false), 19 | ); 20 | await define( 21 | denops, 22 | bufnr, 23 | "fixup:instant-fixup", 24 | (denops, bufnr, range) => 25 | doFixup(denops, bufnr, range, gatherCandidates, true), 26 | ); 27 | 28 | const kinds = ["amend", "reword"] as const; 29 | for (const kind of kinds) { 30 | for (const instant of [true, false]) { 31 | await define( 32 | denops, 33 | bufnr, 34 | instant ? `fixup:instant-${kind}` : `fixup:${kind}`, 35 | (denops, bufnr, range) => 36 | doFixupInteractive( 37 | denops, 38 | bufnr, 39 | range, 40 | kind, 41 | gatherCandidates, 42 | instant, 43 | ), 44 | ); 45 | } 46 | } 47 | 48 | await alias( 49 | denops, 50 | bufnr, 51 | "fixup", 52 | "fixup:fixup", 53 | ); 54 | }); 55 | } 56 | 57 | async function doInstantSquash(denops: Denops, commit: string): Promise { 58 | // autosquash without opening an editor 59 | await denops.dispatch("gin", "command", "", [ 60 | "-c", 61 | "sequence.editor=true", 62 | "rebase", 63 | "--interactive", 64 | "--autostash", 65 | "--autosquash", 66 | "--quiet", 67 | `${commit}~`, 68 | ]); 69 | 70 | // suppress false-positive detection of file changes 71 | await denops.cmd("silent checktime"); 72 | } 73 | 74 | async function doFixup( 75 | denops: Denops, 76 | bufnr: number, 77 | range: Range, 78 | gatherCandidates: GatherCandidates, 79 | instant: boolean, 80 | ): Promise { 81 | const xs = await gatherCandidates(denops, bufnr, range); 82 | const commit = xs.map((v) => v.commit).join("\n"); 83 | await denops.dispatch("gin", "command", "", [ 84 | "commit", 85 | `--fixup=${commit}`, 86 | ]); 87 | 88 | if (instant) { 89 | await doInstantSquash(denops, `${commit}~`); 90 | } 91 | } 92 | 93 | async function doFixupInteractive( 94 | denops: Denops, 95 | bufnr: number, 96 | range: Range, 97 | kind: "amend" | "reword", 98 | gatherCandidates: GatherCandidates, 99 | instant: boolean, 100 | ): Promise { 101 | const xs = await gatherCandidates(denops, bufnr, range); 102 | const commit = xs.map((v) => v.commit).join("\n"); 103 | // Do not block Vim so that users can edit commit message 104 | const args = ["commit", `--fixup=${kind}:${commit}`]; 105 | if (instant) { 106 | args.push("--quiet"); 107 | } 108 | denops 109 | .dispatch("gin", "command", "", args) 110 | .then( 111 | instant ? () => doInstantSquash(denops, `${commit}~`) : undefined, 112 | (e) => console.error(`failed to execute git commit: ${e}`), 113 | ); 114 | } 115 | -------------------------------------------------------------------------------- /denops/gin/action/log.ts: -------------------------------------------------------------------------------- 1 | import type { Denops } from "jsr:@denops/std@^7.0.0"; 2 | import * as batch from "jsr:@denops/std@^7.0.0/batch"; 3 | import { alias, define, GatherCandidates, Range } from "./core.ts"; 4 | 5 | export type Candidate = { commitish: string }; 6 | 7 | export async function init( 8 | denops: Denops, 9 | bufnr: number, 10 | gatherCandidates: GatherCandidates, 11 | ): Promise { 12 | await batch.batch(denops, async (denops) => { 13 | const openers = [ 14 | "edit", 15 | "split", 16 | "vsplit", 17 | "tabedit", 18 | ]; 19 | for (const opener of openers) { 20 | await define( 21 | denops, 22 | bufnr, 23 | `log:${opener}`, 24 | (denops, bufnr, range) => 25 | doLog(denops, bufnr, range, opener, [], gatherCandidates), 26 | ); 27 | } 28 | await alias( 29 | denops, 30 | bufnr, 31 | "log", 32 | "log:edit", 33 | ); 34 | }); 35 | } 36 | 37 | async function doLog( 38 | denops: Denops, 39 | bufnr: number, 40 | range: Range, 41 | opener: string, 42 | extraArgs: string[], 43 | gatherCandidates: GatherCandidates, 44 | ): Promise { 45 | const xs = await gatherCandidates(denops, bufnr, range); 46 | for (const x of xs) { 47 | await denops.dispatch("gin", "log:command", "", "", [ 48 | `++opener=${opener}`, 49 | ...extraArgs, 50 | x.commitish, 51 | ]); 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /denops/gin/action/merge.ts: -------------------------------------------------------------------------------- 1 | import type { Denops } from "jsr:@denops/std@^7.0.0"; 2 | import * as batch from "jsr:@denops/std@^7.0.0/batch"; 3 | import { alias, define, GatherCandidates, Range } from "./core.ts"; 4 | 5 | export type Candidate = { commit: string }; 6 | 7 | export async function init( 8 | denops: Denops, 9 | bufnr: number, 10 | gatherCandidates: GatherCandidates, 11 | ): Promise { 12 | await batch.batch(denops, async (denops) => { 13 | await define( 14 | denops, 15 | bufnr, 16 | "merge:ff", 17 | (denops, bufnr, range) => 18 | doMerge(denops, bufnr, range, "ff", gatherCandidates), 19 | ); 20 | await define( 21 | denops, 22 | bufnr, 23 | "merge:no-ff", 24 | (denops, bufnr, range) => 25 | doMerge(denops, bufnr, range, "no-ff", gatherCandidates), 26 | ); 27 | await define( 28 | denops, 29 | bufnr, 30 | "merge:ff-only", 31 | (denops, bufnr, range) => 32 | doMerge(denops, bufnr, range, "ff-only", gatherCandidates), 33 | ); 34 | await alias( 35 | denops, 36 | bufnr, 37 | "merge", 38 | "merge:ff", 39 | ); 40 | }); 41 | } 42 | 43 | async function doMerge( 44 | denops: Denops, 45 | bufnr: number, 46 | range: Range, 47 | method: "ff" | "no-ff" | "ff-only", 48 | gatherCandidates: GatherCandidates, 49 | ): Promise { 50 | const xs = await gatherCandidates(denops, bufnr, range); 51 | const x = xs.at(0); 52 | if (!x) { 53 | return; 54 | } 55 | await denops.dispatch("gin", "command", "", [ 56 | "merge", 57 | `--${method}`, 58 | x.commit, 59 | ]); 60 | } 61 | -------------------------------------------------------------------------------- /denops/gin/action/patch.ts: -------------------------------------------------------------------------------- 1 | import type { Denops } from "jsr:@denops/std@^7.0.0"; 2 | import * as batch from "jsr:@denops/std@^7.0.0/batch"; 3 | import { alias, define, GatherCandidates, Range } from "./core.ts"; 4 | 5 | export type Candidate = { path: string }; 6 | 7 | export async function init( 8 | denops: Denops, 9 | bufnr: number, 10 | gatherCandidates: GatherCandidates, 11 | ): Promise { 12 | await batch.batch(denops, async (denops) => { 13 | await define( 14 | denops, 15 | bufnr, 16 | "patch:both", 17 | (denops, bufnr, range) => 18 | doPatch(denops, bufnr, range, [], gatherCandidates), 19 | ); 20 | await define( 21 | denops, 22 | bufnr, 23 | "patch:head", 24 | (denops, bufnr, range) => 25 | doPatch(denops, bufnr, range, ["++no-worktree"], gatherCandidates), 26 | ); 27 | await define( 28 | denops, 29 | bufnr, 30 | "patch:worktree", 31 | (denops, bufnr, range) => 32 | doPatch(denops, bufnr, range, ["++no-head"], gatherCandidates), 33 | ); 34 | await alias( 35 | denops, 36 | bufnr, 37 | "patch", 38 | "patch:both", 39 | ); 40 | }); 41 | } 42 | 43 | async function doPatch( 44 | denops: Denops, 45 | bufnr: number, 46 | range: Range, 47 | extraArgs: string[], 48 | gatherCandidates: GatherCandidates, 49 | ): Promise { 50 | const xs = await gatherCandidates(denops, bufnr, range); 51 | for (const x of xs) { 52 | await denops.dispatch("gin", "patch:command", "", "", [ 53 | `++opener=tabedit`, 54 | ...extraArgs, 55 | x.path, 56 | ]); 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /denops/gin/action/rebase.ts: -------------------------------------------------------------------------------- 1 | import type { Denops } from "jsr:@denops/std@^7.0.0"; 2 | import * as batch from "jsr:@denops/std@^7.0.0/batch"; 3 | import * as helper from "jsr:@denops/std@^7.0.0/helper"; 4 | import { define, GatherCandidates, Range } from "./core.ts"; 5 | 6 | export type Candidate = { commit: string }; 7 | 8 | export async function init( 9 | denops: Denops, 10 | bufnr: number, 11 | gatherCandidates: GatherCandidates, 12 | ): Promise { 13 | await batch.batch(denops, async (denops) => { 14 | await define( 15 | denops, 16 | bufnr, 17 | "rebase", 18 | (denops, bufnr, range) => 19 | doRebase(denops, bufnr, range, gatherCandidates), 20 | ); 21 | await define( 22 | denops, 23 | bufnr, 24 | "rebase:i", 25 | (denops, bufnr, range) => 26 | doRebaseInteractive(denops, bufnr, range, gatherCandidates), 27 | ); 28 | await define( 29 | denops, 30 | bufnr, 31 | "rebase:instant-drop", 32 | (denops, bufnr, range) => 33 | doRebaseInstantDrop(denops, bufnr, range, gatherCandidates), 34 | ); 35 | }); 36 | } 37 | 38 | async function doRebase( 39 | denops: Denops, 40 | bufnr: number, 41 | range: Range, 42 | gatherCandidates: GatherCandidates, 43 | ): Promise { 44 | const xs = await gatherCandidates(denops, bufnr, range); 45 | const x = xs.at(0); 46 | if (!x) { 47 | return; 48 | } 49 | await denops.dispatch("gin", "command", "", [ 50 | "rebase", 51 | x.commit, 52 | ]); 53 | 54 | // suppress false-positive detection of file changes 55 | await denops.cmd("silent checktime"); 56 | } 57 | 58 | async function doRebaseInteractive( 59 | denops: Denops, 60 | bufnr: number, 61 | range: Range, 62 | gatherCandidates: GatherCandidates, 63 | ): Promise { 64 | const xs = await gatherCandidates(denops, bufnr, range); 65 | const x = xs.at(0); 66 | if (!x) { 67 | return; 68 | } 69 | // NOTE: 70 | // We must NOT await the command otherwise Vim would freeze 71 | // because command proxy could not work if we await here. 72 | denops.dispatch("gin", "command", "", [ 73 | "rebase", 74 | "--interactive", 75 | x.commit, 76 | ]).catch(async (e) => { 77 | await helper.echoerr(denops, e.toString()); 78 | }).then( 79 | // suppress false-positive detection of file changes 80 | // NOTE: must be done on resolve because the rebase is not awaited 81 | () => denops.cmd("silent checktime"), 82 | ); 83 | } 84 | 85 | async function doRebaseInstantDrop( 86 | denops: Denops, 87 | bufnr: number, 88 | range: Range, 89 | gatherCandidates: GatherCandidates, 90 | ): Promise { 91 | const xs = await gatherCandidates(denops, bufnr, range); 92 | const x = xs.at(0); 93 | if (!x) { 94 | return; 95 | } 96 | await denops.dispatch("gin", "command", "", [ 97 | "rebase", 98 | "--onto", 99 | `${x.commit}~`, 100 | x.commit, 101 | "HEAD", 102 | ]); 103 | 104 | // suppress false-positive detection of file changes 105 | await denops.cmd("silent checktime"); 106 | } 107 | -------------------------------------------------------------------------------- /denops/gin/action/reset.ts: -------------------------------------------------------------------------------- 1 | import type { Denops } from "jsr:@denops/std@^7.0.0"; 2 | import * as batch from "jsr:@denops/std@^7.0.0/batch"; 3 | import { define, GatherCandidates, Range } from "./core.ts"; 4 | 5 | export type Candidate = { commit: string }; 6 | 7 | export async function init( 8 | denops: Denops, 9 | bufnr: number, 10 | gatherCandidates: GatherCandidates, 11 | ): Promise { 12 | await batch.batch(denops, async (denops) => { 13 | await define( 14 | denops, 15 | bufnr, 16 | "reset", 17 | (denops, bufnr, range) => 18 | doReset(denops, bufnr, range, "", gatherCandidates), 19 | ); 20 | await define( 21 | denops, 22 | bufnr, 23 | "reset:soft", 24 | (denops, bufnr, range) => 25 | doReset(denops, bufnr, range, "soft", gatherCandidates), 26 | ); 27 | await define( 28 | denops, 29 | bufnr, 30 | "reset:hard", 31 | (denops, bufnr, range) => 32 | doReset(denops, bufnr, range, "hard", gatherCandidates), 33 | ); 34 | await define( 35 | denops, 36 | bufnr, 37 | "reset:merge", 38 | (denops, bufnr, range) => 39 | doReset(denops, bufnr, range, "merge", gatherCandidates), 40 | ); 41 | await define( 42 | denops, 43 | bufnr, 44 | "reset:keep", 45 | (denops, bufnr, range) => 46 | doReset(denops, bufnr, range, "keep", gatherCandidates), 47 | ); 48 | }); 49 | } 50 | 51 | export async function doReset( 52 | denops: Denops, 53 | bufnr: number, 54 | range: Range, 55 | mode: "" | "soft" | "hard" | "merge" | "keep", 56 | gatherCandidates: GatherCandidates, 57 | ): Promise { 58 | const xs = await gatherCandidates(denops, bufnr, range); 59 | const x = xs.at(0); 60 | if (!x) { 61 | return; 62 | } 63 | await denops.dispatch("gin", "command", "", [ 64 | "reset", 65 | "--quiet", 66 | ...(mode ? [`--${mode}`] : []), 67 | x.commit, 68 | ]); 69 | } 70 | -------------------------------------------------------------------------------- /denops/gin/action/reset_file.ts: -------------------------------------------------------------------------------- 1 | import type { Denops } from "jsr:@denops/std@^7.0.0"; 2 | import * as batch from "jsr:@denops/std@^7.0.0/batch"; 3 | import { define, GatherCandidates, Range } from "./core.ts"; 4 | 5 | export type Candidate = { path: string }; 6 | 7 | export async function init( 8 | denops: Denops, 9 | bufnr: number, 10 | gatherCandidates: GatherCandidates, 11 | ): Promise { 12 | await batch.batch(denops, async (denops) => { 13 | await define( 14 | denops, 15 | bufnr, 16 | "reset", 17 | (denops, bufnr, range) => 18 | doResetFile(denops, bufnr, range, gatherCandidates), 19 | ); 20 | }); 21 | } 22 | 23 | export async function doResetFile( 24 | denops: Denops, 25 | bufnr: number, 26 | range: Range, 27 | gatherCandidates: GatherCandidates, 28 | ): Promise { 29 | const xs = await gatherCandidates(denops, bufnr, range); 30 | await denops.dispatch("gin", "command", "", [ 31 | "reset", 32 | "--quiet", 33 | "--", 34 | ...xs.map((x) => x.path), 35 | ]); 36 | } 37 | -------------------------------------------------------------------------------- /denops/gin/action/restore.ts: -------------------------------------------------------------------------------- 1 | import type { Denops } from "jsr:@denops/std@^7.0.0"; 2 | import * as batch from "jsr:@denops/std@^7.0.0/batch"; 3 | import { define, GatherCandidates, Range } from "./core.ts"; 4 | 5 | export type Candidate = { path: string }; 6 | 7 | export async function init( 8 | denops: Denops, 9 | bufnr: number, 10 | gatherCandidates: GatherCandidates, 11 | ): Promise { 12 | await batch.batch(denops, async (denops) => { 13 | await define( 14 | denops, 15 | bufnr, 16 | "restore", 17 | (denops, bufnr, range) => 18 | doRestore( 19 | denops, 20 | bufnr, 21 | range, 22 | ["--ignore-unmerged"], 23 | gatherCandidates, 24 | ), 25 | ); 26 | await define( 27 | denops, 28 | bufnr, 29 | "restore:staged", 30 | (denops, bufnr, range) => 31 | doRestore( 32 | denops, 33 | bufnr, 34 | range, 35 | ["--ignore-unmerged", "--staged"], 36 | gatherCandidates, 37 | ), 38 | ); 39 | await define( 40 | denops, 41 | bufnr, 42 | "restore:ours", 43 | (denops, bufnr, range) => 44 | doRestore( 45 | denops, 46 | bufnr, 47 | range, 48 | ["--ignore-unmerged", "--ours"], 49 | gatherCandidates, 50 | ), 51 | ); 52 | await define( 53 | denops, 54 | bufnr, 55 | "restore:theirs", 56 | (denops, bufnr, range) => 57 | doRestore( 58 | denops, 59 | bufnr, 60 | range, 61 | ["--ignore-unmerged", "--theirs"], 62 | gatherCandidates, 63 | ), 64 | ); 65 | await define( 66 | denops, 67 | bufnr, 68 | "restore:conflict:merge", 69 | (denops, bufnr, range) => 70 | doRestore(denops, bufnr, range, ["--conflict=merge"], gatherCandidates), 71 | ); 72 | await define( 73 | denops, 74 | bufnr, 75 | "restore:conflict:diff3", 76 | (denops, bufnr, range) => 77 | doRestore(denops, bufnr, range, ["--conflict=diff3"], gatherCandidates), 78 | ); 79 | }); 80 | } 81 | 82 | async function doRestore( 83 | denops: Denops, 84 | bufnr: number, 85 | range: Range, 86 | extraArgs: string[], 87 | gatherCandidates: GatherCandidates, 88 | ): Promise { 89 | const xs = await gatherCandidates(denops, bufnr, range); 90 | await denops.dispatch("gin", "command", "", [ 91 | "restore", 92 | "--quiet", 93 | ...extraArgs, 94 | "--", 95 | ...xs.map((x) => x.path), 96 | ]); 97 | } 98 | -------------------------------------------------------------------------------- /denops/gin/action/revert.ts: -------------------------------------------------------------------------------- 1 | import type { Denops } from "jsr:@denops/std@^7.0.0"; 2 | import * as batch from "jsr:@denops/std@^7.0.0/batch"; 3 | import { define, GatherCandidates, Range } from "./core.ts"; 4 | 5 | export type Candidate = { commit: string }; 6 | 7 | export async function init( 8 | denops: Denops, 9 | bufnr: number, 10 | gatherCandidates: GatherCandidates, 11 | ): Promise { 12 | await batch.batch(denops, async (denops) => { 13 | await define( 14 | denops, 15 | bufnr, 16 | `revert`, 17 | (denops, bufnr, range) => 18 | doRevert(denops, bufnr, range, "", gatherCandidates), 19 | ); 20 | await define( 21 | denops, 22 | bufnr, 23 | `revert:1`, 24 | (denops, bufnr, range) => 25 | doRevert(denops, bufnr, range, "1", gatherCandidates), 26 | ); 27 | await define( 28 | denops, 29 | bufnr, 30 | `revert:2`, 31 | (denops, bufnr, range) => 32 | doRevert(denops, bufnr, range, "2", gatherCandidates), 33 | ); 34 | }); 35 | } 36 | 37 | async function doRevert( 38 | denops: Denops, 39 | bufnr: number, 40 | range: Range, 41 | mainline: "" | "1" | "2", 42 | gatherCandidates: GatherCandidates, 43 | ): Promise { 44 | const xs = await gatherCandidates(denops, bufnr, range); 45 | const x = xs.at(0); 46 | if (!x) { 47 | return; 48 | } 49 | await denops.dispatch("gin", "command", "", [ 50 | "revert", 51 | ...(mainline ? [] : ["--mainline", mainline]), 52 | x.commit, 53 | ]); 54 | } 55 | -------------------------------------------------------------------------------- /denops/gin/action/rm.ts: -------------------------------------------------------------------------------- 1 | import type { Denops } from "jsr:@denops/std@^7.0.0"; 2 | import * as batch from "jsr:@denops/std@^7.0.0/batch"; 3 | import { define, GatherCandidates, Range } from "./core.ts"; 4 | 5 | export type Candidate = { path: string }; 6 | 7 | export async function init( 8 | denops: Denops, 9 | bufnr: number, 10 | gatherCandidates: GatherCandidates, 11 | ): Promise { 12 | await batch.batch(denops, async (denops) => { 13 | await define( 14 | denops, 15 | bufnr, 16 | "rm", 17 | (denops, bufnr, range) => 18 | doRm(denops, bufnr, range, [], gatherCandidates), 19 | ); 20 | await define( 21 | denops, 22 | bufnr, 23 | "rm:force", 24 | (denops, bufnr, range) => 25 | doRm(denops, bufnr, range, ["--force"], gatherCandidates), 26 | ); 27 | }); 28 | } 29 | 30 | async function doRm( 31 | denops: Denops, 32 | bufnr: number, 33 | range: Range, 34 | extraArgs: string[], 35 | gatherCandidates: GatherCandidates, 36 | ): Promise { 37 | const xs = await gatherCandidates(denops, bufnr, range); 38 | await denops.dispatch("gin", "command", "", [ 39 | "rm", 40 | "--quiet", 41 | "--ignore-unmatch", 42 | "--cached", 43 | ...extraArgs, 44 | "--", 45 | ...xs.map((x) => x.path), 46 | ]); 47 | } 48 | -------------------------------------------------------------------------------- /denops/gin/action/show.ts: -------------------------------------------------------------------------------- 1 | import type { Denops } from "jsr:@denops/std@^7.0.0"; 2 | import * as batch from "jsr:@denops/std@^7.0.0/batch"; 3 | import { alias, define, GatherCandidates, Range } from "./core.ts"; 4 | 5 | export type Candidate = { commit: string }; 6 | 7 | export async function init( 8 | denops: Denops, 9 | bufnr: number, 10 | gatherCandidates: GatherCandidates, 11 | ): Promise { 12 | const openers = [ 13 | "edit", 14 | "split", 15 | "vsplit", 16 | "tabedit", 17 | ]; 18 | await batch.batch(denops, async (denops) => { 19 | for (const opener of openers) { 20 | await define( 21 | denops, 22 | bufnr, 23 | `show:${opener}`, 24 | (denops, bufnr, range) => 25 | doShow(denops, bufnr, range, opener, false, gatherCandidates), 26 | ); 27 | await define( 28 | denops, 29 | bufnr, 30 | `show:${opener}:emojify`, 31 | (denops, bufnr, range) => 32 | doShow(denops, bufnr, range, opener, true, gatherCandidates), 33 | ); 34 | } 35 | await alias( 36 | denops, 37 | bufnr, 38 | "show", 39 | "show:edit", 40 | ); 41 | await alias( 42 | denops, 43 | bufnr, 44 | "show:emojify", 45 | "show:edit:emojify", 46 | ); 47 | }); 48 | } 49 | 50 | async function doShow( 51 | denops: Denops, 52 | bufnr: number, 53 | range: Range, 54 | opener: string, 55 | emojify: boolean, 56 | gatherCandidates: GatherCandidates, 57 | ): Promise { 58 | const xs = await gatherCandidates(denops, bufnr, range); 59 | for (const x of xs) { 60 | await denops.dispatch("gin", "buffer:command", "", "", [ 61 | `++opener=${opener}`, 62 | ...(emojify ? [`++emojify`] : []), 63 | "show", 64 | x.commit, 65 | ]); 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /denops/gin/action/stage.ts: -------------------------------------------------------------------------------- 1 | import type { Denops } from "jsr:@denops/std@^7.0.0"; 2 | import * as batch from "jsr:@denops/std@^7.0.0/batch"; 3 | import { define, GatherCandidates, Range } from "./core.ts"; 4 | import { doResetFile } from "./reset_file.ts"; 5 | 6 | export type Candidate = { path: string; XY: string }; 7 | 8 | export async function init( 9 | denops: Denops, 10 | bufnr: number, 11 | gatherCandidates: GatherCandidates, 12 | ): Promise { 13 | await batch.batch(denops, async (denops) => { 14 | await define( 15 | denops, 16 | bufnr, 17 | "stage", 18 | (denops, bufnr, range) => doStage(denops, bufnr, range, gatherCandidates), 19 | ); 20 | await define( 21 | denops, 22 | bufnr, 23 | "stage:intent-to-add", 24 | (denops, bufnr, range) => 25 | doStageIntentToAdd(denops, bufnr, range, gatherCandidates), 26 | ); 27 | await define( 28 | denops, 29 | bufnr, 30 | "unstage", 31 | (denops, bufnr, range) => 32 | doResetFile(denops, bufnr, range, gatherCandidates), 33 | ); 34 | await define( 35 | denops, 36 | bufnr, 37 | "unstage:intent-to-add", 38 | (denops, bufnr, range) => 39 | doUnstageIntentToAdd(denops, bufnr, range, gatherCandidates), 40 | ); 41 | }); 42 | } 43 | 44 | async function doStage( 45 | denops: Denops, 46 | bufnr: number, 47 | range: Range, 48 | gatherCandidates: GatherCandidates, 49 | ): Promise { 50 | const xs = await gatherCandidates(denops, bufnr, range); 51 | const xsRemoved = xs.filter((x) => x.XY.endsWith("D")); 52 | const xsOthers = xs.filter((x) => !x.XY.endsWith(".") && !x.XY.endsWith("D")); 53 | if (xsRemoved.length) { 54 | await denops.dispatch("gin", "command", "", [ 55 | "rm", 56 | "--quiet", 57 | "--ignore-unmatch", 58 | "--cached", 59 | "--", 60 | ...xsRemoved.map((x) => x.path), 61 | ]); 62 | } 63 | if (xsOthers.length) { 64 | await denops.dispatch("gin", "command", "", [ 65 | "add", 66 | "--ignore-errors", 67 | "--force", 68 | "--", 69 | ...xsOthers.map((x) => x.path), 70 | ]); 71 | } 72 | } 73 | 74 | async function doStageIntentToAdd( 75 | denops: Denops, 76 | bufnr: number, 77 | range: Range, 78 | gatherCandidates: GatherCandidates, 79 | ): Promise { 80 | const xs = await gatherCandidates(denops, bufnr, range); 81 | const xsUnknown = xs.filter((x) => x.XY === "??"); 82 | const xsRemoved = xs.filter((x) => x.XY.endsWith("D")); 83 | const xsOthers = xs.filter((x) => x.XY !== "??" && !x.XY.endsWith("D")); 84 | if (xsUnknown.length) { 85 | await denops.dispatch("gin", "command", "", [ 86 | "add", 87 | "--ignore-errors", 88 | "--force", 89 | "--intent-to-add", 90 | "--", 91 | ...xsUnknown.map((x) => x.path), 92 | ]); 93 | } 94 | if (xsRemoved.length) { 95 | await denops.dispatch("gin", "command", "", [ 96 | "rm", 97 | "--quiet", 98 | "--ignore-unmatch", 99 | "--cached", 100 | "--", 101 | ...xsRemoved.map((x) => x.path), 102 | ]); 103 | } 104 | if (xsOthers.length) { 105 | await denops.dispatch("gin", "command", "", [ 106 | "add", 107 | "--ignore-errors", 108 | "--force", 109 | "--", 110 | ...xsOthers.map((x) => x.path), 111 | ]); 112 | } 113 | } 114 | 115 | async function doUnstageIntentToAdd( 116 | denops: Denops, 117 | bufnr: number, 118 | range: Range, 119 | gatherCandidates: GatherCandidates, 120 | ): Promise { 121 | const xs = await gatherCandidates(denops, bufnr, range); 122 | const xsAdded = xs.filter((x) => x.XY === "A."); 123 | const xsOthers = xs.filter((x) => x.XY !== "A."); 124 | if (xsAdded.length) { 125 | await denops.dispatch("gin", "command", "", [ 126 | "reset", 127 | "--quiet", 128 | "--", 129 | ...xsAdded.map((x) => x.path), 130 | ]); 131 | await denops.dispatch("gin", "command", "", [ 132 | "add", 133 | "--ignore-errors", 134 | "--force", 135 | "--intent-to-add", 136 | "--", 137 | ...xsAdded.map((x) => x.path), 138 | ]); 139 | } 140 | if (xsOthers.length) { 141 | await denops.dispatch("gin", "command", "", [ 142 | "reset", 143 | "--quiet", 144 | "--", 145 | ...xsOthers.map((x) => x.path), 146 | ]); 147 | } 148 | } 149 | -------------------------------------------------------------------------------- /denops/gin/action/stash.ts: -------------------------------------------------------------------------------- 1 | import type { Denops } from "jsr:@denops/std@^7.0.0"; 2 | import * as batch from "jsr:@denops/std@^7.0.0/batch"; 3 | import { define, GatherCandidates, Range } from "./core.ts"; 4 | 5 | export type Candidate = { path: string }; 6 | 7 | export async function init( 8 | denops: Denops, 9 | bufnr: number, 10 | gatherCandidates: GatherCandidates, 11 | ): Promise { 12 | await batch.batch(denops, async (denops) => { 13 | await define( 14 | denops, 15 | bufnr, 16 | "stash", 17 | (denops, bufnr, range) => 18 | doStash(denops, bufnr, range, [], gatherCandidates), 19 | ); 20 | await define( 21 | denops, 22 | bufnr, 23 | "stash:keep-index", 24 | (denops, bufnr, range) => 25 | doStash(denops, bufnr, range, ["--keep-index"], gatherCandidates), 26 | ); 27 | }); 28 | } 29 | 30 | async function doStash( 31 | denops: Denops, 32 | bufnr: number, 33 | range: Range, 34 | extraArgs: string[], 35 | gatherCandidates: GatherCandidates, 36 | ): Promise { 37 | const xs = await gatherCandidates(denops, bufnr, range); 38 | await denops.dispatch("gin", "command", "", [ 39 | "stash", 40 | "push", 41 | "--all", 42 | ...extraArgs, 43 | "--", 44 | ...xs.map((x) => x.path), 45 | ]); 46 | } 47 | -------------------------------------------------------------------------------- /denops/gin/action/switch.ts: -------------------------------------------------------------------------------- 1 | import type { Denops } from "jsr:@denops/std@^7.0.0"; 2 | import * as batch from "jsr:@denops/std@^7.0.0/batch"; 3 | import { define, GatherCandidates, Range } from "./core.ts"; 4 | 5 | export type Candidate = { target: string }; 6 | 7 | export async function init( 8 | denops: Denops, 9 | bufnr: number, 10 | gatherCandidates: GatherCandidates, 11 | ): Promise { 12 | await batch.batch(denops, async (denops) => { 13 | await define( 14 | denops, 15 | bufnr, 16 | "switch", 17 | (denops, bufnr, range) => 18 | doSwitch(denops, bufnr, range, [], gatherCandidates), 19 | ); 20 | await define( 21 | denops, 22 | bufnr, 23 | "switch:detach", 24 | (denops, bufnr, range) => 25 | doSwitch(denops, bufnr, range, ["--detach"], gatherCandidates), 26 | ); 27 | }); 28 | } 29 | 30 | async function doSwitch( 31 | denops: Denops, 32 | bufnr: number, 33 | range: Range, 34 | extraArgs: string[], 35 | gatherCandidates: GatherCandidates, 36 | ): Promise { 37 | const xs = await gatherCandidates(denops, bufnr, range); 38 | const x = xs.at(0); 39 | if (!x) { 40 | return; 41 | } 42 | await denops.dispatch("gin", "command", "", [ 43 | "switch", 44 | ...extraArgs, 45 | x.target, 46 | ]); 47 | } 48 | -------------------------------------------------------------------------------- /denops/gin/action/tag.ts: -------------------------------------------------------------------------------- 1 | import type { Denops } from "jsr:@denops/std@^7.0.0"; 2 | import * as batch from "jsr:@denops/std@^7.0.0/batch"; 3 | import * as helper from "jsr:@denops/std@^7.0.0/helper"; 4 | import { alias, define, GatherCandidates, Range } from "./core.ts"; 5 | 6 | export type Candidate = { commit: string }; 7 | 8 | export async function init( 9 | denops: Denops, 10 | bufnr: number, 11 | gatherCandidates: GatherCandidates, 12 | ): Promise { 13 | await batch.batch(denops, async (denops) => { 14 | await define( 15 | denops, 16 | bufnr, 17 | "tag:lightweight", 18 | (denops, bufnr, range) => 19 | doTag(denops, bufnr, range, false, gatherCandidates), 20 | ); 21 | await define( 22 | denops, 23 | bufnr, 24 | "tag:annotate", 25 | (denops, bufnr, range) => 26 | doTagInteractive( 27 | denops, 28 | bufnr, 29 | range, 30 | true, 31 | false, 32 | false, 33 | gatherCandidates, 34 | ), 35 | ); 36 | await define( 37 | denops, 38 | bufnr, 39 | "tag:sign", 40 | (denops, bufnr, range) => 41 | doTagInteractive( 42 | denops, 43 | bufnr, 44 | range, 45 | false, 46 | true, 47 | false, 48 | gatherCandidates, 49 | ), 50 | ); 51 | await define( 52 | denops, 53 | bufnr, 54 | "tag:lightweight:force", 55 | (denops, bufnr, range) => 56 | doTag(denops, bufnr, range, true, gatherCandidates), 57 | ); 58 | await define( 59 | denops, 60 | bufnr, 61 | "tag:annotate:force", 62 | (denops, bufnr, range) => 63 | doTagInteractive( 64 | denops, 65 | bufnr, 66 | range, 67 | true, 68 | false, 69 | true, 70 | gatherCandidates, 71 | ), 72 | ); 73 | await define( 74 | denops, 75 | bufnr, 76 | "tag:sign:force", 77 | (denops, bufnr, range) => 78 | doTagInteractive( 79 | denops, 80 | bufnr, 81 | range, 82 | false, 83 | true, 84 | true, 85 | gatherCandidates, 86 | ), 87 | ); 88 | await alias( 89 | denops, 90 | bufnr, 91 | "tag", 92 | "tag:annotate", 93 | ); 94 | }); 95 | } 96 | 97 | async function doTag( 98 | denops: Denops, 99 | bufnr: number, 100 | range: Range, 101 | force: boolean, 102 | gatherCandidates: GatherCandidates, 103 | ): Promise { 104 | const xs = await gatherCandidates(denops, bufnr, range); 105 | const x = xs.at(0); 106 | if (!x) { 107 | return; 108 | } 109 | const name = await helper.input(denops, { 110 | prompt: `Name: `, 111 | }); 112 | if (!name) { 113 | await helper.echo(denops, "Cancelled"); 114 | return; 115 | } 116 | await denops.dispatch("gin", "command", "", [ 117 | "tag", 118 | ...(force ? ["--force"] : []), 119 | name, 120 | x.commit, 121 | ]); 122 | } 123 | 124 | async function doTagInteractive( 125 | denops: Denops, 126 | bufnr: number, 127 | range: Range, 128 | annotate: boolean, 129 | sign: boolean, 130 | force: boolean, 131 | gatherCandidates: GatherCandidates, 132 | ): Promise { 133 | const xs = await gatherCandidates(denops, bufnr, range); 134 | const x = xs.at(0); 135 | if (!x) { 136 | return; 137 | } 138 | const name = await helper.input(denops, { 139 | prompt: `Name: `, 140 | }); 141 | if (!name) { 142 | await helper.echo(denops, "Cancelled"); 143 | return; 144 | } 145 | // NOTE: 146 | // We must NOT await the command otherwise Vim would freeze 147 | // because command proxy could not work if we await here. 148 | denops.dispatch("gin", "command", "", [ 149 | "tag", 150 | ...(annotate ? ["--annotate"] : []), 151 | ...(sign ? ["--sign"] : []), 152 | ...(force ? ["--force"] : []), 153 | name, 154 | x.commit, 155 | ]).catch(async (e) => { 156 | await helper.echoerr(denops, e.toString()); 157 | }).then( 158 | // suppress false-positive detection of file changes 159 | // NOTE: must be done on resolve because the tag is not awaited 160 | () => denops.cmd("silent checktime"), 161 | ); 162 | } 163 | -------------------------------------------------------------------------------- /denops/gin/action/yank.ts: -------------------------------------------------------------------------------- 1 | import type { Denops } from "jsr:@denops/std@^7.0.0"; 2 | import * as batch from "jsr:@denops/std@^7.0.0/batch"; 3 | import { define, GatherCandidates, Range } from "./core.ts"; 4 | import { yank } from "../util/yank.ts"; 5 | 6 | export type Candidate = { value: string }; 7 | 8 | export type Options = { 9 | prefix?: string; 10 | suffix?: string; 11 | }; 12 | 13 | export async function init( 14 | denops: Denops, 15 | bufnr: number, 16 | gatherCandidates: GatherCandidates, 17 | options: Options = {}, 18 | ): Promise { 19 | const prefix = options.prefix ?? ""; 20 | const suffix = options.suffix ?? ""; 21 | await batch.batch(denops, async (denops) => { 22 | await define( 23 | denops, 24 | bufnr, 25 | `${prefix}yank${suffix}`, 26 | (denops, bufnr, range) => 27 | doYank( 28 | denops, 29 | bufnr, 30 | range, 31 | gatherCandidates, 32 | ), 33 | ); 34 | }); 35 | } 36 | 37 | async function doYank( 38 | denops: Denops, 39 | bufnr: number, 40 | range: Range, 41 | gatherCandidates: GatherCandidates, 42 | ): Promise { 43 | const xs = await gatherCandidates(denops, bufnr, range); 44 | const txt = xs.map((v) => v.value).join("\n"); 45 | await yank(denops, txt); 46 | } 47 | -------------------------------------------------------------------------------- /denops/gin/command/bare/command.ts: -------------------------------------------------------------------------------- 1 | import type { Denops } from "jsr:@denops/std@^7.0.0"; 2 | import * as autocmd from "jsr:@denops/std@^7.0.0/autocmd"; 3 | import * as helper from "jsr:@denops/std@^7.0.0/helper"; 4 | import * as option from "jsr:@denops/std@^7.0.0/option"; 5 | import { removeAnsiEscapeCode } from "../../util/ansi_escape_code.ts"; 6 | import { execute } from "../../git/executor.ts"; 7 | 8 | export type ExecOptions = { 9 | worktree?: string; 10 | encoding?: string; 11 | fileformat?: string; 12 | stdoutIndicator?: string; 13 | stderrIndicator?: string; 14 | }; 15 | 16 | export async function exec( 17 | denops: Denops, 18 | args: string[], 19 | options: ExecOptions = {}, 20 | ): Promise { 21 | const eventignore = await option.eventignore.get(denops); 22 | const { stdout, stderr } = await execute(denops, args, { 23 | worktree: options.worktree, 24 | throwOnError: true, 25 | stdoutIndicator: options.stdoutIndicator, 26 | stderrIndicator: options.stderrIndicator, 27 | }); 28 | const encoding = options.encoding ?? "utf8"; 29 | const decoder = new TextDecoder(encoding); 30 | const text = decoder.decode(new Uint8Array([...stdout, ...stderr])); 31 | const content = text.split( 32 | getSeparator(options.fileformat), 33 | ); 34 | await helper.echo( 35 | denops, 36 | removeAnsiEscapeCode(content.join("\n")), 37 | ); 38 | if (!eventignore.includes("all")) { 39 | await denops.call( 40 | "gin#internal#util#debounce", 41 | "doautocmd User GinCommandPost", 42 | 100, 43 | ); 44 | } 45 | } 46 | 47 | function getSeparator(fileformat: string | undefined): RegExp { 48 | switch (fileformat) { 49 | case "dos": 50 | return /\r\n/g; 51 | case "unix": 52 | return /\n/g; 53 | case "mac": 54 | return /\r/g; 55 | default: 56 | return /\r?\n/g; 57 | } 58 | } 59 | 60 | export async function bind(denops: Denops, bufnr: number): Promise { 61 | await autocmd.group( 62 | denops, 63 | `gin_core_echo_command_bind_${bufnr}`, 64 | (helper) => { 65 | helper.remove(); 66 | helper.define( 67 | "User", 68 | "GinCommandPost", 69 | `call gin#util#reload(${bufnr})`, 70 | ); 71 | }, 72 | ); 73 | } 74 | -------------------------------------------------------------------------------- /denops/gin/command/bare/main.ts: -------------------------------------------------------------------------------- 1 | import type { Denops } from "jsr:@denops/std@^7.0.0"; 2 | import { assert, is } from "jsr:@core/unknownutil@^4.0.0"; 3 | import * as helper from "jsr:@denops/std@^7.0.0/helper"; 4 | import { parseOpts, validateOpts } from "jsr:@denops/std@^7.0.0/argument"; 5 | import { normCmdArgs, parseSilent } from "../../util/cmd.ts"; 6 | import { exec } from "./command.ts"; 7 | 8 | export function main(denops: Denops): void { 9 | denops.dispatcher = { 10 | ...denops.dispatcher, 11 | "command": (mods, args) => { 12 | assert(mods, is.String, { name: "mods" }); 13 | assert(args, is.ArrayOf(is.String), { name: "args" }); 14 | const silent = parseSilent(mods); 15 | return helper.ensureSilent(denops, silent, () => { 16 | return helper.friendlyCall(denops, () => command(denops, args)); 17 | }); 18 | }, 19 | }; 20 | } 21 | 22 | async function command(denops: Denops, args: string[]): Promise { 23 | const [opts, residue] = parseOpts(await normCmdArgs(denops, args)); 24 | validateOpts(opts, [ 25 | "worktree", 26 | "enc", 27 | "encoding", 28 | "ff", 29 | "fileformat", 30 | ]); 31 | await exec(denops, residue, { 32 | worktree: opts.worktree, 33 | encoding: opts.enc ?? opts.encoding, 34 | fileformat: opts.ff ?? opts.fileformat, 35 | }); 36 | } 37 | -------------------------------------------------------------------------------- /denops/gin/command/branch/command.ts: -------------------------------------------------------------------------------- 1 | import type { Denops } from "jsr:@denops/std@^7.0.0"; 2 | import { unnullish } from "jsr:@lambdalisue/unnullish@^1.0.0"; 3 | import * as buffer from "jsr:@denops/std@^7.0.0/buffer"; 4 | import * as option from "jsr:@denops/std@^7.0.0/option"; 5 | import { format as formatBufname } from "jsr:@denops/std@^7.0.0/bufname"; 6 | import { Flags } from "jsr:@denops/std@^7.0.0/argument"; 7 | import { findWorktreeFromDenops } from "../../git/worktree.ts"; 8 | 9 | export type ExecOptions = { 10 | worktree?: string; 11 | patterns?: string[]; 12 | flags?: Flags; 13 | opener?: string; 14 | cmdarg?: string; 15 | mods?: string; 16 | bang?: boolean; 17 | }; 18 | 19 | export async function exec( 20 | denops: Denops, 21 | options: ExecOptions = {}, 22 | ): Promise { 23 | const verbose = await option.verbose.get(denops); 24 | 25 | const worktree = await findWorktreeFromDenops(denops, { 26 | worktree: options.worktree, 27 | verbose: !!verbose, 28 | }); 29 | 30 | const bufname = formatBufname({ 31 | scheme: "ginbranch", 32 | expr: worktree, 33 | params: { 34 | ...options.flags ?? {}, 35 | }, 36 | fragment: unnullish(options.patterns, (v) => `${v.join(" ")}$`), 37 | }); 38 | return await buffer.open(denops, bufname, { 39 | opener: options.opener, 40 | cmdarg: options.cmdarg, 41 | mods: options.mods, 42 | bang: options.bang, 43 | }); 44 | } 45 | -------------------------------------------------------------------------------- /denops/gin/command/branch/main.ts: -------------------------------------------------------------------------------- 1 | import type { Denops } from "jsr:@denops/std@^7.0.0"; 2 | import * as helper from "jsr:@denops/std@^7.0.0/helper"; 3 | import { assert, is } from "jsr:@core/unknownutil@^4.0.0"; 4 | import { 5 | builtinOpts, 6 | formatOpts, 7 | parse, 8 | validateOpts, 9 | } from "jsr:@denops/std@^7.0.0/argument"; 10 | 11 | import { fillCmdArgs, normCmdArgs, parseSilent } from "../../util/cmd.ts"; 12 | import { exec } from "./command.ts"; 13 | import { edit } from "./edit.ts"; 14 | 15 | export function main(denops: Denops): void { 16 | denops.dispatcher = { 17 | ...denops.dispatcher, 18 | "branch:command": (bang, mods, args) => { 19 | assert(bang, is.String, { name: "bang" }); 20 | assert(mods, is.String, { name: "mods" }); 21 | assert(args, is.ArrayOf(is.String), { name: "args" }); 22 | const silent = parseSilent(mods); 23 | return helper.ensureSilent(denops, silent, () => { 24 | return helper.friendlyCall( 25 | denops, 26 | () => command(denops, bang, mods, args), 27 | ); 28 | }); 29 | }, 30 | "branch:edit": (bufnr, bufname) => { 31 | assert(bufnr, is.Number, { name: "bufnr" }); 32 | assert(bufname, is.String, { name: "bufname" }); 33 | return helper.friendlyCall(denops, () => edit(denops, bufnr, bufname)); 34 | }, 35 | }; 36 | } 37 | 38 | async function command( 39 | denops: Denops, 40 | bang: string, 41 | mods: string, 42 | args: string[], 43 | ): Promise { 44 | args = await fillCmdArgs(denops, args, "branch"); 45 | args = await normCmdArgs(denops, args); 46 | 47 | const [opts, flags, residue] = parse(args); 48 | validateOpts(opts, [ 49 | "worktree", 50 | "opener", 51 | ...builtinOpts, 52 | ]); 53 | 54 | await exec(denops, { 55 | worktree: opts.worktree, 56 | patterns: residue, 57 | flags, 58 | opener: opts.opener, 59 | cmdarg: formatOpts(opts, builtinOpts).join(" "), 60 | mods, 61 | bang: bang === "!", 62 | }); 63 | } 64 | -------------------------------------------------------------------------------- /denops/gin/command/branch/parser.ts: -------------------------------------------------------------------------------- 1 | const branchAliasPattern = /^\s{2}(remotes\/([^\/]+)\/(\S+))\s+-> (\S+)$/; 2 | const remoteBranchPattern = 3 | /^\s{2}(remotes\/([^\/]+)\/(\S+))\s+([a-f0-9]+) (.*)$/; 4 | const localBranchPattern = 5 | /^([*+ ]) (\(.*?\)|\S+)\s+([a-f0-9]+) (?:\((.*)\) )?(?:\[(\S+)(?:: [^\]]+)?\] )?(.*)$/; 6 | 7 | export class GitBranchParseError extends Error { 8 | constructor(message: string) { 9 | super(message); 10 | this.name = "GitBranchParseError"; 11 | } 12 | } 13 | 14 | type LocalBranch = { 15 | kind: "local"; 16 | record: string; 17 | active: boolean; 18 | target: string; 19 | branch: string; 20 | commit: string; 21 | worktree?: string; 22 | upstream?: string; 23 | message: string; 24 | }; 25 | 26 | type RemoteBranch = { 27 | kind: "remote"; 28 | record: string; 29 | target: string; 30 | remote: string; 31 | branch: string; 32 | commit: string; 33 | message: string; 34 | }; 35 | 36 | type BranchAlias = { 37 | kind: "alias"; 38 | record: string; 39 | target: string; 40 | remote: string; 41 | branch: string; 42 | origin: string; 43 | }; 44 | 45 | export type Branch = LocalBranch | RemoteBranch | BranchAlias; 46 | 47 | export interface GitBranchResult { 48 | branches: Branch[]; 49 | } 50 | 51 | export function parse(content: string[]): GitBranchResult { 52 | const branches: Branch[] = content.filter((v) => v).map((record) => { 53 | const m1 = record.match(branchAliasPattern); 54 | if (m1) { 55 | return { 56 | kind: "alias", 57 | record, 58 | target: m1[1], 59 | remote: m1[2], 60 | branch: m1[3], 61 | origin: m1[4], 62 | }; 63 | } 64 | const m2 = record.match(remoteBranchPattern); 65 | if (m2) { 66 | return { 67 | kind: "remote", 68 | record, 69 | target: m2[1], 70 | remote: m2[2], 71 | branch: m2[3], 72 | commit: m2[4], 73 | message: m2[5], 74 | }; 75 | } 76 | const m3 = record.match(localBranchPattern); 77 | if (m3) { 78 | return { 79 | kind: "local", 80 | record, 81 | active: m3[1] === "*", 82 | target: m3[2], 83 | branch: m3[2], 84 | commit: m3[3], 85 | worktree: m3[4] ?? undefined, 86 | upstream: m3[5] ?? undefined, 87 | message: m3[6], 88 | }; 89 | } 90 | throw new Error(`Failed to parse 'git branch' record '${record}'`); 91 | }); 92 | return { branches }; 93 | } 94 | -------------------------------------------------------------------------------- /denops/gin/command/branch/parser_test.ts: -------------------------------------------------------------------------------- 1 | import { assertSnapshot } from "jsr:@std/testing@^1.0.0/snapshot"; 2 | import { parse } from "./parser.ts"; 3 | 4 | Deno.test("parse", async function (t): Promise { 5 | await t.step("returns proper branches", async (t) => { 6 | const content = await Deno.readTextFile( 7 | new URL("./testdata/branch.txt", import.meta.url), 8 | ); 9 | const result = parse(content.split("\n")); 10 | await assertSnapshot(t, result); 11 | }); 12 | 13 | await t.step("returns proper branches even with worktree", async (t) => { 14 | const content = await Deno.readTextFile( 15 | new URL("./testdata/branch-with-worktree.txt", import.meta.url), 16 | ); 17 | const result = parse(content.split("\n")); 18 | await assertSnapshot(t, result); 19 | }); 20 | }); 21 | -------------------------------------------------------------------------------- /denops/gin/command/branch/testdata/branch-with-worktree.txt: -------------------------------------------------------------------------------- 1 | + fix-branch-on-worktree 3476d25 (/Users/alisue/ghq/github.com/lambdalisue/my [work] (tree)) [main: ahead 1] :herb: Add branch parse test 2 | gin-blame b2a1c55 [main: ahead 1, behind 5] WIP 3 | * main 227f6c0 [origin/main] Merge pull request #102 from lambdalisue/upgrade-git-browse 4 | support-bang-bk de1f346 [support-bang: gone] :+1: A bang (!) to forcibly open a buffer 5 | support-read ecf6a74 [origin/support-read: gone] :+1: Add `postProcessor` options 6 | support-references f1d2830 [origin/support-references: ahead 12, behind 8] :shower: Remove unused variables 7 | upgrade-deps 3ecec96 [origin/upgrade-deps: gone] :herb: Force a custom serializer of `assertSnapshot` 8 | upgrade-git-browse 3a534bc [origin/upgrade-git-browse: gone] :package: Upgrade git-browse 9 | -------------------------------------------------------------------------------- /denops/gin/command/branch/testdata/branch.txt: -------------------------------------------------------------------------------- 1 | gin-blame b2a1c55 [main: ahead 1, behind 5] WIP 2 | * main 227f6c0 [origin/main] Merge pull request #102 from lambdalisue/upgrade-git-browse 3 | support-bang-bk de1f346 [support-bang: gone] :+1: A bang (!) to forcibly open a buffer 4 | support-read ecf6a74 [origin/support-read: gone] :+1: Add `postProcessor` options 5 | support-references f1d2830 [origin/support-references: ahead 12, behind 8] :shower: Remove unused variables 6 | upgrade-deps 3ecec96 [origin/upgrade-deps: gone] :herb: Force a custom serializer of `assertSnapshot` 7 | upgrade-git-browse 3a534bc [origin/upgrade-git-browse: gone] :package: Upgrade git-browse 8 | -------------------------------------------------------------------------------- /denops/gin/command/browse/command.ts: -------------------------------------------------------------------------------- 1 | import type { Denops } from "jsr:@denops/std@^7.0.0"; 2 | import { unnullish } from "jsr:@lambdalisue/unnullish@^1.0.0"; 3 | import { assert, is } from "jsr:@core/unknownutil@^4.0.0"; 4 | import { systemopen } from "jsr:@lambdalisue/systemopen@^1.0.0"; 5 | import { getURL, Options } from "jsr:@lambdalisue/git-browse@^1.0.1/cli"; 6 | import * as batch from "jsr:@denops/std@^7.0.0/batch"; 7 | import * as vars from "jsr:@denops/std@^7.0.0/variable"; 8 | import * as path from "jsr:@std/path@^1.0.0"; 9 | import * as option from "jsr:@denops/std@^7.0.0/option"; 10 | import { findWorktreeFromDenops } from "../../git/worktree.ts"; 11 | import { yank } from "../../util/yank.ts"; 12 | 13 | export type ExecOptions = Omit & { 14 | worktree?: string; 15 | yank?: string | boolean; 16 | noBrowser?: boolean; 17 | }; 18 | 19 | export async function exec( 20 | denops: Denops, 21 | commitish: string, 22 | options: ExecOptions = {}, 23 | ): Promise { 24 | const [verbose, aliases] = await batch.collect(denops, (denops) => [ 25 | option.verbose.get(denops), 26 | vars.g.get(denops, "gin_browse_aliases", {}), 27 | ]); 28 | assert(aliases, is.RecordOf(is.String), { 29 | name: "g:gin_browse_aliases", 30 | }); 31 | 32 | const worktree = await findWorktreeFromDenops(denops, { 33 | worktree: options.worktree, 34 | verbose: !!verbose, 35 | }); 36 | 37 | options.path = unnullish( 38 | options.path, 39 | (p) => path.isAbsolute(p) ? path.relative(worktree, p) : p, 40 | ); 41 | options.path = unnullish( 42 | options.path, 43 | (p) => p === "" ? "." : p, 44 | ); 45 | const url = await getURL(commitish, { 46 | cwd: worktree, 47 | remote: options.remote, 48 | path: options.path, 49 | home: options.home, 50 | commit: options.commit, 51 | pr: options.pr, 52 | permalink: options.permalink, 53 | aliases, 54 | }); 55 | 56 | if (options.yank != null && options.yank !== false) { 57 | await yank( 58 | denops, 59 | url.href, 60 | options.yank === true ? undefined : options.yank, 61 | ); 62 | } 63 | if (options.noBrowser) { 64 | await denops.cmd("echomsg url", { url: url.href }); 65 | } else { 66 | await systemopen(url.href); 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /denops/gin/command/browse/main.ts: -------------------------------------------------------------------------------- 1 | import type { Denops } from "jsr:@denops/std@^7.0.0"; 2 | import { assert, ensure, is } from "jsr:@core/unknownutil@^4.0.0"; 3 | import * as batch from "jsr:@denops/std@^7.0.0/batch"; 4 | import * as fn from "jsr:@denops/std@^7.0.0/function"; 5 | import * as helper from "jsr:@denops/std@^7.0.0/helper"; 6 | import { parse, validateOpts } from "jsr:@denops/std@^7.0.0/argument"; 7 | import { fillCmdArgs, normCmdArgs } from "../../util/cmd.ts"; 8 | import { exec } from "./command.ts"; 9 | 10 | type Range = readonly [number, number]; 11 | 12 | const isRange = is.TupleOf([is.Number, is.Number] as const); 13 | 14 | export function main(denops: Denops): void { 15 | denops.dispatcher = { 16 | ...denops.dispatcher, 17 | "browse:command": (args, range) => { 18 | assert(args, is.ArrayOf(is.String), { name: "args" }); 19 | assert(range, is.UnionOf([is.Undefined, isRange]), { name: "range" }); 20 | return helper.friendlyCall( 21 | denops, 22 | () => 23 | command(denops, args, { 24 | range, 25 | }), 26 | ); 27 | }, 28 | }; 29 | } 30 | 31 | type CommandOptions = { 32 | range?: Range; 33 | }; 34 | 35 | async function command( 36 | denops: Denops, 37 | args: string[], 38 | options: CommandOptions = {}, 39 | ): Promise { 40 | args = await fillCmdArgs(denops, args, "browse"); 41 | args = await normCmdArgs(denops, args); 42 | 43 | const [opts, flags, residue] = parse(args); 44 | validateOpts(opts, [ 45 | "worktree", 46 | "repository", 47 | "yank", 48 | ]); 49 | 50 | const [commitish, rawpath] = parseResidue(residue); 51 | const path = formatPath(await ensurePath(denops, rawpath), options.range); 52 | await exec(denops, commitish ?? "HEAD", { 53 | worktree: opts.worktree, 54 | yank: opts.yank === "" ? true : (opts.yank ?? false), 55 | noBrowser: ("n" in flags || "no-browser" in flags), 56 | remote: ensure(flags.remote, is.UnionOf([is.Undefined, is.String]), { 57 | "message": "REMOTE in --remote={REMOTE} must be string", 58 | }), 59 | path: "repository" in opts ? undefined : path, 60 | home: "home" in flags, 61 | commit: "commit" in flags, 62 | pr: "pr" in flags, 63 | permalink: "permalink" in flags, 64 | }); 65 | } 66 | 67 | function parseResidue( 68 | residue: string[], 69 | ): [string | undefined, string | undefined] { 70 | switch (residue.length) { 71 | // GinBrowse [{options}] 72 | case 0: 73 | return [undefined, undefined]; 74 | // GinBrowse [{options}] {commitish} 75 | case 1: 76 | return [residue[0], undefined]; 77 | // GinBrowse [{options}] {commitish} {path} 78 | case 2: 79 | return [residue[0], residue[1]]; 80 | default: 81 | throw new Error("Invalid number of arguments"); 82 | } 83 | } 84 | 85 | async function ensurePath( 86 | denops: Denops, 87 | path?: string, 88 | ): Promise { 89 | const bufname = await fn.expand(denops, path ?? "%") as string; 90 | const abspath = await fn.fnamemodify(denops, bufname, ":p"); 91 | if (path) { 92 | return abspath; 93 | } 94 | const [buftype, cwd] = await batch.collect(denops, (denops) => [ 95 | fn.getbufvar(denops, "%", "&buftype") as Promise, 96 | fn.getcwd(denops), 97 | ]); 98 | return buftype ? cwd : abspath; 99 | } 100 | 101 | function formatPath(path: string, range?: Range): string { 102 | if (!range) { 103 | return path; 104 | } 105 | const [start, end] = range; 106 | if (start === end) { 107 | return `${path}:${start}`; 108 | } 109 | return `${path}:${start}:${end}`; 110 | } 111 | -------------------------------------------------------------------------------- /denops/gin/command/buffer/command.ts: -------------------------------------------------------------------------------- 1 | import type { Denops } from "jsr:@denops/std@^7.0.0"; 2 | import { unnullish } from "jsr:@lambdalisue/unnullish@^1.0.0"; 3 | import * as buffer from "jsr:@denops/std@^7.0.0/buffer"; 4 | import * as option from "jsr:@denops/std@^7.0.0/option"; 5 | import { format as formatBufname } from "jsr:@denops/std@^7.0.0/bufname"; 6 | import { findWorktreeFromDenops } from "../../git/worktree.ts"; 7 | 8 | export type ExecOptions = { 9 | processor?: string[]; 10 | worktree?: string; 11 | monochrome?: boolean; 12 | opener?: string; 13 | emojify?: boolean; 14 | cmdarg?: string; 15 | mods?: string; 16 | bang?: boolean; 17 | }; 18 | 19 | export async function exec( 20 | denops: Denops, 21 | args: string[], 22 | options: ExecOptions = {}, 23 | ): Promise { 24 | const verbose = await option.verbose.get(denops); 25 | const worktree = await findWorktreeFromDenops(denops, { 26 | worktree: options.worktree, 27 | verbose: !!verbose, 28 | }); 29 | const bufname = formatBufname({ 30 | scheme: "gin", 31 | expr: worktree, 32 | params: { 33 | processor: unnullish(options.processor, (v) => v.join(" ")), 34 | monochrome: unnullish(options.monochrome, (v) => v ? "" : undefined), 35 | emojify: unnullish(options.emojify, (v) => v ? "" : undefined), 36 | }, 37 | fragment: `${args.join(" ")}$`, 38 | }); 39 | return await buffer.open(denops, bufname, { 40 | opener: options.opener, 41 | cmdarg: options.cmdarg, 42 | mods: options.mods, 43 | bang: options.bang, 44 | }); 45 | } 46 | -------------------------------------------------------------------------------- /denops/gin/command/buffer/edit.ts: -------------------------------------------------------------------------------- 1 | import type { Denops } from "jsr:@denops/std@^7.0.0"; 2 | import { emojify } from "jsr:@lambdalisue/github-emoji@^1.0.0"; 3 | import { unnullish } from "jsr:@lambdalisue/unnullish@^1.0.0"; 4 | import { ensure, is } from "jsr:@core/unknownutil@^4.0.0"; 5 | import * as batch from "jsr:@denops/std@^7.0.0/batch"; 6 | import * as buffer from "jsr:@denops/std@^7.0.0/buffer"; 7 | import * as option from "jsr:@denops/std@^7.0.0/option"; 8 | import * as vars from "jsr:@denops/std@^7.0.0/variable"; 9 | import { 10 | builtinOpts, 11 | parseOpts, 12 | validateOpts, 13 | } from "jsr:@denops/std@^7.0.0/argument"; 14 | import { parse as parseBufname } from "jsr:@denops/std@^7.0.0/bufname"; 15 | import { 16 | buildDecorationsFromAnsiEscapeCode, 17 | } from "../../util/ansi_escape_code.ts"; 18 | import { execute } from "../../git/executor.ts"; 19 | 20 | export async function edit( 21 | denops: Denops, 22 | bufnr: number, 23 | bufname: string, 24 | ): Promise { 25 | const cmdarg = await vars.v.get(denops, "cmdarg") as string; 26 | const [opts, _] = parseOpts(cmdarg.split(" ")); 27 | validateOpts(opts, builtinOpts); 28 | const { scheme, expr, params, fragment } = parseBufname(bufname); 29 | if (!fragment) { 30 | throw new Error(`A buffer '${scheme}://' requires a fragment part`); 31 | } 32 | const args = fragment.replace(/\$$/, "").split(" "); 33 | await exec(denops, bufnr, args, { 34 | processor: unnullish( 35 | params?.processor, 36 | (v) => 37 | ensure(v, is.String, { message: "processor must be string" }).split( 38 | " ", 39 | ), 40 | ), 41 | worktree: expr, 42 | monochrome: "monochrome" in (params ?? {}), 43 | encoding: opts.enc ?? opts.encoding, 44 | fileformat: opts.ff ?? opts.fileformat, 45 | emojify: "emojify" in (params ?? {}), 46 | }); 47 | } 48 | 49 | export type ExecOptions = { 50 | processor?: string[]; 51 | worktree?: string; 52 | monochrome?: boolean; 53 | encoding?: string; 54 | fileformat?: string; 55 | emojify?: boolean; 56 | stdoutIndicator?: string; 57 | stderrIndicator?: string; 58 | }; 59 | 60 | export async function exec( 61 | denops: Denops, 62 | bufnr: number, 63 | args: string[], 64 | options: ExecOptions, 65 | ): Promise { 66 | args = [ 67 | ...(options.monochrome ? [] : [ 68 | // It seems 'color.ui' is not enough on Windows 69 | "-c", 70 | "color.branch=always", 71 | "-c", 72 | "color.diff=always", 73 | "-c", 74 | "color.status=always", 75 | "-c", 76 | "color.ui=always", 77 | ]), 78 | ...args, 79 | ]; 80 | const { stdout } = await execute(denops, args, { 81 | processor: options.processor, 82 | worktree: options.worktree, 83 | throwOnError: true, 84 | stdoutIndicator: options.stdoutIndicator ?? "null", 85 | stderrIndicator: options.stderrIndicator, 86 | }); 87 | const { content, fileformat, fileencoding } = await buffer.decode( 88 | denops, 89 | bufnr, 90 | stdout, 91 | { 92 | fileformat: options.fileformat, 93 | fileencoding: options.encoding, 94 | }, 95 | ); 96 | const [trimmed, decorations] = await buildDecorationsFromAnsiEscapeCode( 97 | denops, 98 | options.emojify ? content.map(emojify) : content, 99 | ); 100 | await buffer.replace( 101 | denops, 102 | bufnr, 103 | trimmed, 104 | { 105 | fileformat, 106 | fileencoding, 107 | }, 108 | ); 109 | await buffer.decorate(denops, bufnr, decorations); 110 | await buffer.concrete(denops, bufnr); 111 | await buffer.ensure(denops, bufnr, async () => { 112 | await batch.batch(denops, async (denops) => { 113 | await option.filetype.setLocal(denops, "gin"); 114 | await option.bufhidden.setLocal(denops, "unload"); 115 | await option.buftype.setLocal(denops, "nofile"); 116 | await option.swapfile.setLocal(denops, false); 117 | await option.modifiable.setLocal(denops, false); 118 | }); 119 | }); 120 | } 121 | -------------------------------------------------------------------------------- /denops/gin/command/buffer/main.ts: -------------------------------------------------------------------------------- 1 | import type { Denops } from "jsr:@denops/std@^7.0.0"; 2 | import { unnullish } from "jsr:@lambdalisue/unnullish@^1.0.0"; 3 | import { assert, is } from "jsr:@core/unknownutil@^4.0.0"; 4 | import * as helper from "jsr:@denops/std@^7.0.0/helper"; 5 | import * as buffer from "jsr:@denops/std@^7.0.0/buffer"; 6 | import { 7 | builtinOpts, 8 | formatOpts, 9 | parseOpts, 10 | validateOpts, 11 | } from "jsr:@denops/std@^7.0.0/argument"; 12 | 13 | import { normCmdArgs, parseSilent } from "../../util/cmd.ts"; 14 | import { exec } from "./command.ts"; 15 | import { edit } from "./edit.ts"; 16 | import { read } from "./read.ts"; 17 | 18 | export function main(denops: Denops): void { 19 | denops.dispatcher = { 20 | ...denops.dispatcher, 21 | "buffer:command": (bang, mods, args) => { 22 | assert(bang, is.String, { name: "bang" }); 23 | assert(mods, is.String, { name: "mods" }); 24 | assert(args, is.ArrayOf(is.String), { name: "args" }); 25 | const silent = parseSilent(mods); 26 | return helper.ensureSilent(denops, silent, () => { 27 | return helper.friendlyCall( 28 | denops, 29 | () => command(denops, bang, mods, args), 30 | ); 31 | }); 32 | }, 33 | "buffer:edit": (bufnr, bufname) => { 34 | assert(bufnr, is.Number, { name: "bufnr" }); 35 | assert(bufname, is.String, { name: "bufname" }); 36 | return helper.friendlyCall( 37 | denops, 38 | () => edit(denops, bufnr, bufname), 39 | ); 40 | }, 41 | "buffer:read": (bufnr, bufname) => { 42 | assert(bufnr, is.Number, { name: "bufnr" }); 43 | assert(bufname, is.String, { name: "bufname" }); 44 | return helper.friendlyCall( 45 | denops, 46 | () => read(denops, bufnr, bufname), 47 | ); 48 | }, 49 | }; 50 | } 51 | 52 | async function command( 53 | denops: Denops, 54 | bang: string, 55 | mods: string, 56 | args: string[], 57 | ): Promise { 58 | const [opts, residue] = parseOpts(await normCmdArgs(denops, args)); 59 | validateOpts(opts, [ 60 | "processor", 61 | "worktree", 62 | "monochrome", 63 | "opener", 64 | "emojify", 65 | ...builtinOpts, 66 | ]); 67 | return exec(denops, residue, { 68 | processor: opts.processor?.split(" "), 69 | worktree: opts.worktree, 70 | monochrome: unnullish(opts.monochrome, () => true), 71 | opener: opts.opener, 72 | emojify: unnullish(opts.emojify, () => true), 73 | cmdarg: formatOpts(opts, builtinOpts).join(" "), 74 | mods, 75 | bang: bang === "!", 76 | }); 77 | } 78 | -------------------------------------------------------------------------------- /denops/gin/command/buffer/read.ts: -------------------------------------------------------------------------------- 1 | import type { Denops } from "jsr:@denops/std@^7.0.0"; 2 | import { emojify } from "jsr:@lambdalisue/github-emoji@^1.0.0"; 3 | import { unnullish } from "jsr:@lambdalisue/unnullish@^1.0.0"; 4 | import { ensure, is } from "jsr:@core/unknownutil@^4.0.0"; 5 | import * as buffer from "jsr:@denops/std@^7.0.0/buffer"; 6 | import * as vars from "jsr:@denops/std@^7.0.0/variable"; 7 | import { 8 | builtinOpts, 9 | parseOpts, 10 | validateOpts, 11 | } from "jsr:@denops/std@^7.0.0/argument"; 12 | import { parse as parseBufname } from "jsr:@denops/std@^7.0.0/bufname"; 13 | import { execute } from "../../git/executor.ts"; 14 | 15 | export async function read( 16 | denops: Denops, 17 | bufnr: number, 18 | bufname: string, 19 | ): Promise { 20 | const cmdarg = await vars.v.get(denops, "cmdarg") as string; 21 | const [opts, _] = parseOpts(cmdarg.split(" ")); 22 | validateOpts(opts, builtinOpts); 23 | const { scheme, expr, params, fragment } = parseBufname(bufname); 24 | if (!fragment) { 25 | throw new Error(`A buffer '${scheme}://' requires a fragment part`); 26 | } 27 | const args = fragment.replace(/\$$/, "").split(" "); 28 | await exec(denops, bufnr, args, { 29 | processor: unnullish( 30 | params?.processor, 31 | (v) => 32 | ensure(v, is.String, { message: "processor must be string" }).split( 33 | " ", 34 | ), 35 | ), 36 | worktree: expr, 37 | encoding: opts.enc ?? opts.encoding, 38 | fileformat: opts.ff ?? opts.fileformat, 39 | emojify: "emojify" in (params ?? {}), 40 | }); 41 | } 42 | 43 | export type ExecOptions = { 44 | processor?: string[]; 45 | worktree?: string; 46 | encoding?: string; 47 | fileformat?: string; 48 | emojify?: boolean; 49 | lnum?: number; 50 | }; 51 | 52 | export async function exec( 53 | denops: Denops, 54 | bufnr: number, 55 | args: string[], 56 | options: ExecOptions, 57 | ): Promise { 58 | const { stdout } = await execute(denops, args, { 59 | processor: options.processor, 60 | worktree: options.worktree, 61 | throwOnError: true, 62 | }); 63 | const { content } = await buffer.decode( 64 | denops, 65 | bufnr, 66 | stdout, 67 | { 68 | fileformat: options.fileformat, 69 | fileencoding: options.encoding, 70 | }, 71 | ); 72 | await buffer.append( 73 | denops, 74 | bufnr, 75 | options.emojify ? content.map(emojify) : content, 76 | { 77 | lnum: options.lnum, 78 | }, 79 | ); 80 | } 81 | -------------------------------------------------------------------------------- /denops/gin/command/chaperon/main.ts: -------------------------------------------------------------------------------- 1 | import type { Denops } from "jsr:@denops/std@^7.0.0"; 2 | import * as helper from "jsr:@denops/std@^7.0.0/helper"; 3 | import { assert, is } from "jsr:@core/unknownutil@^4.0.0"; 4 | import { 5 | builtinOpts, 6 | formatOpts, 7 | parseOpts, 8 | validateOpts, 9 | } from "jsr:@denops/std@^7.0.0/argument"; 10 | import { fillCmdArgs, normCmdArgs, parseSilent } from "../../util/cmd.ts"; 11 | import { ensurePath } from "../../util/ensure_path.ts"; 12 | import { exec } from "./command.ts"; 13 | 14 | export function main(denops: Denops): void { 15 | denops.dispatcher = { 16 | ...denops.dispatcher, 17 | "chaperon:command": (bang, mods, args) => { 18 | assert(bang, is.String, { name: "bang" }); 19 | assert(mods, is.String, { name: "mods" }); 20 | assert(args, is.ArrayOf(is.String), { name: "args" }); 21 | const silent = parseSilent(mods); 22 | return helper.ensureSilent(denops, silent, () => { 23 | return helper.friendlyCall( 24 | denops, 25 | () => command(denops, bang, mods, args), 26 | ); 27 | }); 28 | }, 29 | }; 30 | } 31 | 32 | async function command( 33 | denops: Denops, 34 | bang: string, 35 | mods: string, 36 | args: string[], 37 | ): Promise { 38 | args = await fillCmdArgs(denops, args, "chaperon"); 39 | args = await normCmdArgs(denops, args); 40 | 41 | const [opts, residue] = parseOpts(args); 42 | validateOpts(opts, [ 43 | "worktree", 44 | "opener", 45 | "no-ours", 46 | "no-theirs", 47 | ...builtinOpts, 48 | ]); 49 | 50 | const [rawpath] = parseResidue(residue); 51 | await exec(denops, await ensurePath(denops, rawpath), { 52 | worktree: opts.worktree, 53 | opener: opts.opener, 54 | noOurs: "no-ours" in opts, 55 | noTheirs: "no-theirs" in opts, 56 | cmdarg: formatOpts(opts, builtinOpts).join(" "), 57 | mods, 58 | bang: bang === "!", 59 | }); 60 | } 61 | 62 | function parseResidue( 63 | residue: string[], 64 | ): [string | undefined] { 65 | switch (residue.length) { 66 | // GinChaperon [{options}] 67 | case 0: 68 | return [undefined]; 69 | // GinChaperon [{options}] {path} 70 | case 1: 71 | return [residue[0]]; 72 | default: 73 | throw new Error("Invalid number of arguments"); 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /denops/gin/command/chaperon/util.ts: -------------------------------------------------------------------------------- 1 | import * as fs from "jsr:@std/fs@^1.0.0"; 2 | import * as path from "jsr:@std/path@^1.0.0"; 3 | import { findGitdir } from "../../git/finder.ts"; 4 | 5 | const beginMarker = `${"<".repeat(7)} `; 6 | const endMarker = `${">".repeat(7)} `; 7 | 8 | export function stripConflicts(content: string[]): string[] { 9 | let inner = false; 10 | return content.filter((v) => { 11 | if (v.startsWith(beginMarker)) { 12 | inner = true; 13 | return false; 14 | } else if (v.startsWith(endMarker)) { 15 | inner = false; 16 | return false; 17 | } 18 | return !inner; 19 | }); 20 | } 21 | 22 | const validAliasHeads = [ 23 | "MERGE_HEAD", 24 | "REBASE_HEAD", 25 | "CHERRY_PICK_HEAD", 26 | "REVERT_HEAD", 27 | ] as const; 28 | export type AliasHead = typeof validAliasHeads[number]; 29 | 30 | export async function getInProgressAliasHead( 31 | worktree: string, 32 | ): Promise { 33 | const gitdir = await findGitdir(worktree); 34 | for (const head of validAliasHeads) { 35 | if (await fs.exists(path.join(gitdir, head))) { 36 | return head; 37 | } 38 | } 39 | return undefined; 40 | } 41 | -------------------------------------------------------------------------------- /denops/gin/command/chaperon/util_test.ts: -------------------------------------------------------------------------------- 1 | import { assertEquals } from "jsr:@std/assert@^1.0.0"; 2 | import { stripConflicts } from "./util.ts"; 3 | 4 | // https://github.com/ojacobson/conflicts 5 | Deno.test("stripConflicts", async (t) => { 6 | await t.step("Simple edit conflict", () => { 7 | const input = [ 8 | "- Tea", 9 | "- Eggs", 10 | "<<<<<<< HEAD", 11 | "- Bacon", 12 | "=======", 13 | "- Beef", 14 | ">>>>>>> origin/edit-conflict/right", 15 | "- Coffee", 16 | "- A week's worth of rubber bands", 17 | ]; 18 | assertEquals(stripConflicts(input), [ 19 | "- Tea", 20 | "- Eggs", 21 | "- Coffee", 22 | "- A week's worth of rubber bands", 23 | ]); 24 | }); 25 | await t.step("Append conflict", () => { 26 | const input = [ 27 | "- Tea", 28 | "- Eggs", 29 | "- Ham", 30 | "- Coffee", 31 | "- A week's worth of rubber bands", 32 | "<<<<<<< HEAD", 33 | "- Toys for Bob", 34 | "=======", 35 | "- Cookies", 36 | ">>>>>>> origin/append-conflict/right", 37 | ]; 38 | assertEquals(stripConflicts(input), [ 39 | "- Tea", 40 | "- Eggs", 41 | "- Ham", 42 | "- Coffee", 43 | "- A week's worth of rubber bands", 44 | ]); 45 | }); 46 | }); 47 | -------------------------------------------------------------------------------- /denops/gin/command/diff/command.ts: -------------------------------------------------------------------------------- 1 | import type { Denops } from "jsr:@denops/std@^7.0.0"; 2 | import * as path from "jsr:@std/path@^1.0.0"; 3 | import { unnullish } from "jsr:@lambdalisue/unnullish@^1.0.0"; 4 | import * as buffer from "jsr:@denops/std@^7.0.0/buffer"; 5 | import * as option from "jsr:@denops/std@^7.0.0/option"; 6 | import { format as formatBufname } from "jsr:@denops/std@^7.0.0/bufname"; 7 | import { Flags } from "jsr:@denops/std@^7.0.0/argument"; 8 | import { findWorktreeFromDenops } from "../../git/worktree.ts"; 9 | 10 | export type ExecOptions = { 11 | processor?: string[]; 12 | worktree?: string; 13 | commitish?: string; 14 | paths?: string[]; 15 | flags?: Flags; 16 | opener?: string; 17 | cmdarg?: string; 18 | mods?: string; 19 | bang?: boolean; 20 | }; 21 | 22 | export async function exec( 23 | denops: Denops, 24 | options: ExecOptions = {}, 25 | ): Promise { 26 | const verbose = await option.verbose.get(denops); 27 | 28 | const worktree = await findWorktreeFromDenops(denops, { 29 | worktree: options.worktree, 30 | verbose: !!verbose, 31 | }); 32 | 33 | const paths = options.paths?.map((p) => 34 | path.isAbsolute(p) ? path.relative(worktree, p) : p 35 | ); 36 | 37 | const bufname = formatBufname({ 38 | scheme: "gindiff", 39 | expr: worktree, 40 | params: { 41 | ...options.flags ?? {}, 42 | processor: unnullish(options.processor, (v) => v.join(" ")), 43 | commitish: options.commitish, 44 | }, 45 | fragment: unnullish(paths, JSON.stringify), 46 | }); 47 | return await buffer.open(denops, bufname, { 48 | opener: options.opener, 49 | cmdarg: options.cmdarg, 50 | mods: options.mods, 51 | bang: options.bang, 52 | }); 53 | } 54 | -------------------------------------------------------------------------------- /denops/gin/command/diff/commitish.ts: -------------------------------------------------------------------------------- 1 | export const INDEX = Symbol("INDEX"); 2 | export const WORKTREE = Symbol("WORKTREE"); 3 | 4 | export type Commitish = string | typeof INDEX | typeof WORKTREE; 5 | 6 | // git diff 7 | // INDEX -> WORKTREE 8 | // git diff A 9 | // A -> WORKTREE 10 | // 11 | // git diff --cached 12 | // HEAD -> INDEX 13 | // git diff --cached A 14 | // A -> INDEX 15 | // 16 | // git diff A..B 17 | // A -> B 18 | // git diff A...B 19 | // A...B -> B 20 | export function parseCommitish( 21 | commitish: string, 22 | cached = false, 23 | ): [Commitish, Commitish] { 24 | if (commitish.includes("...")) { 25 | return [commitish, commitish.split("...")[1]]; 26 | } else if (commitish.includes("..")) { 27 | const [a, b] = commitish.split("..", 2); 28 | return [a, b]; 29 | } else if (cached) { 30 | return [commitish, INDEX]; 31 | } else { 32 | return [commitish || INDEX, WORKTREE]; 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /denops/gin/command/diff/commitish_test.ts: -------------------------------------------------------------------------------- 1 | import { assertEquals } from "jsr:@std/assert@^1.0.0"; 2 | import { Commitish, INDEX, parseCommitish, WORKTREE } from "./commitish.ts"; 3 | 4 | Deno.test("parseCommitish", () => { 5 | const testcases: [[string, boolean], [Commitish, Commitish]][] = [ 6 | [["", false], [INDEX, WORKTREE]], 7 | [["A", false], ["A", WORKTREE]], 8 | [["", true], ["", INDEX]], 9 | [["A", true], ["A", INDEX]], 10 | [["A..B", false], ["A", "B"]], 11 | [["A...B", false], ["A...B", "B"]], 12 | ]; 13 | for (const [[commitish, cached], [lhs, rhs]] of testcases) { 14 | assertEquals([lhs, rhs], parseCommitish(commitish, cached)); 15 | } 16 | }); 17 | -------------------------------------------------------------------------------- /denops/gin/command/diff/edit.ts: -------------------------------------------------------------------------------- 1 | import type { Denops } from "jsr:@denops/std@^7.0.0"; 2 | import { unnullish } from "jsr:@lambdalisue/unnullish@^1.0.0"; 3 | import { ensure, is } from "jsr:@core/unknownutil@^4.0.0"; 4 | import * as batch from "jsr:@denops/std@^7.0.0/batch"; 5 | import * as mapping from "jsr:@denops/std@^7.0.0/mapping"; 6 | import * as option from "jsr:@denops/std@^7.0.0/option"; 7 | import * as vars from "jsr:@denops/std@^7.0.0/variable"; 8 | import { parse as parseBufname } from "jsr:@denops/std@^7.0.0/bufname"; 9 | import { 10 | builtinOpts, 11 | Flags, 12 | formatFlags, 13 | parseOpts, 14 | validateOpts, 15 | } from "jsr:@denops/std@^7.0.0/argument"; 16 | import * as buffer from "jsr:@denops/std@^7.0.0/buffer"; 17 | import { exec as execBuffer } from "../../command/buffer/edit.ts"; 18 | 19 | export async function edit( 20 | denops: Denops, 21 | bufnr: number, 22 | bufname: string, 23 | ): Promise { 24 | const cmdarg = await vars.v.get(denops, "cmdarg") as string; 25 | const [opts, _] = parseOpts(cmdarg.split(" ")); 26 | validateOpts(opts, builtinOpts); 27 | const { expr, params, fragment } = parseBufname(bufname); 28 | await exec(denops, bufnr, { 29 | processor: unnullish( 30 | params?.processor, 31 | (v) => 32 | ensure(v, is.String, { message: "processor must be string" }).split( 33 | " ", 34 | ), 35 | ), 36 | worktree: expr, 37 | commitish: unnullish( 38 | params?.commitish, 39 | (v) => ensure(v, is.String, { message: "commitish must be string" }), 40 | ), 41 | paths: unnullish(fragment, JSON.parse), 42 | flags: { 43 | ...params, 44 | processor: undefined, 45 | commitish: undefined, 46 | }, 47 | encoding: opts.enc ?? opts.encoding, 48 | fileformat: opts.ff ?? opts.fileformat, 49 | }); 50 | } 51 | 52 | export type ExecOptions = { 53 | processor?: string[]; 54 | worktree?: string; 55 | commitish?: string; 56 | paths?: string[]; 57 | flags?: Flags; 58 | encoding?: string; 59 | fileformat?: string; 60 | }; 61 | 62 | export async function exec( 63 | denops: Denops, 64 | bufnr: number, 65 | options: ExecOptions, 66 | ): Promise { 67 | const filenames = options.paths?.map((v) => v.replaceAll("\\", "/")); 68 | const args = [ 69 | "diff", 70 | ...formatFlags(options.flags ?? {}), 71 | ...(unnullish(options.commitish, (v) => [v]) ?? []), 72 | "--", 73 | ...(filenames ?? []), 74 | ]; 75 | await execBuffer(denops, bufnr, args, { 76 | processor: options.processor, 77 | worktree: options.worktree, 78 | encoding: options.encoding, 79 | fileformat: options.fileformat, 80 | }); 81 | await buffer.ensure(denops, bufnr, async () => { 82 | await batch.batch(denops, async (denops) => { 83 | await option.filetype.setLocal(denops, "gin-diff"); 84 | await option.bufhidden.setLocal(denops, "unload"); 85 | await option.buftype.setLocal(denops, "nofile"); 86 | await option.swapfile.setLocal(denops, false); 87 | await option.modifiable.setLocal(denops, false); 88 | await mapping.map( 89 | denops, 90 | "(gin-diffjump-old)", 91 | `call denops#request('gin', 'diff:jump:old', [])`, 92 | { 93 | buffer: true, 94 | noremap: true, 95 | }, 96 | ); 97 | await mapping.map( 98 | denops, 99 | "(gin-diffjump-new)", 100 | `call denops#request('gin', 'diff:jump:new', [])`, 101 | { 102 | buffer: true, 103 | noremap: true, 104 | }, 105 | ); 106 | await mapping.map( 107 | denops, 108 | "(gin-diffjump-smart)", 109 | `call denops#request('gin', 'diff:jump:smart', [])`, 110 | { 111 | buffer: true, 112 | noremap: true, 113 | }, 114 | ); 115 | }); 116 | }); 117 | } 118 | -------------------------------------------------------------------------------- /denops/gin/command/diff/jump_test.diff: -------------------------------------------------------------------------------- 1 | --- a/path/to/lao 2002-02-21 23:30:39.942229878 -0800 2 | +++ b/path/to/tzu 2002-02-21 23:30:50.442260588 -0800 3 | @@ -1,7 +1,6 @@ 4 | -The Way that can be told of is not the eternal Way; 5 | -The name that can be named is not the eternal name. 6 | The Nameless is the origin of Heaven and Earth; 7 | -The Named is the mother of all things. 8 | +The named is the mother of all things. 9 | + 10 | Therefore let there always be non-being, 11 | so we may see their subtlety, 12 | And let there always be being, 13 | @@ -9,3 +8,6 @@ 14 | The two are the same, 15 | But after they are produced, 16 | they have different names. 17 | +They both may be called deep and profound. 18 | +Deeper and more profound, 19 | +The door of all subtleties! 20 | -------------------------------------------------------------------------------- /denops/gin/command/diff/jump_test.ts: -------------------------------------------------------------------------------- 1 | import { assertEquals } from "jsr:@std/assert@^1.0.0"; 2 | import { findJumpNew, findJumpOld, Jump } from "./jump.ts"; 3 | 4 | const example = (await Deno.readTextFile( 5 | new URL("./jump_test.diff", import.meta.url), 6 | )).split("\n"); 7 | 8 | Deno.test("findOldJump", () => { 9 | const testcases: [number, Jump | undefined][] = [ 10 | [0, undefined], 11 | [1, undefined], 12 | [2, undefined], 13 | [3, { path: "a/path/to/lao", lnum: 1 }], 14 | [4, { path: "a/path/to/lao", lnum: 2 }], 15 | [5, { path: "a/path/to/lao", lnum: 3 }], 16 | [6, { path: "a/path/to/lao", lnum: 4 }], 17 | [7, { path: "a/path/to/lao", lnum: 4 }], 18 | [8, { path: "a/path/to/lao", lnum: 4 }], 19 | [9, { path: "a/path/to/lao", lnum: 5 }], 20 | [10, { path: "a/path/to/lao", lnum: 6 }], 21 | [11, { path: "a/path/to/lao", lnum: 7 }], 22 | [12, undefined], 23 | [13, { path: "a/path/to/lao", lnum: 9 }], 24 | [14, { path: "a/path/to/lao", lnum: 10 }], 25 | [15, { path: "a/path/to/lao", lnum: 11 }], 26 | [16, { path: "a/path/to/lao", lnum: 11 }], 27 | [17, { path: "a/path/to/lao", lnum: 11 }], 28 | [18, { path: "a/path/to/lao", lnum: 11 }], 29 | ]; 30 | for (const [idx, exp] of testcases) { 31 | assertEquals(exp, findJumpOld(idx, example)); 32 | } 33 | }); 34 | 35 | Deno.test("findNewJump", () => { 36 | const testcases: [number, Jump | undefined][] = [ 37 | [0, undefined], 38 | [1, undefined], 39 | [2, undefined], 40 | [3, { path: "b/path/to/tzu", lnum: 1 }], 41 | [4, { path: "b/path/to/tzu", lnum: 1 }], 42 | [5, { path: "b/path/to/tzu", lnum: 1 }], 43 | [6, { path: "b/path/to/tzu", lnum: 1 }], 44 | [7, { path: "b/path/to/tzu", lnum: 2 }], 45 | [8, { path: "b/path/to/tzu", lnum: 3 }], 46 | [9, { path: "b/path/to/tzu", lnum: 4 }], 47 | [10, { path: "b/path/to/tzu", lnum: 5 }], 48 | [11, { path: "b/path/to/tzu", lnum: 6 }], 49 | [12, undefined], 50 | [13, { path: "b/path/to/tzu", lnum: 8 }], 51 | [14, { path: "b/path/to/tzu", lnum: 9 }], 52 | [15, { path: "b/path/to/tzu", lnum: 10 }], 53 | [16, { path: "b/path/to/tzu", lnum: 11 }], 54 | [17, { path: "b/path/to/tzu", lnum: 12 }], 55 | [18, { path: "b/path/to/tzu", lnum: 13 }], 56 | ]; 57 | for (const [idx, exp] of testcases) { 58 | assertEquals(exp, findJumpNew(idx, example)); 59 | } 60 | }); 61 | -------------------------------------------------------------------------------- /denops/gin/command/diff/main.ts: -------------------------------------------------------------------------------- 1 | import type { Denops } from "jsr:@denops/std@^7.0.0"; 2 | import { as, assert, is } from "jsr:@core/unknownutil@^4.0.0"; 3 | import * as helper from "jsr:@denops/std@^7.0.0/helper"; 4 | import { 5 | builtinOpts, 6 | formatOpts, 7 | parse, 8 | validateOpts, 9 | } from "jsr:@denops/std@^7.0.0/argument"; 10 | import { fillCmdArgs, normCmdArgs, parseSilent } from "../../util/cmd.ts"; 11 | import { exec } from "./command.ts"; 12 | import { edit } from "./edit.ts"; 13 | import { read } from "./read.ts"; 14 | import { jumpNew, jumpOld, jumpSmart } from "./jump.ts"; 15 | 16 | export function main(denops: Denops): void { 17 | denops.dispatcher = { 18 | ...denops.dispatcher, 19 | "diff:command": (bang, mods, args) => { 20 | assert(bang, is.String, { name: "bang" }); 21 | assert(mods, is.String, { name: "mods" }); 22 | assert(args, is.ArrayOf(is.String), { name: "args" }); 23 | const silent = parseSilent(mods); 24 | return helper.ensureSilent(denops, silent, () => { 25 | return helper.friendlyCall( 26 | denops, 27 | () => command(denops, bang, mods, args), 28 | ); 29 | }); 30 | }, 31 | "diff:edit": (bufnr, bufname) => { 32 | assert(bufnr, is.Number, { name: "bufnr" }); 33 | assert(bufname, is.String, { name: "bufname" }); 34 | return helper.friendlyCall(denops, () => edit(denops, bufnr, bufname)); 35 | }, 36 | "diff:read": (bufnr, bufname) => { 37 | assert(bufnr, is.Number, { name: "bufnr" }); 38 | assert(bufname, is.String, { name: "bufname" }); 39 | return helper.friendlyCall(denops, () => read(denops, bufnr, bufname)); 40 | }, 41 | "diff:jump:new": (mods) => { 42 | assert(mods, as.Optional(is.String), { name: "mods" }); 43 | return helper.friendlyCall(denops, () => jumpNew(denops, mods ?? "")); 44 | }, 45 | "diff:jump:old": (mods) => { 46 | assert(mods, as.Optional(is.String), { name: "mods" }); 47 | return helper.friendlyCall(denops, () => jumpOld(denops, mods ?? "")); 48 | }, 49 | "diff:jump:smart": (mods) => { 50 | assert(mods, as.Optional(is.String), { name: "mods" }); 51 | return helper.friendlyCall(denops, () => jumpSmart(denops, mods ?? "")); 52 | }, 53 | }; 54 | } 55 | 56 | async function command( 57 | denops: Denops, 58 | bang: string, 59 | mods: string, 60 | args: string[], 61 | ): Promise { 62 | args = await fillCmdArgs(denops, args, "diff"); 63 | args = await normCmdArgs(denops, args); 64 | 65 | const [opts, flags, residue] = parse(args); 66 | validateOpts(opts, [ 67 | "processor", 68 | "worktree", 69 | "opener", 70 | ...builtinOpts, 71 | ]); 72 | const [commitish, paths] = parseResidue(residue); 73 | await exec(denops, { 74 | processor: opts.processor?.split(" "), 75 | worktree: opts.worktree, 76 | commitish, 77 | paths, 78 | flags, 79 | opener: opts.opener, 80 | cmdarg: formatOpts(opts, builtinOpts).join(" "), 81 | mods, 82 | bang: bang === "!", 83 | }); 84 | } 85 | 86 | function parseResidue(residue: string[]): [string | undefined, string[]] { 87 | const index = residue.indexOf("--"); 88 | const head = index === -1 ? residue : residue.slice(0, index); 89 | const tail = index === -1 ? [] : residue.slice(index + 1); 90 | // GinDiff [{options}] 91 | // GinDiff [{options}] {commitish} 92 | // GinDiff [{options}] -- {path}... 93 | // GinDiff [{options}] {commitish} -- {path}... 94 | switch (head.length) { 95 | case 0: 96 | return [undefined, tail]; 97 | case 1: 98 | return [head[0], tail]; 99 | default: 100 | throw new Error("Invalid number of arguments"); 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /denops/gin/command/diff/read.ts: -------------------------------------------------------------------------------- 1 | import type { Denops } from "jsr:@denops/std@^7.0.0"; 2 | import { unnullish } from "jsr:@lambdalisue/unnullish@^1.0.0"; 3 | import { ensure, is } from "jsr:@core/unknownutil@^4.0.0"; 4 | import * as vars from "jsr:@denops/std@^7.0.0/variable"; 5 | import { parse as parseBufname } from "jsr:@denops/std@^7.0.0/bufname"; 6 | import { 7 | builtinOpts, 8 | Flags, 9 | formatFlags, 10 | parseOpts, 11 | validateOpts, 12 | } from "jsr:@denops/std@^7.0.0/argument"; 13 | import { exec as execBuffer } from "../../command/buffer/read.ts"; 14 | 15 | export async function read( 16 | denops: Denops, 17 | bufnr: number, 18 | bufname: string, 19 | ): Promise { 20 | const cmdarg = await vars.v.get(denops, "cmdarg") as string; 21 | const [opts, _] = parseOpts(cmdarg.split(" ")); 22 | validateOpts(opts, builtinOpts); 23 | const { expr, params, fragment } = parseBufname(bufname); 24 | await exec(denops, bufnr, { 25 | processor: unnullish(opts.processor, (v) => v.split(" ")), 26 | worktree: expr, 27 | commitish: unnullish( 28 | params?.commitish, 29 | (v) => ensure(v, is.String, { message: "commitish must be string" }), 30 | ), 31 | paths: unnullish(fragment, JSON.parse), 32 | flags: { 33 | ...params, 34 | commitish: undefined, 35 | }, 36 | encoding: opts.enc ?? opts.encoding, 37 | fileformat: opts.ff ?? opts.fileformat, 38 | }); 39 | } 40 | 41 | export type ExecOptions = { 42 | processor?: string[]; 43 | worktree?: string; 44 | commitish?: string; 45 | paths?: string[]; 46 | flags?: Flags; 47 | encoding?: string; 48 | fileformat?: string; 49 | }; 50 | 51 | export async function exec( 52 | denops: Denops, 53 | bufnr: number, 54 | options: ExecOptions, 55 | ): Promise { 56 | const filenames = options.paths?.map((v) => v.replaceAll("\\", "/")); 57 | const args = [ 58 | "diff", 59 | ...formatFlags(options.flags ?? {}), 60 | ...unnullish(options.commitish, (v) => [v]) ?? [], 61 | "--", 62 | ...(filenames ?? []), 63 | ]; 64 | await execBuffer(denops, bufnr, args, { 65 | processor: options.processor, 66 | worktree: options.worktree, 67 | encoding: options.encoding, 68 | fileformat: options.fileformat, 69 | }); 70 | } 71 | -------------------------------------------------------------------------------- /denops/gin/command/edit/command.ts: -------------------------------------------------------------------------------- 1 | import type { Denops } from "jsr:@denops/std@^7.0.0"; 2 | import * as path from "jsr:@std/path@^1.0.0"; 3 | import * as buffer from "jsr:@denops/std@^7.0.0/buffer"; 4 | import * as option from "jsr:@denops/std@^7.0.0/option"; 5 | import { format as formatBufname } from "jsr:@denops/std@^7.0.0/bufname"; 6 | import { findWorktreeFromDenops } from "../../git/worktree.ts"; 7 | 8 | export type ExecOptions = { 9 | worktree?: string; 10 | commitish?: string; 11 | opener?: string; 12 | cmdarg?: string; 13 | mods?: string; 14 | bang?: boolean; 15 | }; 16 | 17 | export async function exec( 18 | denops: Denops, 19 | filename: string, 20 | options: ExecOptions = {}, 21 | ): Promise { 22 | const verbose = await option.verbose.get(denops); 23 | 24 | const worktree = await findWorktreeFromDenops(denops, { 25 | worktree: options.worktree, 26 | verbose: !!verbose, 27 | }); 28 | 29 | const relpath = path.isAbsolute(filename) 30 | ? path.relative(worktree, filename) 31 | : filename; 32 | 33 | const bufname = formatBufname({ 34 | scheme: "ginedit", 35 | expr: worktree, 36 | params: { 37 | commitish: options.commitish, 38 | }, 39 | fragment: relpath, 40 | }); 41 | return await buffer.open(denops, bufname.toString(), { 42 | opener: options.opener, 43 | cmdarg: options.cmdarg, 44 | mods: options.mods, 45 | bang: options.bang, 46 | }); 47 | } 48 | -------------------------------------------------------------------------------- /denops/gin/command/edit/edit.ts: -------------------------------------------------------------------------------- 1 | import type { Denops } from "jsr:@denops/std@^7.0.0"; 2 | import { ensure, is } from "jsr:@core/unknownutil@^4.0.0"; 3 | import { unnullish } from "jsr:@lambdalisue/unnullish@^1.0.0"; 4 | import * as autocmd from "jsr:@denops/std@^7.0.0/autocmd"; 5 | import * as batch from "jsr:@denops/std@^7.0.0/batch"; 6 | import * as buffer from "jsr:@denops/std@^7.0.0/buffer"; 7 | import * as option from "jsr:@denops/std@^7.0.0/option"; 8 | import * as vars from "jsr:@denops/std@^7.0.0/variable"; 9 | import { parseOpts, validateOpts } from "jsr:@denops/std@^7.0.0/argument"; 10 | import { parse as parseBufname } from "jsr:@denops/std@^7.0.0/bufname"; 11 | import { execute } from "../../git/executor.ts"; 12 | import { formatTreeish } from "./util.ts"; 13 | 14 | export async function edit( 15 | denops: Denops, 16 | bufnr: number, 17 | bufname: string, 18 | ): Promise { 19 | const cmdarg = (await vars.v.get(denops, "cmdarg")) as string; 20 | const [opts, _] = parseOpts(cmdarg.split(" ")); 21 | validateOpts(opts, ["enc", "encoding", "ff", "fileformat"]); 22 | const { scheme, expr, params, fragment } = parseBufname(bufname); 23 | if (!fragment) { 24 | throw new Error(`A buffer '${scheme}://' requires a fragment part`); 25 | } 26 | await exec(denops, bufnr, fragment, { 27 | worktree: expr, 28 | commitish: unnullish( 29 | params?.commitish, 30 | (v) => ensure(v, is.String, { message: "commitish must be string" }), 31 | ), 32 | encoding: opts.enc ?? opts.encoding, 33 | fileformat: opts.ff ?? opts.fileformat, 34 | }); 35 | } 36 | 37 | export type ExecOptions = { 38 | worktree?: string; 39 | commitish?: string; 40 | encoding?: string; 41 | fileformat?: string; 42 | }; 43 | 44 | export async function exec( 45 | denops: Denops, 46 | bufnr: number, 47 | relpath: string, 48 | options: ExecOptions, 49 | ): Promise { 50 | const filename = relpath.replaceAll("\\", "/"); 51 | const args = ["show", ...formatTreeish(options.commitish, filename)]; 52 | const { stdout } = await execute(denops, args, { 53 | worktree: options.worktree, 54 | throwOnError: true, 55 | stdoutIndicator: "null", 56 | }); 57 | const { content, fileformat, fileencoding } = await buffer.decode( 58 | denops, 59 | bufnr, 60 | stdout, 61 | { 62 | fileformat: options.fileformat, 63 | fileencoding: options.encoding, 64 | }, 65 | ); 66 | await buffer.replace(denops, bufnr, content, { 67 | fileformat, 68 | fileencoding, 69 | }); 70 | await buffer.concrete(denops, bufnr); 71 | await buffer.ensure(denops, bufnr, async () => { 72 | await batch.batch(denops, async (denops) => { 73 | await denops.cmd("filetype detect"); 74 | await option.swapfile.setLocal(denops, false); 75 | await option.bufhidden.setLocal(denops, "unload"); 76 | if (options.commitish) { 77 | await option.buftype.setLocal(denops, "nowrite"); 78 | await option.modifiable.setLocal(denops, false); 79 | } else { 80 | await option.buftype.setLocal(denops, "acwrite"); 81 | await autocmd.group( 82 | denops, 83 | "gin_command_edit_edit_internal", 84 | (helper) => { 85 | helper.remove("*", ""); 86 | helper.define( 87 | "BufWriteCmd", 88 | "", 89 | `call denops#request('gin', 'edit:write', [bufnr(), expand('')])`, 90 | { 91 | nested: true, 92 | }, 93 | ); 94 | }, 95 | ); 96 | } 97 | }); 98 | }); 99 | } 100 | -------------------------------------------------------------------------------- /denops/gin/command/edit/main.ts: -------------------------------------------------------------------------------- 1 | import type { Denops } from "jsr:@denops/std@^7.0.0"; 2 | import { assert, is } from "jsr:@core/unknownutil@^4.0.0"; 3 | import * as helper from "jsr:@denops/std@^7.0.0/helper"; 4 | import { 5 | builtinOpts, 6 | formatOpts, 7 | parseOpts, 8 | validateOpts, 9 | } from "jsr:@denops/std@^7.0.0/argument"; 10 | import { fillCmdArgs, normCmdArgs, parseSilent } from "../../util/cmd.ts"; 11 | import { exec } from "./command.ts"; 12 | import { edit } from "./edit.ts"; 13 | import { read } from "./read.ts"; 14 | import { write } from "./write.ts"; 15 | 16 | export function main(denops: Denops): void { 17 | denops.dispatcher = { 18 | ...denops.dispatcher, 19 | "edit:command": (bang, mods, args) => { 20 | assert(bang, is.String, { name: "bang" }); 21 | assert(mods, is.String, { name: "mods" }); 22 | assert(args, is.ArrayOf(is.String), { name: "args" }); 23 | const silent = parseSilent(mods); 24 | return helper.ensureSilent(denops, silent, () => { 25 | return helper.friendlyCall( 26 | denops, 27 | () => command(denops, bang, mods, args), 28 | ); 29 | }); 30 | }, 31 | "edit:edit": (bufnr, bufname) => { 32 | assert(bufnr, is.Number, { name: "bufnr" }); 33 | assert(bufname, is.String, { name: "bufname" }); 34 | return helper.friendlyCall(denops, () => edit(denops, bufnr, bufname)); 35 | }, 36 | "edit:read": (bufnr, bufname) => { 37 | assert(bufnr, is.Number, { name: "bufnr" }); 38 | assert(bufname, is.String, { name: "bufname" }); 39 | return helper.friendlyCall(denops, () => read(denops, bufnr, bufname)); 40 | }, 41 | "edit:write": (bufnr, bufname) => { 42 | assert(bufnr, is.Number, { name: "bufnr" }); 43 | assert(bufname, is.String, { name: "bufname" }); 44 | return helper.friendlyCall(denops, () => write(denops, bufnr, bufname)); 45 | }, 46 | }; 47 | } 48 | 49 | async function command( 50 | denops: Denops, 51 | bang: string, 52 | mods: string, 53 | args: string[], 54 | ): Promise { 55 | args = await fillCmdArgs(denops, args, "edit"); 56 | args = await normCmdArgs(denops, args); 57 | 58 | const [opts, residue] = parseOpts(args); 59 | validateOpts(opts, [ 60 | "worktree", 61 | "opener", 62 | ...builtinOpts, 63 | ]); 64 | const [commitish, filename] = parseResidue(residue); 65 | await exec(denops, filename, { 66 | worktree: opts.worktree, 67 | commitish, 68 | opener: opts.opener, 69 | cmdarg: formatOpts(opts, builtinOpts).join(" "), 70 | mods, 71 | bang: bang === "!", 72 | }); 73 | } 74 | 75 | function parseResidue( 76 | residue: string[], 77 | ): [string | undefined, string] { 78 | // GinEdit [{options}] {path} 79 | // GinEdit [{options}] {commitish} {path} 80 | switch (residue.length) { 81 | case 1: 82 | return [undefined, residue[0]]; 83 | case 2: 84 | return [residue[0], residue[1]]; 85 | default: 86 | throw new Error("Invalid number of arguments"); 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /denops/gin/command/edit/read.ts: -------------------------------------------------------------------------------- 1 | import type { Denops } from "jsr:@denops/std@^7.0.0"; 2 | import { ensure, is } from "jsr:@core/unknownutil@^4.0.0"; 3 | import { unnullish } from "jsr:@lambdalisue/unnullish@^1.0.0"; 4 | import * as buffer from "jsr:@denops/std@^7.0.0/buffer"; 5 | import * as vars from "jsr:@denops/std@^7.0.0/variable"; 6 | import { parseOpts, validateOpts } from "jsr:@denops/std@^7.0.0/argument"; 7 | import { parse as parseBufname } from "jsr:@denops/std@^7.0.0/bufname"; 8 | import { execute } from "../../git/executor.ts"; 9 | import { formatTreeish } from "./util.ts"; 10 | 11 | export async function read( 12 | denops: Denops, 13 | bufnr: number, 14 | bufname: string, 15 | ): Promise { 16 | const cmdarg = ensure(await vars.v.get(denops, "cmdarg"), is.String); 17 | const [opts, _] = parseOpts(cmdarg.split(" ")); 18 | validateOpts(opts, ["enc", "encoding", "ff", "fileformat"]); 19 | const { scheme, expr, params, fragment } = parseBufname(bufname); 20 | if (!fragment) { 21 | throw new Error(`A buffer '${scheme}://' requires a fragment part`); 22 | } 23 | await exec(denops, bufnr, fragment, { 24 | worktree: expr, 25 | commitish: unnullish( 26 | params?.commitish, 27 | (v) => ensure(v, is.String, { message: "commitish must be string" }), 28 | ), 29 | encoding: opts.enc ?? opts.encoding, 30 | fileformat: opts.ff ?? opts.fileformat, 31 | }); 32 | } 33 | 34 | export type ExecOptions = { 35 | worktree?: string; 36 | commitish?: string; 37 | lnum?: number; 38 | encoding?: string; 39 | fileformat?: string; 40 | }; 41 | 42 | export async function exec( 43 | denops: Denops, 44 | bufnr: number, 45 | relpath: string, 46 | options: ExecOptions, 47 | ): Promise { 48 | const filename = relpath.replaceAll("\\", "/"); 49 | const args = ["show", ...formatTreeish(options.commitish, filename)]; 50 | const { stdout } = await execute( 51 | denops, 52 | args, 53 | { 54 | worktree: options.worktree, 55 | throwOnError: true, 56 | stdoutIndicator: "null", 57 | }, 58 | ); 59 | const { content } = await buffer.decode(denops, bufnr, stdout, { 60 | fileformat: options.fileformat, 61 | fileencoding: options.encoding, 62 | }); 63 | await buffer.append(denops, bufnr, content, { 64 | lnum: options.lnum, 65 | }); 66 | } 67 | -------------------------------------------------------------------------------- /denops/gin/command/edit/util.ts: -------------------------------------------------------------------------------- 1 | import { assert, is } from "jsr:@core/unknownutil@^4.0.0"; 2 | 3 | export function formatTreeish( 4 | commitish: string | string[] | undefined, 5 | relpath: string, 6 | ): [] | [string] { 7 | if (commitish) { 8 | assert(commitish, is.String, { name: "commitish" }); 9 | return [`${commitish}:${relpath}`]; 10 | } else { 11 | return [`:${relpath}`]; 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /denops/gin/command/edit/write.ts: -------------------------------------------------------------------------------- 1 | import type { Denops } from "jsr:@denops/std@^7.0.0"; 2 | import * as path from "jsr:@std/path@^1.0.0"; 3 | import * as fs from "jsr:@std/fs@^1.0.0"; 4 | import * as batch from "jsr:@denops/std@^7.0.0/batch"; 5 | import * as fn from "jsr:@denops/std@^7.0.0/function"; 6 | import * as option from "jsr:@denops/std@^7.0.0/option"; 7 | import { parse as parseBufname } from "jsr:@denops/std@^7.0.0/bufname"; 8 | import { findWorktreeFromDenops } from "../../git/worktree.ts"; 9 | import { exec as execBare } from "../../command/bare/command.ts"; 10 | 11 | export async function write( 12 | denops: Denops, 13 | bufnr: number, 14 | bufname: string, 15 | ): Promise { 16 | const { scheme, expr, fragment } = parseBufname(bufname); 17 | if (!fragment) { 18 | throw new Error(`A buffer '${scheme}://' requires a fragment part`); 19 | } 20 | await exec(denops, bufnr, fragment, { 21 | worktree: expr, 22 | }); 23 | } 24 | 25 | export type ExecOptions = { 26 | worktree?: string; 27 | }; 28 | 29 | export async function exec( 30 | denops: Denops, 31 | bufnr: number, 32 | relpath: string, 33 | options: ExecOptions, 34 | ): Promise { 35 | const [verbose, fileencoding, fileformat, content] = await batch.collect( 36 | denops, 37 | (denops) => [ 38 | option.verbose.get(denops), 39 | option.fileencoding.getBuffer(denops, bufnr), 40 | option.fileformat.getBuffer(denops, bufnr), 41 | fn.getbufline(denops, bufnr, 1, "$"), 42 | ], 43 | ); 44 | 45 | const worktree = await findWorktreeFromDenops(denops, { 46 | worktree: options.worktree, 47 | verbose: !!verbose, 48 | }); 49 | 50 | const original = path.join(worktree, relpath); 51 | let restore: () => Promise; 52 | const f = await Deno.makeTempFile({ 53 | dir: path.dirname(original), 54 | }); 55 | try { 56 | await Deno.rename(original, f); 57 | restore = () => Deno.rename(f, original); 58 | } catch (e) { 59 | if (!(e instanceof Deno.errors.NotFound)) { 60 | throw e; 61 | } 62 | await Deno.remove(f); 63 | restore = () => Deno.remove(original); 64 | } 65 | try { 66 | await fs.copy(f, original); 67 | await Deno.writeTextFile(original, `${content.join("\n")}\n`); 68 | await fn.setbufvar(denops, bufnr, "&modified", 0); 69 | await execBare(denops, [ 70 | "add", 71 | "--force", 72 | "--", 73 | relpath, 74 | ], { 75 | worktree, 76 | encoding: fileencoding, 77 | fileformat, 78 | }); 79 | } finally { 80 | await restore(); 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /denops/gin/command/log/command.ts: -------------------------------------------------------------------------------- 1 | import type { Denops } from "jsr:@denops/std@^7.0.0"; 2 | import * as path from "jsr:@std/path@^1.0.0"; 3 | import { unnullish } from "jsr:@lambdalisue/unnullish@^1.0.0"; 4 | import * as buffer from "jsr:@denops/std@^7.0.0/buffer"; 5 | import * as option from "jsr:@denops/std@^7.0.0/option"; 6 | import { format as formatBufname } from "jsr:@denops/std@^7.0.0/bufname"; 7 | import { Flags } from "jsr:@denops/std@^7.0.0/argument"; 8 | import { findWorktreeFromDenops } from "../../git/worktree.ts"; 9 | 10 | export type ExecOptions = { 11 | worktree?: string; 12 | commitish?: string; 13 | paths?: string[]; 14 | flags?: Flags; 15 | opener?: string; 16 | emojify?: boolean; 17 | cmdarg?: string; 18 | mods?: string; 19 | bang?: boolean; 20 | }; 21 | 22 | export async function exec( 23 | denops: Denops, 24 | options: ExecOptions = {}, 25 | ): Promise { 26 | const verbose = await option.verbose.get(denops); 27 | 28 | const worktree = await findWorktreeFromDenops(denops, { 29 | worktree: options.worktree, 30 | verbose: !!verbose, 31 | }); 32 | 33 | const paths = options.paths?.map((p) => 34 | path.isAbsolute(p) ? path.relative(worktree, p) : p 35 | ); 36 | 37 | const bufname = formatBufname({ 38 | scheme: "ginlog", 39 | expr: worktree, 40 | params: { 41 | ...options.flags ?? {}, 42 | commitish: options.commitish, 43 | emojify: unnullish(options.emojify, (v) => v ? "" : undefined), 44 | }, 45 | fragment: unnullish(paths, JSON.stringify), 46 | }); 47 | return await buffer.open(denops, bufname, { 48 | opener: options.opener, 49 | cmdarg: options.cmdarg, 50 | mods: options.mods, 51 | bang: options.bang, 52 | }); 53 | } 54 | -------------------------------------------------------------------------------- /denops/gin/command/log/main.ts: -------------------------------------------------------------------------------- 1 | import type { Denops } from "jsr:@denops/std@^7.0.0"; 2 | import { assert, is } from "jsr:@core/unknownutil@^4.0.0"; 3 | import { unnullish } from "jsr:@lambdalisue/unnullish@^1.0.0"; 4 | import * as helper from "jsr:@denops/std@^7.0.0/helper"; 5 | import { 6 | builtinOpts, 7 | formatOpts, 8 | parse, 9 | validateOpts, 10 | } from "jsr:@denops/std@^7.0.0/argument"; 11 | import { fillCmdArgs, normCmdArgs, parseSilent } from "../../util/cmd.ts"; 12 | import { exec } from "./command.ts"; 13 | import { edit } from "./edit.ts"; 14 | import { read } from "./read.ts"; 15 | 16 | export function main(denops: Denops): void { 17 | denops.dispatcher = { 18 | ...denops.dispatcher, 19 | "log:command": (bang, mods, args) => { 20 | assert(bang, is.String, { name: "bang" }); 21 | assert(mods, is.String, { name: "mods" }); 22 | assert(args, is.ArrayOf(is.String), { name: "args" }); 23 | const silent = parseSilent(mods); 24 | return helper.ensureSilent(denops, silent, () => { 25 | return helper.friendlyCall( 26 | denops, 27 | () => command(denops, bang, mods, args), 28 | ); 29 | }); 30 | }, 31 | "log:edit": (bufnr, bufname) => { 32 | assert(bufnr, is.Number, { name: "bufnr" }); 33 | assert(bufname, is.String, { name: "bufname" }); 34 | return helper.friendlyCall(denops, () => edit(denops, bufnr, bufname)); 35 | }, 36 | "log:read": (bufnr, bufname) => { 37 | assert(bufnr, is.Number, { name: "bufnr" }); 38 | assert(bufname, is.String, { name: "bufname" }); 39 | return helper.friendlyCall(denops, () => read(denops, bufnr, bufname)); 40 | }, 41 | }; 42 | } 43 | 44 | async function command( 45 | denops: Denops, 46 | bang: string, 47 | mods: string, 48 | args: string[], 49 | ): Promise { 50 | args = await fillCmdArgs(denops, args, "log"); 51 | args = await normCmdArgs(denops, args); 52 | 53 | const [opts, flags, residue] = parse(args); 54 | validateOpts(opts, [ 55 | "worktree", 56 | "opener", 57 | "emojify", 58 | ...builtinOpts, 59 | ]); 60 | 61 | const [commitish, paths] = parseResidue(residue); 62 | await exec(denops, { 63 | worktree: opts.worktree, 64 | commitish, 65 | paths, 66 | flags, 67 | opener: opts.opener, 68 | emojify: unnullish(opts.emojify, () => true), 69 | cmdarg: formatOpts(opts, builtinOpts).join(" "), 70 | mods, 71 | bang: bang === "!", 72 | }); 73 | } 74 | 75 | function parseResidue(residue: string[]): [string | undefined, string[]] { 76 | const index = residue.indexOf("--"); 77 | const head = index === -1 ? residue : residue.slice(0, index); 78 | const tail = index === -1 ? [] : residue.slice(index + 1); 79 | // GinLog [{options}] 80 | // GinLog [{options}] {commitish} 81 | // GinLog [{options}] -- {pathspec}... 82 | // GinLog [{options}] {commitish} -- {pathspec}... 83 | switch (head.length) { 84 | case 0: 85 | return [undefined, tail]; 86 | case 1: 87 | return [head[0], tail]; 88 | default: 89 | throw new Error("Invalid number of arguments"); 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /denops/gin/command/log/parser.ts: -------------------------------------------------------------------------------- 1 | const defaultPattern = 2 | /^(?:[ *\|\\\/+\-=<>]*|[ *\|\\\/+\-=<>]*commit (?:[+\-=<>] )?)?([a-fA-F0-9]+)\b/; 3 | 4 | export interface GitLogResult { 5 | entries: Entry[]; 6 | } 7 | 8 | export type Entry = { 9 | commit: string; 10 | }; 11 | 12 | export function parse(content: string[], pattern?: RegExp): GitLogResult { 13 | const entries: Entry[] = content.filter((v) => v).flatMap((record) => { 14 | const m = record.match(pattern ?? defaultPattern); 15 | if (m) { 16 | return [{ 17 | commit: m[1], 18 | }]; 19 | } 20 | return []; 21 | }); 22 | return { entries }; 23 | } 24 | -------------------------------------------------------------------------------- /denops/gin/command/log/read.ts: -------------------------------------------------------------------------------- 1 | import type { Denops } from "jsr:@denops/std@^7.0.0"; 2 | import { unnullish } from "jsr:@lambdalisue/unnullish@^1.0.0"; 3 | import { ensure, is } from "jsr:@core/unknownutil@^4.0.0"; 4 | import * as vars from "jsr:@denops/std@^7.0.0/variable"; 5 | import { parse as parseBufname } from "jsr:@denops/std@^7.0.0/bufname"; 6 | import { 7 | builtinOpts, 8 | Flags, 9 | formatFlags, 10 | parseOpts, 11 | validateOpts, 12 | } from "jsr:@denops/std@^7.0.0/argument"; 13 | import { exec as execBuffer } from "../../command/buffer/read.ts"; 14 | 15 | export async function read( 16 | denops: Denops, 17 | bufnr: number, 18 | bufname: string, 19 | ): Promise { 20 | const cmdarg = ensure(await vars.v.get(denops, "cmdarg"), is.String); 21 | const [opts, _] = parseOpts(cmdarg.split(" ")); 22 | validateOpts(opts, builtinOpts); 23 | const { expr, params, fragment } = parseBufname(bufname); 24 | await exec(denops, bufnr, { 25 | worktree: expr, 26 | commitish: unnullish( 27 | params?.commitish, 28 | (v) => ensure(v, is.String, { message: "commitish must be string" }), 29 | ), 30 | paths: unnullish(fragment, JSON.parse), 31 | flags: { 32 | ...params, 33 | commitish: undefined, 34 | }, 35 | encoding: opts.enc ?? opts.encoding, 36 | fileformat: opts.ff ?? opts.fileformat, 37 | emojify: "emojify" in (params ?? {}), 38 | }); 39 | } 40 | 41 | export type ExecOptions = { 42 | worktree?: string; 43 | commitish?: string; 44 | paths?: string[]; 45 | flags?: Flags; 46 | encoding?: string; 47 | fileformat?: string; 48 | emojify?: boolean; 49 | }; 50 | 51 | export async function exec( 52 | denops: Denops, 53 | bufnr: number, 54 | options: ExecOptions, 55 | ): Promise { 56 | const filenames = options.paths?.map((v) => v.replaceAll("\\", "/")); 57 | const args = [ 58 | "log", 59 | ...formatFlags(options.flags ?? {}), 60 | ...unnullish(options.commitish, (v) => [v]) ?? [], 61 | "--", 62 | ...(filenames ?? []), 63 | ]; 64 | await execBuffer(denops, bufnr, args, { 65 | worktree: options.worktree, 66 | encoding: options.encoding, 67 | fileformat: options.fileformat, 68 | emojify: options.emojify, 69 | }); 70 | } 71 | -------------------------------------------------------------------------------- /denops/gin/command/patch/main.ts: -------------------------------------------------------------------------------- 1 | import type { Denops } from "jsr:@denops/std@^7.0.0"; 2 | import { assert, is } from "jsr:@core/unknownutil@^4.0.0"; 3 | import * as helper from "jsr:@denops/std@^7.0.0/helper"; 4 | import { 5 | builtinOpts, 6 | formatOpts, 7 | parseOpts, 8 | validateOpts, 9 | } from "jsr:@denops/std@^7.0.0/argument"; 10 | import { fillCmdArgs, normCmdArgs, parseSilent } from "../../util/cmd.ts"; 11 | import { ensurePath } from "../../util/ensure_path.ts"; 12 | import { exec } from "./command.ts"; 13 | 14 | export function main(denops: Denops): void { 15 | denops.dispatcher = { 16 | ...denops.dispatcher, 17 | "patch:command": (bang, mods, args) => { 18 | assert(bang, is.String, { name: "bang" }); 19 | assert(mods, is.String, { name: "mods" }); 20 | assert(args, is.ArrayOf(is.String), { name: "args" }); 21 | const silent = parseSilent(mods); 22 | return helper.ensureSilent(denops, silent, () => { 23 | return helper.friendlyCall( 24 | denops, 25 | () => command(denops, bang, mods, args), 26 | ); 27 | }); 28 | }, 29 | }; 30 | } 31 | 32 | async function command( 33 | denops: Denops, 34 | bang: string, 35 | mods: string, 36 | args: string[], 37 | ): Promise { 38 | args = await fillCmdArgs(denops, args, "patch"); 39 | args = await normCmdArgs(denops, args); 40 | 41 | const [opts, residue] = parseOpts(args); 42 | validateOpts(opts, [ 43 | "worktree", 44 | "opener", 45 | "no-head", 46 | "no-worktree", 47 | ...builtinOpts, 48 | ]); 49 | 50 | const [rawpath] = parseResidue(residue); 51 | await exec(denops, await ensurePath(denops, rawpath), { 52 | worktree: opts.worktree, 53 | noHead: "no-head" in opts, 54 | noWorktree: "no-worktree" in opts, 55 | opener: opts.opener, 56 | cmdarg: formatOpts(opts, builtinOpts).join(" "), 57 | mods, 58 | bang: bang === "!", 59 | }); 60 | } 61 | 62 | function parseResidue( 63 | residue: string[], 64 | ): [string | undefined] { 65 | switch (residue.length) { 66 | // GinPatch [{options}] 67 | case 0: 68 | return [undefined]; 69 | // GinPatch [{options}] {path} 70 | case 1: 71 | return [residue[0]]; 72 | default: 73 | throw new Error("Invalid number of arguments"); 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /denops/gin/command/status/command.ts: -------------------------------------------------------------------------------- 1 | import type { Denops } from "jsr:@denops/std@^7.0.0"; 2 | import { unnullish } from "jsr:@lambdalisue/unnullish@^1.0.0"; 3 | import * as buffer from "jsr:@denops/std@^7.0.0/buffer"; 4 | import * as option from "jsr:@denops/std@^7.0.0/option"; 5 | import { format as formatBufname } from "jsr:@denops/std@^7.0.0/bufname"; 6 | import { Flags } from "jsr:@denops/std@^7.0.0/argument"; 7 | import { findWorktreeFromDenops } from "../../git/worktree.ts"; 8 | 9 | export type ExecOptions = { 10 | worktree?: string; 11 | pathspecs?: string[]; 12 | flags?: Flags; 13 | opener?: string; 14 | cmdarg?: string; 15 | mods?: string; 16 | bang?: boolean; 17 | }; 18 | 19 | export async function exec( 20 | denops: Denops, 21 | options: ExecOptions = {}, 22 | ): Promise { 23 | const verbose = await option.verbose.get(denops); 24 | 25 | const worktree = await findWorktreeFromDenops(denops, { 26 | worktree: options.worktree, 27 | verbose: !!verbose, 28 | }); 29 | 30 | const bufname = formatBufname({ 31 | scheme: "ginstatus", 32 | expr: worktree, 33 | params: { 34 | "untracked-files": "", 35 | ...options.flags ?? {}, 36 | }, 37 | fragment: unnullish(options.pathspecs, (v) => `${JSON.stringify(v)}$`), 38 | }); 39 | return await buffer.open(denops, bufname, { 40 | opener: options.opener, 41 | cmdarg: options.cmdarg, 42 | mods: options.mods, 43 | bang: options.bang, 44 | }); 45 | } 46 | -------------------------------------------------------------------------------- /denops/gin/command/status/main.ts: -------------------------------------------------------------------------------- 1 | import type { Denops } from "jsr:@denops/std@^7.0.0"; 2 | import { assert, is } from "jsr:@core/unknownutil@^4.0.0"; 3 | import * as helper from "jsr:@denops/std@^7.0.0/helper"; 4 | import { 5 | builtinOpts, 6 | formatOpts, 7 | parse, 8 | validateFlags, 9 | validateOpts, 10 | } from "jsr:@denops/std@^7.0.0/argument"; 11 | import { fillCmdArgs, normCmdArgs, parseSilent } from "../../util/cmd.ts"; 12 | import { exec } from "./command.ts"; 13 | import { edit } from "./edit.ts"; 14 | 15 | export function main(denops: Denops): void { 16 | denops.dispatcher = { 17 | ...denops.dispatcher, 18 | "status:command": (bang, mods, args) => { 19 | assert(bang, is.String, { name: "bang" }); 20 | assert(mods, is.String, { name: "mods" }); 21 | assert(args, is.ArrayOf(is.String), { name: "args" }); 22 | const silent = parseSilent(mods); 23 | return helper.ensureSilent(denops, silent, () => { 24 | return helper.friendlyCall( 25 | denops, 26 | () => command(denops, bang, mods, args), 27 | ); 28 | }); 29 | }, 30 | "status:edit": (bufnr, bufname) => { 31 | assert(bufnr, is.Number, { name: "bufnr" }); 32 | assert(bufname, is.String, { name: "bufname" }); 33 | return helper.friendlyCall(denops, () => edit(denops, bufnr, bufname)); 34 | }, 35 | }; 36 | } 37 | 38 | const allowedFlags = [ 39 | "u", 40 | "untracked-files", 41 | "ignore-submodules", 42 | "ignored", 43 | "renames", 44 | "no-renames", 45 | "find-renames", 46 | ]; 47 | 48 | async function command( 49 | denops: Denops, 50 | bang: string, 51 | mods: string, 52 | args: string[], 53 | ): Promise { 54 | args = await fillCmdArgs(denops, args, "status"); 55 | args = await normCmdArgs(denops, args); 56 | 57 | const [opts, flags, residue] = parse(args); 58 | validateOpts(opts, [ 59 | "worktree", 60 | "opener", 61 | ...builtinOpts, 62 | ]); 63 | validateFlags(flags, allowedFlags); 64 | await exec(denops, { 65 | worktree: opts.worktree, 66 | pathspecs: residue, 67 | flags, 68 | opener: opts.opener, 69 | cmdarg: formatOpts(opts, builtinOpts).join(" "), 70 | mods, 71 | bang: bang === "!", 72 | }); 73 | } 74 | -------------------------------------------------------------------------------- /denops/gin/command/status/parser.ts: -------------------------------------------------------------------------------- 1 | const entryWithOrigPathPattern = 2 | /^([^#]{2}) (?:"(.*?)"|(.*)) -> (?:"(.*?)"|(.*))$/; 3 | const entryWithoutOrigPathPattern = /^([^#]{2}) (?:"(.*?)"|(.*))$/; 4 | 5 | export interface GitStatusResult { 6 | entries: Entry[]; 7 | } 8 | 9 | export type Entry = { 10 | XY: string; 11 | path: string; 12 | origPath?: string; 13 | }; 14 | 15 | export function parse(content: string[]): GitStatusResult { 16 | const entries: Entry[] = content.filter((v) => v).flatMap((record) => { 17 | const m1 = record.match(entryWithOrigPathPattern); 18 | if (m1) { 19 | return [{ 20 | XY: m1[1].replace(" ", "."), 21 | path: m1[4] ?? m1[5], 22 | origPath: m1[2] ?? m1[3], 23 | }]; 24 | } 25 | const m2 = record.match(entryWithoutOrigPathPattern); 26 | if (m2) { 27 | return [{ 28 | XY: m2[1].replace(" ", "."), 29 | path: m2[2] ?? m2[3], 30 | }]; 31 | } 32 | return []; 33 | }); 34 | return { entries }; 35 | } 36 | -------------------------------------------------------------------------------- /denops/gin/command/status/parser_test.ts: -------------------------------------------------------------------------------- 1 | import { assertEquals } from "jsr:@std/assert@^1.0.0"; 2 | import { parse } from "./parser.ts"; 3 | 4 | Deno.test(`parse`, () => { 5 | const content = [ 6 | "## main...origin/main +10 -5", 7 | " M README.md", 8 | " M README -> README.md", 9 | "MM README.md", 10 | "!! README.md", 11 | "?? README.md", 12 | ]; 13 | assertEquals(parse(content), { 14 | entries: [ 15 | { 16 | XY: ".M", 17 | path: "README.md", 18 | }, 19 | { 20 | XY: ".M", 21 | origPath: "README", 22 | path: "README.md", 23 | }, 24 | { 25 | XY: "MM", 26 | path: "README.md", 27 | }, 28 | { 29 | XY: "!!", 30 | path: "README.md", 31 | }, 32 | { 33 | XY: "??", 34 | path: "README.md", 35 | }, 36 | ], 37 | }); 38 | }); 39 | 40 | Deno.test(`parse with spaces`, () => { 41 | const content = [ 42 | "## main...origin/main +10 -5", 43 | ' M "R E A D M E.md"', 44 | ' M "R E A D M E" -> "R E A D M E.md"', 45 | 'MM "R E A D M E.md"', 46 | '!! "R E A D M E.md"', 47 | '?? "R E A D M E.md"', 48 | ]; 49 | assertEquals(parse(content), { 50 | entries: [ 51 | { 52 | XY: ".M", 53 | path: "R E A D M E.md", 54 | }, 55 | { 56 | XY: ".M", 57 | origPath: "R E A D M E", 58 | path: "R E A D M E.md", 59 | }, 60 | { 61 | XY: "MM", 62 | path: "R E A D M E.md", 63 | }, 64 | { 65 | XY: "!!", 66 | path: "R E A D M E.md", 67 | }, 68 | { 69 | XY: "??", 70 | path: "R E A D M E.md", 71 | }, 72 | ], 73 | }); 74 | }); 75 | -------------------------------------------------------------------------------- /denops/gin/component/branch.ts: -------------------------------------------------------------------------------- 1 | import type { Denops } from "jsr:@denops/std@^7.0.0"; 2 | import { Cache } from "jsr:@lambdalisue/ttl-cache@^1.0.0"; 3 | import { decodeUtf8 } from "../util/text.ts"; 4 | import { findWorktreeFromDenops } from "../git/worktree.ts"; 5 | import { execute } from "../git/process.ts"; 6 | 7 | type Data = [string, string]; 8 | 9 | const cache = new Cache(100); 10 | 11 | async function getData( 12 | denops: Denops, 13 | ): Promise { 14 | return cache.get("data") ?? await (async () => { 15 | const worktree = await findWorktreeFromDenops(denops); 16 | const result = await getBranches(worktree); 17 | cache.set("data", result); 18 | return result; 19 | })(); 20 | } 21 | 22 | async function getBranches( 23 | cwd: string, 24 | ): Promise { 25 | let stdout: Uint8Array; 26 | try { 27 | stdout = await execute([ 28 | "rev-parse", 29 | "--abbrev-ref", 30 | "--symbolic-full-name", 31 | "HEAD", 32 | "@{u}", 33 | ], { 34 | noOptionalLocks: true, 35 | cwd, 36 | }); 37 | } catch { 38 | stdout = await execute([ 39 | "branch", 40 | "--show-current", 41 | ], { 42 | noOptionalLocks: true, 43 | cwd, 44 | }); 45 | } 46 | const [branch, upstream] = decodeUtf8(stdout).split("\n"); 47 | return [branch, upstream]; 48 | } 49 | 50 | export function main(denops: Denops): void { 51 | denops.dispatcher = { 52 | ...denops.dispatcher, 53 | "component:branch:ascii": async () => { 54 | const [branch, upstream] = await getData(denops); 55 | if (branch && upstream) { 56 | return `${branch} -> ${upstream}`; 57 | } 58 | return branch; 59 | }, 60 | "component:branch:unicode": async () => { 61 | const [branch, upstream] = await getData(denops); 62 | if (branch && upstream) { 63 | return `${branch} → ${upstream}`; 64 | } 65 | return branch; 66 | }, 67 | }; 68 | } 69 | -------------------------------------------------------------------------------- /denops/gin/component/traffic.ts: -------------------------------------------------------------------------------- 1 | import type { Denops } from "jsr:@denops/std@^7.0.0"; 2 | import { Cache } from "jsr:@lambdalisue/ttl-cache@^1.0.0"; 3 | import { decodeUtf8 } from "../util/text.ts"; 4 | import { findWorktreeFromDenops } from "../git/worktree.ts"; 5 | import { execute } from "../git/process.ts"; 6 | 7 | type Data = [number, number]; 8 | 9 | const cache = new Cache(100); 10 | 11 | async function getData( 12 | denops: Denops, 13 | ): Promise { 14 | return cache.get("data") ?? await (async () => { 15 | const worktree = await findWorktreeFromDenops(denops); 16 | const result = await Promise.all([ 17 | getAhead(worktree), 18 | getBehind(worktree), 19 | ]); 20 | cache.set("data", result); 21 | return result; 22 | })(); 23 | } 24 | 25 | async function getAhead(cwd: string): Promise { 26 | const stdout = await execute([ 27 | "rev-list", 28 | "--count", 29 | "@{u}..HEAD", 30 | ], { 31 | noOptionalLocks: true, 32 | cwd, 33 | }); 34 | return Number(decodeUtf8(stdout)); 35 | } 36 | 37 | async function getBehind(cwd: string): Promise { 38 | const stdout = await execute([ 39 | "rev-list", 40 | "--count", 41 | "HEAD..@{u}", 42 | ], { 43 | noOptionalLocks: true, 44 | cwd, 45 | }); 46 | return Number(decodeUtf8(stdout)); 47 | } 48 | 49 | export function main(denops: Denops): void { 50 | denops.dispatcher = { 51 | ...denops.dispatcher, 52 | "component:traffic:ascii": async () => { 53 | const [ahead, behind] = await getData(denops); 54 | let component = ""; 55 | if (ahead) { 56 | component += `^${ahead}`; 57 | } 58 | if (behind) { 59 | component += `v${behind}`; 60 | } 61 | return component; 62 | }, 63 | "component:traffic:unicode": async () => { 64 | const [ahead, behind] = await getData(denops); 65 | let component = ""; 66 | if (ahead) { 67 | component += `↑${ahead}`; 68 | } 69 | if (behind) { 70 | component += `↓${behind}`; 71 | } 72 | return component; 73 | }, 74 | }; 75 | } 76 | -------------------------------------------------------------------------------- /denops/gin/component/worktree.ts: -------------------------------------------------------------------------------- 1 | import type { Denops } from "jsr:@denops/std@^7.0.0"; 2 | import { Cache } from "jsr:@lambdalisue/ttl-cache@^1.0.0"; 3 | import * as path from "jsr:@std/path@^1.0.0"; 4 | import { findWorktreeFromDenops } from "../git/worktree.ts"; 5 | import { findWorktree } from "../git/finder.ts"; 6 | 7 | type Data = string; 8 | 9 | const cache = new Cache(100); 10 | 11 | async function getData( 12 | denops: Denops, 13 | ): Promise { 14 | return cache.get("data") ?? await (async () => { 15 | const worktree = await findWorktreeFromDenops(denops); 16 | const result = await findWorktree(worktree); 17 | cache.set("data", result); 18 | return result; 19 | })(); 20 | } 21 | 22 | export function main(denops: Denops): void { 23 | denops.dispatcher = { 24 | ...denops.dispatcher, 25 | "component:worktree:full": async () => { 26 | const fullpath = await getData(denops); 27 | return fullpath; 28 | }, 29 | "component:worktree:name": async () => { 30 | const fullpath = await getData(denops); 31 | return path.basename(fullpath); 32 | }, 33 | }; 34 | } 35 | -------------------------------------------------------------------------------- /denops/gin/git/finder.ts: -------------------------------------------------------------------------------- 1 | import { resolve, SEPARATOR } from "jsr:@std/path@^1.0.0"; 2 | import { Cache } from "jsr:@lambdalisue/ttl-cache@^1.0.0"; 3 | import { execute } from "./process.ts"; 4 | import { decodeUtf8 } from "../util/text.ts"; 5 | 6 | const ttl = 30000; // seconds 7 | const cacheWorktree = new Cache(ttl); 8 | const cacheGitdir = new Cache(ttl); 9 | 10 | /** 11 | * Find a root path of a git working directory. 12 | * 13 | * @param cwd - A current working directory. 14 | * @returns A root path of a git working directory. 15 | */ 16 | export async function findWorktree(cwd: string): Promise { 17 | const path = await Deno.realPath(cwd); 18 | const result = cacheWorktree.get(path) ?? await (async () => { 19 | let result: string | Error; 20 | try { 21 | result = await revParse(path, [ 22 | "--show-toplevel", 23 | "--show-superproject-working-tree", 24 | ]); 25 | } catch (e) { 26 | result = e; 27 | } 28 | cacheWorktree.set(path, result); 29 | return result; 30 | })(); 31 | if (result instanceof Error) { 32 | throw result; 33 | } 34 | return result; 35 | } 36 | 37 | /** 38 | * Find a .git directory of a git working directory. 39 | * 40 | * @param cwd - A current working directory. 41 | * @returns A root path of a git working directory. 42 | */ 43 | export async function findGitdir(cwd: string): Promise { 44 | const path = await Deno.realPath(cwd); 45 | const result = cacheGitdir.get(path) ?? await (async () => { 46 | let result: string | Error; 47 | try { 48 | result = await revParse(path, ["--git-dir"]); 49 | } catch (e) { 50 | result = e; 51 | } 52 | cacheGitdir.set(path, result); 53 | return result; 54 | })(); 55 | if (result instanceof Error) { 56 | throw result; 57 | } 58 | return result; 59 | } 60 | 61 | async function revParse(cwd: string, args: string[]): Promise { 62 | const terms = cwd.split(SEPARATOR); 63 | if (terms.includes(".git")) { 64 | // `git rev-parse` does not work in a ".git" directory 65 | // so use a parent directory of it instead. 66 | const index = terms.indexOf(".git"); 67 | cwd = terms.slice(0, index).join(SEPARATOR); 68 | } 69 | const stdout = await execute(["rev-parse", ...args], { cwd }); 70 | const output = decodeUtf8(stdout); 71 | return resolve(output.split(/\n/, 2)[0]); 72 | } 73 | -------------------------------------------------------------------------------- /denops/gin/git/finder_test.ts: -------------------------------------------------------------------------------- 1 | import { assertEquals, assertRejects } from "jsr:@std/assert@^1.0.0"; 2 | import { sandbox } from "jsr:@lambdalisue/sandbox@^2.0.0"; 3 | import $ from "jsr:@david/dax@^0.42.0"; 4 | import { join } from "jsr:@std/path@^1.0.0"; 5 | import { findGitdir, findWorktree } from "./finder.ts"; 6 | import { ExecuteError } from "./process.ts"; 7 | 8 | Deno.test({ 9 | name: "findWorktree() returns a root path of a git working directory", 10 | fn: async () => { 11 | await using sbox = await prepare(); 12 | Deno.chdir(join("a", "b", "c")); 13 | assertEquals(await findWorktree("."), sbox.path); 14 | // An internal cache will be used for the following call 15 | assertEquals(await findWorktree("."), sbox.path); 16 | }, 17 | sanitizeResources: false, 18 | sanitizeOps: false, 19 | }); 20 | 21 | Deno.test({ 22 | name: 23 | "findWorktree() throws an error if the path is not in a git working directory", 24 | fn: async () => { 25 | await assertRejects(async () => { 26 | await findWorktree("/"); 27 | }, ExecuteError); 28 | // An internal cache will be used for the following call 29 | await assertRejects(async () => { 30 | await findWorktree("/"); 31 | }, ExecuteError); 32 | }, 33 | sanitizeResources: false, 34 | sanitizeOps: false, 35 | }); 36 | 37 | Deno.test({ 38 | name: "findGitdir() returns a root path of a git working directory", 39 | fn: async () => { 40 | await using sbox = await prepare(); 41 | Deno.chdir(join("a", "b", "c")); 42 | assertEquals(await findGitdir("."), join(sbox.path, ".git")); 43 | // An internal cache will be used for the following call 44 | assertEquals(await findGitdir("."), join(sbox.path, ".git")); 45 | }, 46 | sanitizeResources: false, 47 | sanitizeOps: false, 48 | }); 49 | 50 | Deno.test({ 51 | name: "findGitdir() returns a worktree path of a git working directory", 52 | fn: async () => { 53 | await using sbox = await prepare(); 54 | await $`git worktree add -b test test main`; 55 | Deno.chdir("test"); 56 | assertEquals( 57 | await findGitdir("."), 58 | join(sbox.path, ".git", "worktrees", "test"), 59 | ); 60 | // An internal cache will be used for the following call 61 | assertEquals( 62 | await findGitdir("."), 63 | join(sbox.path, ".git", "worktrees", "test"), 64 | ); 65 | }, 66 | sanitizeResources: false, 67 | sanitizeOps: false, 68 | }); 69 | 70 | Deno.test({ 71 | name: 72 | "findGitdir() throws an error if the path is not in a git working directory", 73 | fn: async () => { 74 | await assertRejects(async () => { 75 | await findGitdir("/"); 76 | }, ExecuteError); 77 | // An internal cache will be used for the following call 78 | await assertRejects(async () => { 79 | await findGitdir("/"); 80 | }, ExecuteError); 81 | }, 82 | sanitizeResources: false, 83 | sanitizeOps: false, 84 | }); 85 | 86 | async function prepare(): ReturnType { 87 | const sbox = await sandbox(); 88 | await $`git init`; 89 | await $`git commit --allow-empty -m 'Initial commit' --no-gpg-sign`; 90 | await $`git switch -c main`; 91 | await Deno.mkdir(join("a", "b", "c"), { recursive: true }); 92 | return sbox; 93 | } 94 | -------------------------------------------------------------------------------- /denops/gin/git/process.ts: -------------------------------------------------------------------------------- 1 | import { decodeUtf8 } from "../util/text.ts"; 2 | 3 | export type RunOptions = Omit & { 4 | noOptionalLocks?: boolean; 5 | printCommand?: boolean; 6 | }; 7 | 8 | export function run( 9 | args: string[], 10 | options: RunOptions = {}, 11 | ): Deno.ChildProcess { 12 | const cmdArgs = ["--no-pager", "--literal-pathspecs"]; 13 | if (options.noOptionalLocks) { 14 | cmdArgs.push("--no-optional-locks"); 15 | } 16 | cmdArgs.push(...args); 17 | if (options.printCommand) { 18 | console.debug(`Run 'git ${cmdArgs.join(" ")}' on '${options.cwd}'`); 19 | } 20 | const command = new Deno.Command("git", { 21 | args: cmdArgs, 22 | stdout: options.stdout, 23 | stderr: options.stderr, 24 | cwd: options.cwd, 25 | env: options.env, 26 | }); 27 | return command.spawn(); 28 | } 29 | 30 | export class ExecuteError extends Error { 31 | constructor( 32 | public args: string[], 33 | public code: number, 34 | public stdout: Uint8Array, 35 | public stderr: Uint8Array, 36 | ) { 37 | super(`[${code}]: ${decodeUtf8(stderr)}`); 38 | this.name = this.constructor.name; 39 | } 40 | } 41 | 42 | export async function execute( 43 | args: string[], 44 | options: RunOptions = {}, 45 | ): Promise { 46 | const proc = run(args, { 47 | ...options, 48 | stdout: "piped", 49 | stderr: "piped", 50 | }); 51 | const { code, success, stdout, stderr } = await proc.output(); 52 | if (!success) { 53 | throw new ExecuteError(args, code, stdout, stderr); 54 | } 55 | return stdout; 56 | } 57 | -------------------------------------------------------------------------------- /denops/gin/git/process_test.ts: -------------------------------------------------------------------------------- 1 | import { assert, assertRejects } from "jsr:@std/assert@^1.0.0"; 2 | import { decodeUtf8 } from "../util/text.ts"; 3 | import { execute, ExecuteError, run } from "./process.ts"; 4 | 5 | Deno.test("run() runs 'git' and return a process", async () => { 6 | const proc = run(["version"], { 7 | stdout: "piped", 8 | }); 9 | const { code, stdout } = await proc.output(); 10 | assert(code === 0); 11 | assert(decodeUtf8(stdout).startsWith("git version")); 12 | }); 13 | 14 | Deno.test("run() runs 'git' and return a process (noOptionalLocks)", async () => { 15 | const proc = run(["version"], { 16 | stdout: "piped", 17 | noOptionalLocks: true, 18 | }); 19 | const { code, stdout } = await proc.output(); 20 | assert(code === 0); 21 | assert(decodeUtf8(stdout).startsWith("git version")); 22 | }); 23 | 24 | Deno.test("execute() runs 'git' and return a stdout on success", async () => { 25 | const stdout = await execute(["version"]); 26 | assert(decodeUtf8(stdout).startsWith("git version")); 27 | }); 28 | 29 | Deno.test("execute() runs 'git' and throw ExecuteError on fail", async () => { 30 | await assertRejects(async () => { 31 | await execute(["no-such-command"]); 32 | }, ExecuteError); 33 | }); 34 | -------------------------------------------------------------------------------- /denops/gin/git/worktree.ts: -------------------------------------------------------------------------------- 1 | import type { Denops } from "jsr:@denops/std@^7.0.0"; 2 | import * as batch from "jsr:@denops/std@^7.0.0/batch"; 3 | import { parse as parseBufname } from "jsr:@denops/std@^7.0.0/bufname"; 4 | import * as fn from "jsr:@denops/std@^7.0.0/function"; 5 | import * as path from "jsr:@std/path@^1.0.0"; 6 | import { GIN_BUFFER_PROTOCOLS } from "../global.ts"; 7 | import { expand } from "../util/expand.ts"; 8 | import { findWorktree } from "./finder.ts"; 9 | 10 | /** 11 | * Find a git worktree from a suspected directory 12 | */ 13 | async function findWorktreeFromSuspect( 14 | suspect: string, 15 | verbose?: boolean, 16 | ): Promise { 17 | // Check if 'anchor' is a gin buffer name 18 | try { 19 | const { scheme, expr } = parseBufname(suspect); 20 | if (verbose) { 21 | console.debug(`The anchor scheme is '${scheme}' and expr is '${expr}'`); 22 | } 23 | if (GIN_BUFFER_PROTOCOLS.includes(scheme)) { 24 | // GIN_BUFFER_PROTOCOLS always records an absolute path in `expr` 25 | return expr; 26 | } 27 | } catch { 28 | // Failed to execute 'parseBufname(suspect)' means that the 'suspect' seems a real filepath. 29 | const candidates = new Set(await normSuspect(suspect)); 30 | for (const c of candidates) { 31 | if (verbose) { 32 | console.debug(`Trying to find a git repository from '${c}'`); 33 | } 34 | try { 35 | return await findWorktree(c); 36 | } catch { 37 | // Fail silently 38 | } 39 | } 40 | } 41 | throw new Error(`No git repository found (searched from ${suspect})`); 42 | } 43 | 44 | /** 45 | * Find a git worktree from suspected directories 46 | */ 47 | async function findWorktreeFromSuspects( 48 | suspects: string[], 49 | verbose?: boolean, 50 | ): Promise { 51 | for (const suspect of suspects) { 52 | try { 53 | return await findWorktreeFromSuspect(suspect, verbose); 54 | } catch { 55 | // Fail silently 56 | } 57 | } 58 | throw new Error( 59 | `No git repository found (searched from ${ 60 | [...suspects.values()].join(", ") 61 | })`, 62 | ); 63 | } 64 | 65 | /** 66 | * Return candidates of worktree anchor directories from the host environment 67 | */ 68 | async function listWorktreeSuspectsFromDenops( 69 | denops: Denops, 70 | verbose?: boolean, 71 | ): Promise { 72 | const [cwd, bufname] = await batch.collect( 73 | denops, 74 | (denops) => [ 75 | fn.getcwd(denops), 76 | fn.expand(denops, "%:p") as Promise, 77 | ], 78 | ); 79 | if (verbose) { 80 | console.debug("listWorktreeSuspectsFromDenops"); 81 | console.debug(` cwd: ${cwd}`); 82 | console.debug(` bufname: ${bufname}`); 83 | } 84 | return [bufname, cwd]; 85 | } 86 | 87 | async function normSuspect(suspect: string): Promise { 88 | try { 89 | const [stat, lstat] = await Promise.all([ 90 | Deno.stat(suspect), 91 | Deno.lstat(suspect), 92 | ]); 93 | if (lstat.isSymlink) { 94 | if (stat.isFile) { 95 | return [ 96 | path.dirname(suspect), 97 | path.dirname(await Deno.realPath(suspect)), 98 | ]; 99 | } else { 100 | return [ 101 | suspect, 102 | await Deno.realPath(suspect), 103 | ]; 104 | } 105 | } else { 106 | if (stat.isFile) { 107 | return [path.dirname(suspect)]; 108 | } else { 109 | return [suspect]; 110 | } 111 | } 112 | } catch (err) { 113 | if (err instanceof Deno.errors.NotFound) { 114 | return []; 115 | } 116 | throw err; 117 | } 118 | } 119 | 120 | /** 121 | * Find a git worktree from the host environment 122 | */ 123 | export async function findWorktreeFromDenops( 124 | denops: Denops, 125 | options: FindWorktreeFromDenopsOptions = {}, 126 | ): Promise { 127 | return await findWorktreeFromSuspects( 128 | options.worktree 129 | ? [await expand(denops, options.worktree)] 130 | : await listWorktreeSuspectsFromDenops(denops, !!options.verbose), 131 | !!options.verbose, 132 | ); 133 | } 134 | 135 | export type FindWorktreeFromDenopsOptions = { 136 | worktree?: string; 137 | verbose?: boolean; 138 | }; 139 | -------------------------------------------------------------------------------- /denops/gin/global.ts: -------------------------------------------------------------------------------- 1 | export const GIN_BUFFER_PROTOCOLS = [ 2 | "gin", 3 | "ginbranch", 4 | "gindiff", 5 | "ginedit", 6 | "ginlog", 7 | "ginstatus", 8 | ]; 9 | export const GIN_FILE_BUFFER_PROTOCOLS = ["gindiff", "ginedit", "ginlog"]; 10 | -------------------------------------------------------------------------------- /denops/gin/main.ts: -------------------------------------------------------------------------------- 1 | import type { Denops } from "jsr:@denops/std@^7.0.0"; 2 | 3 | import { main as mainBare } from "./command/bare/main.ts"; 4 | import { main as mainBuffer } from "./command/buffer/main.ts"; 5 | import { main as mainProxy } from "./proxy/main.ts"; 6 | import { main as mainUtil } from "./util/main.ts"; 7 | 8 | import { main as mainBranch } from "./command/branch/main.ts"; 9 | import { main as mainBrowse } from "./command/browse/main.ts"; 10 | import { main as mainChaperon } from "./command/chaperon/main.ts"; 11 | import { main as mainDiff } from "./command/diff/main.ts"; 12 | import { main as mainEdit } from "./command/edit/main.ts"; 13 | import { main as mainLog } from "./command/log/main.ts"; 14 | import { main as mainPatch } from "./command/patch/main.ts"; 15 | import { main as mainStatus } from "./command/status/main.ts"; 16 | 17 | import { main as mainComponentBranch } from "./component/branch.ts"; 18 | import { main as mainComponentTraffic } from "./component/traffic.ts"; 19 | import { main as mainComponentWorktree } from "./component/worktree.ts"; 20 | 21 | export function main(denops: Denops): void { 22 | mainBare(denops); 23 | mainBuffer(denops); 24 | mainProxy(denops); 25 | mainUtil(denops); 26 | 27 | mainBranch(denops); 28 | mainBrowse(denops); 29 | mainChaperon(denops); 30 | mainDiff(denops); 31 | mainEdit(denops); 32 | mainLog(denops); 33 | mainPatch(denops); 34 | mainStatus(denops); 35 | 36 | mainComponentBranch(denops); 37 | mainComponentTraffic(denops); 38 | mainComponentWorktree(denops); 39 | } 40 | -------------------------------------------------------------------------------- /denops/gin/proxy/askpass.ts: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env -S deno run --no-check --allow-env=GIN_PROXY_ADDRESS --allow-net=127.0.0.1 2 | import { pop, push } from "jsr:@lambdalisue/streamtools@^1.0.0"; 3 | import { ensure, is } from "jsr:@core/unknownutil@^4.0.0"; 4 | 5 | const resultPattern = /^([^:]+):(.*)$/; 6 | 7 | const addr = JSON.parse(Deno.env.get("GIN_PROXY_ADDRESS") ?? "null"); 8 | if (!addr) { 9 | throw new Error("GIN_PROXY_ADDRESS environment variable is required"); 10 | } 11 | 12 | const prompt = Deno.args[0]; 13 | if (!prompt) { 14 | throw new Error("No prompt is specified to the askpass"); 15 | } 16 | 17 | const encoder = new TextEncoder(); 18 | const decoder = new TextDecoder(); 19 | 20 | const conn = await Deno.connect(addr); 21 | await push(conn.writable, encoder.encode(`askpass:${prompt}`)); 22 | const result = decoder.decode( 23 | ensure(await pop(conn.readable), is.InstanceOf(Uint8Array)), 24 | ); 25 | conn.close(); 26 | 27 | const m = result.match(resultPattern); 28 | if (!m) { 29 | throw new Error(`Unexpected result '${result}' is received`); 30 | } 31 | 32 | const [status, value] = m.slice(1); 33 | switch (status) { 34 | case "ok": 35 | console.log(value); 36 | Deno.exit(0); 37 | /* fall through */ 38 | case "err": 39 | console.error(value); 40 | Deno.exit(1); 41 | /* fall through */ 42 | default: 43 | throw new Error(`Unexpected status '${status}' is received`); 44 | } 45 | -------------------------------------------------------------------------------- /denops/gin/proxy/editor.ts: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env -S deno run --no-check --allow-env=GIN_PROXY_ADDRESS --allow-net=127.0.0.1 2 | import { pop, push } from "jsr:@lambdalisue/streamtools@^1.0.0"; 3 | import { ensure, is } from "jsr:@core/unknownutil@^4.0.0"; 4 | 5 | const resultPattern = /^([^:]+):(.*)$/; 6 | 7 | const addr = JSON.parse(Deno.env.get("GIN_PROXY_ADDRESS") ?? "null"); 8 | if (!addr) { 9 | throw new Error("GIN_PROXY_ADDRESS environment variable is required"); 10 | } 11 | 12 | const filename = Deno.args[0]; 13 | if (!filename) { 14 | throw new Error("No filename is specified to the editor"); 15 | } 16 | 17 | const encoder = new TextEncoder(); 18 | const decoder = new TextDecoder(); 19 | 20 | const conn = await Deno.connect(addr); 21 | await push(conn.writable, encoder.encode(`editor:${filename}`)); 22 | const result = decoder.decode( 23 | ensure(await pop(conn.readable), is.InstanceOf(Uint8Array)), 24 | ); 25 | conn.close(); 26 | 27 | const m = result.match(resultPattern); 28 | if (!m) { 29 | throw new Error(`Unexpected result '${result}' is received`); 30 | } 31 | 32 | const [status, value] = m.slice(1); 33 | switch (status) { 34 | case "ok": 35 | Deno.exit(0); 36 | /* fall through */ 37 | case "cancel": 38 | Deno.exit(1); 39 | /* fall through */ 40 | case "err": 41 | console.error(value); 42 | Deno.exit(1); 43 | /* fall through */ 44 | default: 45 | throw new Error(`Unexpected status '${status}' is received`); 46 | } 47 | -------------------------------------------------------------------------------- /denops/gin/proxy/main.ts: -------------------------------------------------------------------------------- 1 | import type { Denops } from "jsr:@denops/std@^7.0.0"; 2 | import { listen } from "./server.ts"; 3 | 4 | export function main(denops: Denops): void { 5 | listen(denops).catch((e) => 6 | console.error(`Unexpected error occured in the proxy server: ${e}`) 7 | ); 8 | } 9 | -------------------------------------------------------------------------------- /denops/gin/proxy/server.ts: -------------------------------------------------------------------------------- 1 | import type { Denops } from "jsr:@denops/std@^7.0.0"; 2 | import * as lambda from "jsr:@denops/std@^7.0.0/lambda"; 3 | import * as batch from "jsr:@denops/std@^7.0.0/batch"; 4 | import * as buffer from "jsr:@denops/std@^7.0.0/buffer"; 5 | import * as fn from "jsr:@denops/std@^7.0.0/function"; 6 | import * as vars from "jsr:@denops/std@^7.0.0/variable"; 7 | import { ensure, is } from "jsr:@core/unknownutil@^4.0.0"; 8 | import * as path from "jsr:@std/path@^1.0.0"; 9 | import { pop, push } from "jsr:@lambdalisue/streamtools@^1.0.0"; 10 | import { decodeUtf8, encodeUtf8 } from "../util/text.ts"; 11 | 12 | const recordPattern = /^([^:]+?):(.*)$/; 13 | 14 | export async function listen(denops: Denops): Promise { 15 | const listener = Deno.listen({ 16 | hostname: "127.0.0.1", 17 | port: 0, 18 | }); 19 | const [disableAskpass, disableEditor] = await batch.collect( 20 | denops, 21 | (denops) => [ 22 | vars.g.get(denops, "gin_proxy_disable_askpass"), 23 | vars.g.get(denops, "gin_proxy_disable_editor"), 24 | ], 25 | ); 26 | await batch.batch(denops, async (denops) => { 27 | await vars.e.set( 28 | denops, 29 | "GIN_PROXY_ADDRESS", 30 | JSON.stringify(listener.addr), 31 | ); 32 | if ( 33 | !ensure(disableAskpass ?? false, is.Boolean, { 34 | message: "g:gin_proxy_disable_askpass must be boolean", 35 | }) 36 | ) { 37 | const script = path.fromFileUrl(new URL("askpass.ts", import.meta.url)); 38 | await vars.e.set( 39 | denops, 40 | "GIT_ASKPASS", 41 | denops.meta.platform === "windows" ? `"${script}"` : `'${script}'`, 42 | ); 43 | } 44 | if ( 45 | !ensure(disableEditor ?? false, is.Boolean, { 46 | message: "g:gin_proxy_disable_editor must be boolean", 47 | }) 48 | ) { 49 | const script = path.fromFileUrl(new URL("editor.ts", import.meta.url)); 50 | await vars.e.set( 51 | denops, 52 | "GIT_EDITOR", 53 | denops.meta.platform === "windows" ? `"${script}"` : `'${script}'`, 54 | ); 55 | } 56 | }); 57 | for await (const conn of listener) { 58 | handleConnection(denops, conn).catch((e) => console.error(e)); 59 | } 60 | } 61 | 62 | async function handleConnection( 63 | denops: Denops, 64 | conn: Deno.Conn, 65 | ): Promise { 66 | const record = decodeUtf8( 67 | ensure(await pop(conn.readable), is.InstanceOf(Uint8Array)), 68 | ); 69 | const m = record.match(recordPattern); 70 | if (!m) { 71 | throw new Error(`Unexpected record '${record}' received`); 72 | } 73 | const [name, value] = m.slice(1); 74 | try { 75 | switch (name) { 76 | case "askpass": 77 | await handleAskpass(denops, conn, value); 78 | break; 79 | case "editor": 80 | await handleEditor(denops, conn, value); 81 | break; 82 | default: 83 | throw new Error(`Unexpected record prefix '${name}' received`); 84 | } 85 | } finally { 86 | conn.close(); 87 | } 88 | } 89 | 90 | async function handleAskpass( 91 | denops: Denops, 92 | conn: Deno.Conn, 93 | prompt: string, 94 | ): Promise { 95 | try { 96 | const value = await fn.inputsecret(denops, prompt); 97 | await push(conn.writable, encodeUtf8(`ok:${value}`)); 98 | } catch (e: unknown) { 99 | await push(conn.writable, encodeUtf8(`err:${e}`)); 100 | } 101 | } 102 | 103 | async function handleEditor( 104 | denops: Denops, 105 | conn: Deno.Conn, 106 | filename: string, 107 | ): Promise { 108 | try { 109 | if (await edit(denops, filename)) { 110 | await push(conn.writable, encodeUtf8("ok:")); 111 | } else { 112 | await Deno.writeFile(filename, new Uint8Array()); 113 | await push(conn.writable, encodeUtf8("cancel:")); 114 | } 115 | } catch (e: unknown) { 116 | await push(conn.writable, encodeUtf8(`err:${e}`)); 117 | } 118 | } 119 | 120 | async function edit( 121 | denops: Denops, 122 | filename: string, 123 | ): Promise { 124 | const opener = await vars.g.get(denops, "gin_proxy_editor_opener", "tabedit"); 125 | const { bufnr } = await buffer.open( 126 | denops, 127 | filename, 128 | { 129 | mods: "silent", 130 | opener, 131 | }, 132 | ); 133 | 134 | const { promise, resolve } = Promise.withResolvers(); 135 | const waiterId = lambda.register(denops, (accept) => { 136 | resolve(!!accept); 137 | }, { once: true }); 138 | await buffer.ensure(denops, bufnr, async () => { 139 | await denops.call("gin#internal#proxy#init", waiterId); 140 | }); 141 | return promise; 142 | } 143 | -------------------------------------------------------------------------------- /denops/gin/util/__snapshots__/ansi_escape_code_test.ts.snap: -------------------------------------------------------------------------------- 1 | export const snapshot = {}; 2 | 3 | snapshot[`buildDecorationsFromAnsiEscapeCode (nvim) 1`] = ` 4 | { 5 | content: [ 6 | "\\x1b[1mbold\\x1b[0m", 7 | "\\x1b[3mitalic\\x1b[0m", 8 | "\\x1b[4munderline\\x1b[0m", 9 | "\\x1b[9mstrikethrough\\x1b[0m", 10 | "\\x1b[31mred\\x1b[0m", 11 | "", 12 | ], 13 | decorations: [ 14 | { 15 | column: 1, 16 | highlight: "GinColorBold", 17 | length: 4, 18 | line: 1, 19 | }, 20 | { 21 | column: 1, 22 | highlight: "GinColorItalic", 23 | length: 6, 24 | line: 2, 25 | }, 26 | { 27 | column: 1, 28 | highlight: "GinColorUnderline", 29 | length: 9, 30 | line: 3, 31 | }, 32 | { 33 | column: 1, 34 | highlight: "GinColorStrike", 35 | length: 13, 36 | line: 4, 37 | }, 38 | { 39 | column: 1, 40 | highlight: "GinColorF1", 41 | length: 3, 42 | line: 5, 43 | }, 44 | ], 45 | trimmed: [ 46 | "bold", 47 | "italic", 48 | "underline", 49 | "strikethrough", 50 | "red", 51 | "", 52 | ], 53 | } 54 | `; 55 | 56 | snapshot[`buildDecorationsFromAnsiEscapeCode (vim) 1`] = ` 57 | { 58 | content: [ 59 | "\\x1b[1mbold\\x1b[0m", 60 | "\\x1b[3mitalic\\x1b[0m", 61 | "\\x1b[4munderline\\x1b[0m", 62 | "\\x1b[9mstrikethrough\\x1b[0m", 63 | "\\x1b[31mred\\x1b[0m", 64 | "", 65 | ], 66 | decorations: [ 67 | { 68 | column: 1, 69 | highlight: "GinColorBold", 70 | length: 4, 71 | line: 1, 72 | }, 73 | { 74 | column: 1, 75 | highlight: "GinColorItalic", 76 | length: 6, 77 | line: 2, 78 | }, 79 | { 80 | column: 1, 81 | highlight: "GinColorUnderline", 82 | length: 9, 83 | line: 3, 84 | }, 85 | { 86 | column: 1, 87 | highlight: "GinColorStrike", 88 | length: 13, 89 | line: 4, 90 | }, 91 | { 92 | column: 1, 93 | highlight: "GinColorF1", 94 | length: 3, 95 | line: 5, 96 | }, 97 | ], 98 | trimmed: [ 99 | "bold", 100 | "italic", 101 | "underline", 102 | "strikethrough", 103 | "red", 104 | "", 105 | ], 106 | } 107 | `; 108 | -------------------------------------------------------------------------------- /denops/gin/util/ansi_escape_code_test.ts: -------------------------------------------------------------------------------- 1 | import { assertSnapshot } from "jsr:@std/testing@^1.0.0/snapshot"; 2 | import { test } from "./testutil.ts"; 3 | import { buildDecorationsFromAnsiEscapeCode } from "./ansi_escape_code.ts"; 4 | 5 | const testText = await Deno.readTextFile( 6 | new URL("./testdata/ansi_escape_code_content.txt", import.meta.url), 7 | ); 8 | 9 | // Based on the Deno.inspect enhancements added in Deno 1.36, assertSnapshot has undergone 10 | // a destructive change, so those supporting Deno 1.36 or earlier can only specify a custom 11 | // serializer to ensure backward compatibility. 12 | // https://github.com/denoland/deno_std/pull/3447#issuecomment-1666839964 13 | const serializer = (v: unknown): string => 14 | Deno.inspect(v, { 15 | depth: Infinity, 16 | sorted: true, 17 | trailingComma: true, 18 | compact: false, 19 | iterableLimit: Infinity, 20 | strAbbreviateSize: Infinity, 21 | breakLength: Infinity, // Added in Deno 1.36.0 22 | escapeSequences: true, // Added in Deno 1.36.0 23 | // deno-lint-ignore no-explicit-any 24 | } as any); 25 | 26 | test("all", "buildDecorationsFromAnsiEscapeCode", async (denops, t) => { 27 | await t.step({ 28 | name: "returns proper Decoration[]", 29 | fn: async () => { 30 | const content = testText.split("\n"); 31 | const [trimmed, decorations] = await buildDecorationsFromAnsiEscapeCode( 32 | denops, 33 | content, 34 | ); 35 | await assertSnapshot(t, { 36 | content, 37 | trimmed, 38 | decorations, 39 | }, { 40 | serializer, 41 | }); 42 | }, 43 | }); 44 | }); 45 | -------------------------------------------------------------------------------- /denops/gin/util/cmd.ts: -------------------------------------------------------------------------------- 1 | import type { Denops } from "jsr:@denops/std@^7.0.0"; 2 | import { assert, is } from "jsr:@core/unknownutil@^4.0.0"; 3 | import * as batch from "jsr:@denops/std@^7.0.0/batch"; 4 | import * as vars from "jsr:@denops/std@^7.0.0/variable"; 5 | import { Silent } from "jsr:@denops/std@^7.0.0/helper"; 6 | import { expand } from "./expand.ts"; 7 | 8 | export function parseSilent(mods: string): Silent { 9 | if (mods.indexOf("silent!") !== -1) { 10 | return "silent!"; 11 | } else if (mods.indexOf("silent") !== -1) { 12 | return "silent"; 13 | } 14 | return ""; 15 | } 16 | 17 | export async function normCmdArgs( 18 | denops: Denops, 19 | args: string[], 20 | ): Promise { 21 | // To reduce RPC, cache result of an arg which starts from '%' or '#' 22 | const cache: Map> = new Map(); 23 | return await Promise.all(args.map((arg) => normCmdArg(denops, arg, cache))); 24 | } 25 | 26 | async function normCmdArg( 27 | denops: Denops, 28 | arg: string, 29 | cache: Map>, 30 | ): Promise { 31 | if (cache.has(arg)) { 32 | return await cache.get(arg)!; 33 | } 34 | if (arg.startsWith("%") || arg.startsWith("#")) { 35 | const p = expand(denops, arg); 36 | cache.set(arg, p); 37 | return await p; 38 | } 39 | return arg.replaceAll(/^\\(%|#)/g, "$1"); 40 | } 41 | 42 | export async function fillCmdArgs( 43 | denops: Denops, 44 | args: string[], 45 | command: string, 46 | ): Promise { 47 | const defaultArgsVarName = `gin_${command}_default_args`; 48 | const persistentArgsVarName = `gin_${command}_persistent_args`; 49 | const [defaultArgs, persistentArgs] = await batch.collect( 50 | denops, 51 | (denops) => [ 52 | vars.g.get(denops, defaultArgsVarName, []), 53 | vars.g.get(denops, persistentArgsVarName, []), 54 | ], 55 | ); 56 | assert(defaultArgs, is.ArrayOf(is.String), { 57 | name: `g:${defaultArgsVarName}`, 58 | }); 59 | assert(persistentArgs, is.ArrayOf(is.String), { 60 | name: `g:${persistentArgsVarName}`, 61 | }); 62 | return [...persistentArgs, ...(args.length === 0 ? defaultArgs : args)]; 63 | } 64 | -------------------------------------------------------------------------------- /denops/gin/util/cmd_test.ts: -------------------------------------------------------------------------------- 1 | import { assertEquals } from "jsr:@std/assert@^1.0.0"; 2 | import * as batch from "jsr:@denops/std@^7.0.0/batch"; 3 | import * as fn from "jsr:@denops/std@^7.0.0/function"; 4 | import * as path from "jsr:@std/path@^1.0.0"; 5 | import { deadline } from "jsr:@std/async@^1.0.0"; 6 | import { test } from "./testutil.ts"; 7 | import { normCmdArgs } from "./cmd.ts"; 8 | 9 | test("all", "cmd", async (denops, t) => { 10 | await t.step({ 11 | name: "normCmdArgs does nothing on args without '%' or '#'", 12 | fn: async () => { 13 | await denops.cmd("%bwipeout!"); 14 | await batch.batch(denops, async (denops) => { 15 | await denops.cmd("edit dummy1"); 16 | await denops.cmd("file dummy2"); 17 | }); 18 | const src = ["a", "b", "c"]; 19 | const dst = await normCmdArgs(denops, src); 20 | const exp = src; 21 | assertEquals(dst, exp); 22 | }, 23 | }); 24 | await t.step({ 25 | name: "normCmdArgs does not expand arg starts from '\%' or '\#'", 26 | fn: async () => { 27 | await denops.cmd("%bwipeout!"); 28 | await batch.batch(denops, async (denops) => { 29 | await denops.cmd("edit dummy1"); 30 | await denops.cmd("file dummy2"); 31 | }); 32 | const src = ["\\%", "\\%:p", "\\%hello", "\\#", "\\#:p", "\\#hello"]; 33 | const dst = await normCmdArgs(denops, src); 34 | const exp = [ 35 | "%", 36 | "%:p", 37 | "%hello", 38 | "#", 39 | "#:p", 40 | "#hello", 41 | ]; 42 | assertEquals(dst, exp); 43 | }, 44 | }); 45 | await t.step({ 46 | name: "normCmdArgs expands arg starts from '%' or '#'", 47 | fn: async () => { 48 | await denops.cmd("%bwipeout!"); 49 | await batch.batch(denops, async (denops) => { 50 | await denops.cmd("edit dummy1"); 51 | await denops.cmd("file dummy2"); 52 | }); 53 | const cwd = await fn.getcwd(denops); 54 | const src = ["%", "%:p", "%hello", "#", "#:p", "#hello"]; 55 | const dst = await normCmdArgs(denops, src); 56 | const exp = [ 57 | "dummy2", 58 | path.join(cwd, "dummy2"), 59 | "dummy2", 60 | "dummy1", 61 | path.join(cwd, "dummy1"), 62 | "dummy1", 63 | ]; 64 | assertEquals(dst, exp); 65 | }, 66 | }); 67 | await t.step({ 68 | name: "normCmdArgs expands massive args starts from '%' or '#'", 69 | fn: async () => { 70 | await denops.cmd("%bwipeout!"); 71 | await batch.batch(denops, async (denops) => { 72 | await denops.cmd("edit dummy1"); 73 | await denops.cmd("file dummy2"); 74 | }); 75 | const src = [ 76 | ...Array(10000).fill("%"), 77 | ...Array(10000).fill("#"), 78 | ]; 79 | const dst = await deadline(normCmdArgs(denops, src), 1000); 80 | const exp = [ 81 | ...Array(10000).fill("dummy2"), 82 | ...Array(10000).fill("dummy1"), 83 | ]; 84 | assertEquals(dst, exp); 85 | }, 86 | }); 87 | }); 88 | -------------------------------------------------------------------------------- /denops/gin/util/ensure_path.ts: -------------------------------------------------------------------------------- 1 | import type { Denops } from "jsr:@denops/std@^7.0.0"; 2 | import * as fn from "jsr:@denops/std@^7.0.0/function"; 3 | 4 | /** 5 | * Ensure the path is absolute. 6 | * 7 | * It returns the absolute path of the given path. If the path is not given, it 8 | * returns the absolute path of the current buffer. 9 | * 10 | * @param denops Denops instance. 11 | * @param path Path to ensure. 12 | * @returns Absolute path. 13 | */ 14 | export async function ensurePath( 15 | denops: Denops, 16 | path?: string, 17 | ): Promise { 18 | const bufname = await fn.expand(denops, path ?? "%") as string; 19 | const abspath = await fn.fnamemodify(denops, bufname, ":p"); 20 | return abspath; 21 | } 22 | -------------------------------------------------------------------------------- /denops/gin/util/expand.ts: -------------------------------------------------------------------------------- 1 | import type { Denops } from "jsr:@denops/std@^7.0.0"; 2 | import * as fn from "jsr:@denops/std@^7.0.0/function"; 3 | import { parse as parseBufname } from "jsr:@denops/std@^7.0.0/bufname"; 4 | import { join } from "jsr:@std/path@^1.0.0/join"; 5 | import { ensure, is } from "jsr:@core/unknownutil@^4.0.0"; 6 | import { GIN_FILE_BUFFER_PROTOCOLS } from "../global.ts"; 7 | 8 | export async function expand(denops: Denops, expr: string): Promise { 9 | const bufname = ensure(await fn.expand(denops, expr), is.String); 10 | try { 11 | const { scheme, expr, fragment } = parseBufname(bufname); 12 | if (fragment && GIN_FILE_BUFFER_PROTOCOLS.includes(scheme)) { 13 | try { 14 | // Return the first path if the buffer has multiple paths 15 | const paths = JSON.parse(fragment); 16 | const path = paths.at(0); 17 | return path ? join(expr, path) : bufname; 18 | } catch { 19 | return join(expr, fragment); 20 | } 21 | } 22 | } catch { 23 | // Ignore errors 24 | } 25 | return bufname; 26 | } 27 | -------------------------------------------------------------------------------- /denops/gin/util/indicator_stream.ts: -------------------------------------------------------------------------------- 1 | import { removeAnsiEscapeCode } from "./ansi_escape_code.ts"; 2 | 3 | export type Indicator = { 4 | open(id: string): void | PromiseLike; 5 | write(id: string, content: string[]): void | PromiseLike; 6 | close(id: string): void | PromiseLike; 7 | }; 8 | 9 | export type IndicatorStreamOptions = { 10 | encoding?: string; 11 | }; 12 | 13 | /** 14 | * IndicatorStream writes chunks to the specified indicator 15 | */ 16 | export class IndicatorStream extends TransformStream { 17 | constructor( 18 | indicator: Indicator, 19 | options: IndicatorStreamOptions = {}, 20 | ) { 21 | const { encoding = "utf-8" } = options; 22 | const id = crypto.randomUUID(); 23 | const decoder = new TextDecoder(encoding); 24 | super({ 25 | start() { 26 | return indicator.open(id); 27 | }, 28 | async transform(chunk, controller) { 29 | const text = removeAnsiEscapeCode(decoder.decode(chunk)); 30 | await indicator.write(id, text.trim().split("\n")); 31 | controller.enqueue(chunk); 32 | }, 33 | flush() { 34 | return indicator.close(id); 35 | }, 36 | }); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /denops/gin/util/main.ts: -------------------------------------------------------------------------------- 1 | import type { Denops } from "jsr:@denops/std@^7.0.0"; 2 | import { as, assert, is } from "jsr:@core/unknownutil@^4.0.0"; 3 | import * as buffer from "jsr:@denops/std@^7.0.0/buffer"; 4 | import { expand } from "./expand.ts"; 5 | import { findWorktreeFromDenops } from "../git/worktree.ts"; 6 | 7 | export function main(denops: Denops): void { 8 | denops.dispatcher = { 9 | ...denops.dispatcher, 10 | "util:reload": (bufnr) => { 11 | assert(bufnr, is.Number, { name: "bufnr" }); 12 | return buffer.reload(denops, bufnr); 13 | }, 14 | 15 | "util:expand": (expr) => { 16 | assert(expr, is.String, { name: "expr" }); 17 | return expand(denops, expr); 18 | }, 19 | 20 | "util:worktree": (worktree) => { 21 | assert(worktree, as.Optional(is.String), { 22 | name: "worktree", 23 | }); 24 | return findWorktreeFromDenops(denops, { worktree }); 25 | }, 26 | }; 27 | } 28 | -------------------------------------------------------------------------------- /denops/gin/util/testdata/ansi_escape_code_content.txt: -------------------------------------------------------------------------------- 1 | bold 2 | italic 3 | underline 4 | strikethrough 5 | red 6 | -------------------------------------------------------------------------------- /denops/gin/util/testutil.ts: -------------------------------------------------------------------------------- 1 | import * as path from "jsr:@std/path@^1.0.0"; 2 | import { test as testOri, TestDefinition } from "jsr:@denops/test@^3.0.0"; 3 | 4 | const runtimepath = path.resolve( 5 | path.fromFileUrl(new URL("../../..", import.meta.url)), 6 | ); 7 | 8 | export function test( 9 | mode: TestDefinition["mode"], 10 | name: string, 11 | fn: TestDefinition["fn"], 12 | ): void; 13 | export function test(def: TestDefinition): void; 14 | export function test( 15 | modeOrDef: TestDefinition["mode"] | TestDefinition, 16 | name?: string, 17 | fn?: TestDefinition["fn"], 18 | ): void { 19 | if (typeof modeOrDef === "string") { 20 | if (!name) { 21 | throw new Error(`'name' attribute is required`); 22 | } 23 | if (!fn) { 24 | throw new Error(`'fn' attribute is required`); 25 | } 26 | test({ mode: modeOrDef, name, fn }); 27 | return; 28 | } 29 | testOri({ 30 | ...modeOrDef, 31 | prelude: [ 32 | `set runtimepath^=${runtimepath}`, 33 | ...(modeOrDef.prelude ?? []), 34 | ], 35 | }); 36 | } 37 | -------------------------------------------------------------------------------- /denops/gin/util/text.ts: -------------------------------------------------------------------------------- 1 | const textEncoder = new TextEncoder(); 2 | const textDecoder = new TextDecoder(); 3 | 4 | /** 5 | * Encode a string into an UTF8 Uint8Array 6 | */ 7 | export function encodeUtf8(input: string): Uint8Array { 8 | return textEncoder.encode(input); 9 | } 10 | 11 | /** 12 | * Decode an UTF8 Uint8Array into a string 13 | */ 14 | export function decodeUtf8(input: BufferSource): string { 15 | return textDecoder.decode(input); 16 | } 17 | 18 | export const NUL = 0x00; 19 | 20 | /** 21 | * Partition NUL separated bytes into chunks. 22 | */ 23 | export function* partition( 24 | bytes: Uint8Array, 25 | start = 0, 26 | ): Generator { 27 | let index = bytes.indexOf(NUL, start); 28 | while (index !== -1) { 29 | yield bytes.subarray(start, index); 30 | start = index + 1; 31 | index = bytes.indexOf(NUL, start); 32 | } 33 | yield bytes.subarray(start); 34 | } 35 | 36 | /** 37 | * Count code points instead 38 | * 39 | * ref: https://coolaj86.com/articles/how-to-count-unicode-characters-in-javascript/ 40 | */ 41 | export function countCodePoints(str: string): number { 42 | let len = 0; 43 | for (let index = 0; index < str.length;) { 44 | let point = str.codePointAt(index); 45 | if (point == undefined) { 46 | throw new Error("codePointAt returns undefined"); 47 | } 48 | let width = 0; 49 | while (point) { 50 | width += 1; 51 | point = point >> 8; 52 | } 53 | index += Math.round(width / 2); 54 | len += 1; 55 | } 56 | return len; 57 | } 58 | 59 | /** 60 | * Count bytes of `str` in Vim 61 | */ 62 | export function countVimBytes(str: string): number { 63 | return encodeUtf8(str).length; 64 | } 65 | -------------------------------------------------------------------------------- /denops/gin/util/text_test.ts: -------------------------------------------------------------------------------- 1 | import { assertEquals } from "jsr:@std/assert@^1.0.0"; 2 | import { 3 | countCodePoints, 4 | countVimBytes, 5 | decodeUtf8, 6 | encodeUtf8, 7 | NUL, 8 | partition, 9 | } from "./text.ts"; 10 | 11 | Deno.test("encodeUtf8() encodes a string into an UTF8 Uint8Array", () => { 12 | const input = "Hello world!"; 13 | const exp = new Uint8Array([ 14 | 72, 15 | 101, 16 | 108, 17 | 108, 18 | 111, 19 | 32, 20 | 119, 21 | 111, 22 | 114, 23 | 108, 24 | 100, 25 | 33, 26 | ]); 27 | assertEquals(encodeUtf8(input), exp); 28 | }); 29 | 30 | Deno.test("decodeUtf8() decodes an UTF8 Uint8Array into a string", () => { 31 | const input = new Uint8Array([ 32 | 72, 33 | 101, 34 | 108, 35 | 108, 36 | 111, 37 | 32, 38 | 119, 39 | 111, 40 | 114, 41 | 108, 42 | 100, 43 | 33, 44 | ]); 45 | const exp = "Hello world!"; 46 | assertEquals(decodeUtf8(input), exp); 47 | }); 48 | 49 | Deno.test("partition", () => { 50 | const b = new Uint8Array([ 51 | 1, 52 | 2, 53 | 3, 54 | 4, 55 | 5, 56 | NUL, 57 | 5, 58 | 4, 59 | 3, 60 | 2, 61 | 1, 62 | NUL, 63 | 1, 64 | NUL, 65 | 2, 66 | 2, 67 | NUL, 68 | 3, 69 | 3, 70 | 3, 71 | ]); 72 | const result = [...partition(b)]; 73 | assertEquals(result, [ 74 | new Uint8Array([1, 2, 3, 4, 5]), 75 | new Uint8Array([5, 4, 3, 2, 1]), 76 | new Uint8Array([1]), 77 | new Uint8Array([2, 2]), 78 | new Uint8Array([3, 3, 3]), 79 | ]); 80 | }); 81 | Deno.test("partition (NUL start)", () => { 82 | const b = new Uint8Array([ 83 | NUL, 84 | 1, 85 | 2, 86 | 3, 87 | 4, 88 | 5, 89 | NUL, 90 | 5, 91 | 4, 92 | 3, 93 | 2, 94 | 1, 95 | NUL, 96 | 1, 97 | NUL, 98 | 2, 99 | 2, 100 | NUL, 101 | 3, 102 | 3, 103 | 3, 104 | ]); 105 | const result = [...partition(b)]; 106 | assertEquals(result, [ 107 | new Uint8Array([]), 108 | new Uint8Array([1, 2, 3, 4, 5]), 109 | new Uint8Array([5, 4, 3, 2, 1]), 110 | new Uint8Array([1]), 111 | new Uint8Array([2, 2]), 112 | new Uint8Array([3, 3, 3]), 113 | ]); 114 | }); 115 | Deno.test("partition (NUL end)", () => { 116 | const b = new Uint8Array([ 117 | 1, 118 | 2, 119 | 3, 120 | 4, 121 | 5, 122 | NUL, 123 | 5, 124 | 4, 125 | 3, 126 | 2, 127 | 1, 128 | NUL, 129 | 1, 130 | NUL, 131 | 2, 132 | 2, 133 | NUL, 134 | 3, 135 | 3, 136 | 3, 137 | NUL, 138 | ]); 139 | const result = [...partition(b)]; 140 | assertEquals(result, [ 141 | new Uint8Array([1, 2, 3, 4, 5]), 142 | new Uint8Array([5, 4, 3, 2, 1]), 143 | new Uint8Array([1]), 144 | new Uint8Array([2, 2]), 145 | new Uint8Array([3, 3, 3]), 146 | new Uint8Array([]), 147 | ]); 148 | }); 149 | Deno.test("partition (NUL start/end)", () => { 150 | const b = new Uint8Array([ 151 | NUL, 152 | 1, 153 | 2, 154 | 3, 155 | 4, 156 | 5, 157 | NUL, 158 | 5, 159 | 4, 160 | 3, 161 | 2, 162 | 1, 163 | NUL, 164 | 1, 165 | NUL, 166 | 2, 167 | 2, 168 | NUL, 169 | 3, 170 | 3, 171 | 3, 172 | NUL, 173 | ]); 174 | const result = [...partition(b)]; 175 | assertEquals(result, [ 176 | new Uint8Array([]), 177 | new Uint8Array([1, 2, 3, 4, 5]), 178 | new Uint8Array([5, 4, 3, 2, 1]), 179 | new Uint8Array([1]), 180 | new Uint8Array([2, 2]), 181 | new Uint8Array([3, 3, 3]), 182 | new Uint8Array([]), 183 | ]); 184 | }); 185 | 186 | Deno.test("countCodePoints", async (t) => { 187 | const testcases: [string, number][] = [ 188 | ["", 0], 189 | ["Hello", 5], 190 | ["こんにちわ", 5], 191 | ["🎉🔥✨💯", 4], 192 | ["🥃", 1], 193 | ]; 194 | for (const [expr, expected] of testcases) { 195 | await t.step(`properly handle "${expr}"`, () => { 196 | const actual = countCodePoints(expr); 197 | assertEquals(actual, expected); 198 | }); 199 | } 200 | }); 201 | Deno.test("countVimBytes", async (t) => { 202 | const testcases: [string, number][] = [ 203 | ["", 0], 204 | ["Hello", 5], 205 | ["こんにちわ", 15], 206 | ["🎉🔥✨💯", 15], 207 | ["🥃", 4], 208 | ]; 209 | for (const [expr, expected] of testcases) { 210 | await t.step(`properly handle "${expr}"`, () => { 211 | const actual = countVimBytes(expr); 212 | assertEquals(actual, expected); 213 | }); 214 | } 215 | }); 216 | -------------------------------------------------------------------------------- /denops/gin/util/yank.ts: -------------------------------------------------------------------------------- 1 | import type { Denops } from "jsr:@denops/std@^7.0.0"; 2 | import * as fn from "jsr:@denops/std@^7.0.0/function"; 3 | import { v } from "jsr:@denops/std@^7.0.0/variable"; 4 | 5 | export async function yank( 6 | denops: Denops, 7 | value: string, 8 | reg?: string, 9 | ): Promise { 10 | reg = reg ?? await v.get(denops, "register"); 11 | await fn.setreg(denops, reg, value); 12 | } 13 | -------------------------------------------------------------------------------- /denops/gin/util/yank_test.ts: -------------------------------------------------------------------------------- 1 | import { assertEquals } from "jsr:@std/assert@^1.0.0"; 2 | import { test } from "./testutil.ts"; 3 | import { yank } from "./yank.ts"; 4 | 5 | test("all", "yank", async (denops, t) => { 6 | await t.step({ 7 | name: "sets the value to v:register", 8 | fn: async () => { 9 | await yank(denops, "Hello world"); 10 | assertEquals(await denops.eval("getreg(v:register)"), "Hello world"); 11 | }, 12 | }); 13 | await t.step({ 14 | name: "sets the value to the named register a", 15 | fn: async () => { 16 | await yank(denops, "Hello world", "a"); 17 | assertEquals(await denops.eval("getreg('a')"), "Hello world"); 18 | }, 19 | }); 20 | }); 21 | -------------------------------------------------------------------------------- /doc/.gitignore: -------------------------------------------------------------------------------- 1 | /tags 2 | -------------------------------------------------------------------------------- /ftplugin/gin-branch.vim: -------------------------------------------------------------------------------- 1 | if exists('b:did_ftplugin') 2 | finish 3 | endif 4 | let b:did_ftplugin = 1 5 | 6 | setlocal nolist nospell 7 | setlocal nowrap nofoldenable 8 | setlocal nomodeline 9 | 10 | if !get(g:, 'gin_branch_disable_default_mappings') 11 | map a (gin-action-choice) 12 | map . (gin-action-repeat) 13 | nmap ? (gin-action-help) 14 | 15 | nmap (gin-action-switch) 16 | 17 | nmap yy (gin-action-yank:branch) 18 | vmap y (gin-action-yank:branch) 19 | endif 20 | -------------------------------------------------------------------------------- /ftplugin/gin-diff.vim: -------------------------------------------------------------------------------- 1 | if exists('b:did_ftplugin') 2 | finish 3 | endif 4 | runtime ftplugin/diff.vim 5 | 6 | if !get(g:, 'gin_diff_disable_default_mappings') 7 | nmap (gin-diffjump-smart)zv 8 | nmap g (gin-diffjump-old)zv 9 | nmap (gin-diffjump-new)zv 10 | endif 11 | 12 | let b:did_ftplugin = 1 13 | -------------------------------------------------------------------------------- /ftplugin/gin-log.vim: -------------------------------------------------------------------------------- 1 | if exists('b:did_ftplugin') 2 | finish 3 | endif 4 | let b:did_ftplugin = 1 5 | 6 | setlocal nobuflisted 7 | setlocal nolist nospell 8 | setlocal nowrap nofoldenable 9 | setlocal cursorline 10 | setlocal nomodeline 11 | 12 | if !get(g:, 'gin_log_disable_default_mappings') 13 | map a (gin-action-choice) 14 | map . (gin-action-repeat) 15 | nmap ? (gin-action-help) 16 | 17 | map (gin-action-show)zv 18 | 19 | nmap yy (gin-action-yank:commit) 20 | vmap y (gin-action-yank:commit) 21 | endif 22 | -------------------------------------------------------------------------------- /ftplugin/gin-status.vim: -------------------------------------------------------------------------------- 1 | if exists('b:did_ftplugin') 2 | finish 3 | endif 4 | let b:did_ftplugin = 1 5 | 6 | setlocal nobuflisted 7 | setlocal nolist nospell 8 | setlocal nowrap nofoldenable 9 | setlocal nomodeline 10 | 11 | if !get(g:, 'gin_status_disable_default_mappings') 12 | map a (gin-action-choice) 13 | map . (gin-action-repeat) 14 | nmap ? (gin-action-help) 15 | 16 | map (gin-action-edit)zv 17 | 18 | map dd (gin-action-diff:smart) 19 | 20 | map pp (gin-action-patch) 21 | 22 | map !! (gin-action-chaperon) 23 | 24 | map << (gin-action-stage) 25 | map >> (gin-action-unstage) 26 | map == (gin-action-stash) 27 | 28 | nmap yy (gin-action-yank:path) 29 | vmap y (gin-action-yank:path) 30 | endif 31 | -------------------------------------------------------------------------------- /ftplugin/gitcommit/gin.vim: -------------------------------------------------------------------------------- 1 | if get(g:, 'gin_proxy_disable_accept_and_cancel_aliases', 0) 2 | finish 3 | endif 4 | 5 | if !exists(':Accept') 6 | command! -buffer Accept wq 7 | endif 8 | if !exists(':Cancel') 9 | command! -buffer Cancel cq 10 | endif 11 | -------------------------------------------------------------------------------- /ftplugin/gitrebase/gin.vim: -------------------------------------------------------------------------------- 1 | if get(g:, 'gin_proxy_disable_accept_and_cancel_aliases', 0) 2 | finish 3 | endif 4 | 5 | if !exists(':Accept') 6 | command! -buffer Accept wq 7 | endif 8 | if !exists(':Cancel') 9 | command! -buffer Cancel cq 10 | endif 11 | -------------------------------------------------------------------------------- /lefthook.yml: -------------------------------------------------------------------------------- 1 | pre-commit: 2 | commands: 3 | lint: 4 | run: deno lint 5 | fmt: 6 | run: deno fmt 7 | -------------------------------------------------------------------------------- /plugin/gin-branch.vim: -------------------------------------------------------------------------------- 1 | if exists('g:loaded_gin_branch') 2 | finish 3 | endif 4 | let g:loaded_gin_branch = 1 5 | 6 | augroup gin_plugin_branch_internal 7 | autocmd! 8 | autocmd BufReadCmd ginbranch://* 9 | \ call denops#request('gin', 'branch:edit', [bufnr(), expand('')]) 10 | augroup END 11 | 12 | function! s:command(bang, mods, args) abort 13 | if denops#plugin#wait('gin') 14 | return 15 | endif 16 | call denops#request('gin', 'branch:command', [a:bang, a:mods, a:args]) 17 | endfunction 18 | 19 | command! -bang -bar -nargs=* GinBranch call s:command(, , []) 20 | -------------------------------------------------------------------------------- /plugin/gin-browse.vim: -------------------------------------------------------------------------------- 1 | if exists('g:loaded_gin_browse') 2 | finish 3 | endif 4 | let g:loaded_gin_browse = 1 5 | 6 | function! s:command(args, range, range_given) abort 7 | let l:Callback = function('denops#notify', [ 8 | \ 'gin', 9 | \ 'browse:command', 10 | \ a:range_given ? [a:args, a:range] : [a:args], 11 | \]) 12 | call denops#plugin#wait_async('gin', l:Callback) 13 | endfunction 14 | 15 | command! -bar -range=0 -nargs=* GinBrowse call s:command([], [, ], ) 16 | -------------------------------------------------------------------------------- /plugin/gin-cd.vim: -------------------------------------------------------------------------------- 1 | if exists('g:loaded_gin_cd') 2 | finish 3 | endif 4 | let g:loaded_gin_cd = 1 5 | 6 | function! s:command(cmd) abort 7 | if denops#plugin#wait('gin') 8 | return 9 | endif 10 | try 11 | let worktree = gin#util#worktree() 12 | execute printf('%s %s', a:cmd, fnameescape(worktree)) 13 | echo printf('[gin] %s on "%s"', a:cmd, worktree) 14 | catch 15 | echohl WarningMsg 16 | echo '[gin] no git repository found' 17 | echohl None 18 | endtry 19 | endfunction 20 | 21 | command! -bar -nargs=0 GinCd call s:command('cd') 22 | command! -bar -nargs=0 GinLcd call s:command('lcd') 23 | command! -bar -nargs=0 GinTcd call s:command('tcd') 24 | -------------------------------------------------------------------------------- /plugin/gin-chaperon.vim: -------------------------------------------------------------------------------- 1 | if exists('g:loaded_gin_chaperon') 2 | finish 3 | endif 4 | let g:loaded_gin_chaperon = 1 5 | 6 | function! s:command(bang, mods, args) abort 7 | if denops#plugin#wait('gin') 8 | return 9 | endif 10 | call denops#request('gin', 'chaperon:command', [a:bang, a:mods, a:args]) 11 | endfunction 12 | 13 | command! -bang -bar -nargs=* GinChaperon call s:command(, , []) 14 | -------------------------------------------------------------------------------- /plugin/gin-diff.vim: -------------------------------------------------------------------------------- 1 | if exists('g:loaded_gin_diff') 2 | finish 3 | endif 4 | let g:loaded_gin_diff = 1 5 | 6 | augroup gin_plugin_diff_internal 7 | autocmd! 8 | autocmd BufReadCmd gindiff://* 9 | \ call denops#request('gin', 'diff:edit', [bufnr(), expand('')]) 10 | autocmd FileReadCmd gindiff://* 11 | \ call denops#request('gin', 'diff:read', [bufnr(), expand('')]) 12 | augroup END 13 | 14 | function! s:command(bang, mods, args) abort 15 | if denops#plugin#wait('gin') 16 | return 17 | endif 18 | call denops#request('gin', 'diff:command', [a:bang, a:mods, a:args]) 19 | endfunction 20 | 21 | command! -bang -bar -nargs=* GinDiff call s:command(, , []) 22 | -------------------------------------------------------------------------------- /plugin/gin-edit.vim: -------------------------------------------------------------------------------- 1 | if exists('g:loaded_gin_edit') 2 | finish 3 | endif 4 | let g:loaded_gin_edit = 1 5 | 6 | augroup gin_plugin_edit_internal 7 | autocmd! 8 | autocmd BufReadCmd ginedit://* 9 | \ call denops#request('gin', 'edit:edit', [bufnr(), expand('')]) 10 | autocmd FileReadCmd ginedit://* 11 | \ call denops#request('gin', 'edit:read', [bufnr(), expand('')]) 12 | augroup END 13 | 14 | function! s:command(bang, mods, args) abort 15 | if denops#plugin#wait('gin') 16 | return 17 | endif 18 | call denops#request('gin', 'edit:command', [a:bang, a:mods, a:args]) 19 | endfunction 20 | 21 | command! -bang -bar -nargs=* GinEdit call s:command(, , []) 22 | -------------------------------------------------------------------------------- /plugin/gin-log.vim: -------------------------------------------------------------------------------- 1 | if exists('g:loaded_gin_log') 2 | finish 3 | endif 4 | let g:loaded_gin_log = 1 5 | 6 | augroup gin_plugin_log_internal 7 | autocmd! 8 | autocmd BufReadCmd ginlog://* 9 | \ call denops#request('gin', 'log:edit', [bufnr(), expand('')]) 10 | autocmd FileReadCmd ginlog://* 11 | \ call denops#request('gin', 'log:read', [bufnr(), expand('')]) 12 | augroup END 13 | 14 | function! s:command(bang, mods, args) abort 15 | if denops#plugin#wait('gin') 16 | return 17 | endif 18 | call denops#request('gin', 'log:command', [a:bang, a:mods, a:args]) 19 | endfunction 20 | 21 | command! -bang -bar -nargs=* GinLog call s:command(, , []) 22 | -------------------------------------------------------------------------------- /plugin/gin-patch.vim: -------------------------------------------------------------------------------- 1 | if exists('g:loaded_gin_patch') 2 | finish 3 | endif 4 | let g:loaded_gin_patch = 1 5 | 6 | function! s:command(bang, mods, args) abort 7 | if denops#plugin#wait('gin') 8 | return 9 | endif 10 | call denops#request('gin', 'patch:command', [a:bang, a:mods, a:args]) 11 | endfunction 12 | 13 | command! -bang -bar -nargs=* GinPatch call s:command(, , []) 14 | -------------------------------------------------------------------------------- /plugin/gin-status.vim: -------------------------------------------------------------------------------- 1 | if exists('g:loaded_gin_status') 2 | finish 3 | endif 4 | let g:loaded_gin_status = 1 5 | 6 | augroup gin_plugin_status_internal 7 | autocmd! 8 | autocmd BufReadCmd ginstatus://* 9 | \ call denops#request('gin', 'status:edit', [bufnr(), expand('')]) 10 | augroup END 11 | 12 | function! s:command(bang, mods, args) abort 13 | if denops#plugin#wait('gin') 14 | return 15 | endif 16 | call denops#request('gin', 'status:command', [a:bang, a:mods, a:args]) 17 | endfunction 18 | 19 | command! -bang -bar -nargs=* GinStatus call s:command(, , []) 20 | -------------------------------------------------------------------------------- /plugin/gin.vim: -------------------------------------------------------------------------------- 1 | if exists('g:loaded_gin') 2 | finish 3 | endif 4 | let g:loaded_gin = 1 5 | 6 | augroup gin_plugin_internal 7 | autocmd! 8 | autocmd User GinCommandPost : 9 | autocmd User GinComponentPost : 10 | autocmd BufReadCmd gin://* 11 | \ call denops#request('gin', 'buffer:edit', [bufnr(), expand('')]) 12 | autocmd FileReadCmd gin://* 13 | \ call denops#request('gin', 'buffer:read', [bufnr(), expand('')]) 14 | augroup END 15 | 16 | function! s:command(bang, mods, args) abort 17 | let args = filter(copy(a:args), { -> v:val !=# '++wait' }) 18 | if index(a:args, '++wait') isnot# -1 19 | if denops#plugin#wait('gin') 20 | return 21 | endif 22 | call denops#request('gin', 'command', [a:mods, args]) 23 | else 24 | let l:Callback = function('denops#notify', [ 25 | \ 'gin', 26 | \ 'command', 27 | \ [a:mods, args], 28 | \]) 29 | call denops#plugin#wait_async('gin', l:Callback) 30 | endif 31 | endfunction 32 | 33 | command! -bar -nargs=* Gin call s:command(, , []) 34 | 35 | function! s:buffer_command(bang, mods, args) abort 36 | if denops#plugin#wait('gin') 37 | return 38 | endif 39 | call denops#request('gin', 'buffer:command', [a:bang, a:mods, a:args]) 40 | endfunction 41 | 42 | command! -bar -nargs=* GinBuffer call s:buffer_command(, , []) 43 | --------------------------------------------------------------------------------