diff --git a/autoload/fugitive.vim b/autoload/fugitive.vim index fae43c8..9f82733 100644 --- a/autoload/fugitive.vim +++ b/autoload/fugitive.vim @@ -1481,6 +1481,12 @@ function! fugitive#BufReadStatus() abort endfor endif + let b:fugitive_diff = { + \ 'Staged': split(system(fugitive#Prepare('diff', '--no-ext-diff', '--no-prefix', '--cached', '--')), "\n"), + \ 'Unstaged': split(system(fugitive#Prepare('diff', '--no-ext-diff', '--no-prefix', '--')), "\n")} + let expanded = get(b:, 'fugitive_expanded', {'Staged': {}, 'Unstaged': {}}) + let b:fugitive_expanded = {'Staged': {}, 'Unstaged': {}} + silent keepjumps %delete_ call s:AddHeader('Head', head) @@ -1490,7 +1496,9 @@ function! fugitive#BufReadStatus() abort endif call s:AddSection('Rebasing ' . rebasing_head, rebasing) call s:AddSection('Unstaged', unstaged) + let unstaged_end = len(unstaged) ? line('$') : 0 call s:AddSection('Staged', staged) + let staged_end = len(staged) ? line('$') : 0 call s:AddSection('Unpushed to ' . push, unpushed) call s:AddSection('Unpulled from ' . pull, unpulled) @@ -1522,6 +1530,14 @@ function! fugitive#BufReadStatus() abort nnoremap cw :Gcommit --amend --only nnoremap cva :Gcommit -v --amend nnoremap cvc :Gcommit -v + nnoremap a :execute StageInline('toggle',line('.'),v:count) + nnoremap i :execute StageInline('toggle',line('.'),v:count) + exe 'nnoremap ' nowait "= :execute StageInline('toggle',line('.'),v:count)" + exe 'nnoremap ' nowait "< :execute StageInline('show', line('.'),v:count)" + exe 'nnoremap ' nowait "> :execute StageInline('hide', line('.'),v:count)" + exe 'xnoremap ' nowait "= :execute StageInline('toggle',line(\"'<\"),line(\"'>\")-line(\"'<\")+1)" + exe 'xnoremap ' nowait "< :execute StageInline('show', line(\"'<\"),line(\"'>\")-line(\"'<\")+1)" + exe 'xnoremap ' nowait "> :execute StageInline('hide', line(\"'<\"),line(\"'>\")-line(\"'<\")+1)" nnoremap D :execute StageDiff('Gdiff') nnoremap dd :execute StageDiff('Gdiff') nnoremap dh :execute StageDiff('Gsdiff') @@ -1537,9 +1553,20 @@ function! fugitive#BufReadStatus() abort nnoremap R :exe ReloadStatus() nnoremap U :echoerr 'Changed to g' nnoremap g :execute StageUndo() - nnoremap . : =fnameescape(StatusCfile()) + nnoremap . : =fnameescape(get(StatusCfile(),0,'')) nnoremap g? :help fugitive-:Gstatus nnoremap :help fugitive-:Gstatus + + for [lnum, section] in [[staged_end, 'Staged'], [unstaged_end, 'Unstaged']] + while len(getline(lnum)) + let filename = matchstr(getline(lnum), '^[A-Z?] \zs.*') + if has_key(expanded[section], filename) + call s:StageInline('show', lnum) + endif + let lnum -= 1 + endwhile + endfor + return '' catch /^fugitive:/ return 'echoerr v:errmsg' @@ -1867,10 +1894,71 @@ function! s:Status(bang, count, mods) abort return '' endfunction -function! s:ReloadStatus() abort - let pos = getpos('.') +function! s:StageSeek(info, fallback) abort + let info = a:info + if empty(info.section) + return a:fallback + endif + let line = search('^' . info.section, 'wn') + if !line + for section in get({'Staged': ['Unstaged'], 'Unstaged': ['Staged']}, info.section, []) + let line = search('^' . section, 'wn') + if line + return line + (info.index > 0 ? 1 : 0) + endif + endfor + return 1 + endif + let i = 0 + while len(getline(line)) + let filename = matchstr(getline(line), '^[A-Z?] \zs.*') + if len(filename) && + \ ((info.filename[-1:-1] ==# '/' && filename[0 : len(info.filename) - 1] ==# info.filename) || + \ (filename[-1:-1] ==# '/' && filename ==# info.filename[0 : len(filename) - 1]) || + \ filename ==# info.filename) + if info.offset < 0 + return line + else + if getline(line+1) !~# '^@' + exe s:StageInline('show', line) + endif + if getline(line+1) !~# '^@' + return line + endif + let type = info.sigil ==# '-' ? '-' : '+' + let offset = -1 + while offset < info.offset + let line += 1 + if getline(line) =~# '^@' + let offset = +matchstr(getline(line), type . '\zs\d\+') - 1 + elseif getline(line) =~# '^[ ' . type . ']' + let offset += 1 + elseif getline(line) !~# '^[ @+-]' + return line - 1 + endif + endwhile + return line + endif + endif + let commit = matchstr(getline(line), '^\%(\%(\x\x\x\)\@!\l\+\s\+\)\=\zs[0-9a-f]\+') + if len(commit) && commit ==# info.commit + return line + endif + if i ==# info.index + let backup = line + endif + let i += getline(line) !~# '^[ @+-]' + let line += 1 + endwhile + return exists('backup') ? backup : line - 1 +endfunction + +function! s:ReloadStatus(...) abort + let original_lnum = a:0 ? a:1 : line('.') + let info = s:StageInfo(original_lnum) call fugitive#BufReadStatus() - call setpos('.', pos) + exe s:StageSeek(info, original_lnum) + normal! 0 return '' endfunction @@ -1910,28 +1998,44 @@ endfunction function! s:StageInfo(...) abort let lnum = a:0 ? a:1 : line('.') + let sigil = matchstr(getline('.'), '^[ @+-]') + let offset = -1 + if getline(lnum) =~# '^[ @+-]' + let type = sigil ==# '-' ? '-' : '+' + while lnum > 0 && getline(lnum) !~# '^@' + if getline(lnum) =~# '^[ '.type.']' + let offset += 1 + endif + let lnum -= 1 + endwhile + let offset += matchstr(getline(lnum), type.'\zs\d\+') + while getline(lnum) =~# '^[ @+-]' + let lnum -= 1 + endwhile + endif let slnum = lnum + 1 let section = '' + let index = 0 while len(getline(slnum - 1)) && empty(section) let slnum -= 1 let section = matchstr(getline(slnum), '^\u\l\+\ze.* (\d\+)$') + if empty(section) && getline(slnum) !~# '^[ @+-]' + let index += 1 + endif endwhile return {'section': section, \ 'heading': getline(slnum), + \ 'sigil': sigil, + \ 'offset': offset, \ 'filename': matchstr(getline(lnum), '^[A-Z?] \zs.*'), \ 'commit': matchstr(getline(lnum), '^\%(\%(\x\x\x\)\@!\l\+\s\+\)\=\zs[0-9a-f]\{4,\}\ze '), \ 'status': matchstr(getline(lnum), '^[A-Z?]\ze \|^\%(\x\x\x\)\@!\l\+\ze [0-9a-f]'), - \ 'index': lnum - slnum} -endfunction - -function! s:StageFileSection(lnum) abort - let info = s:StageInfo(a:lnum) - return [info.filename, info.section] + \ 'index': index} endfunction function! s:StageNext(count) abort for i in range(a:count) - call search('^[A-Z?] .\|^[0-9a-f]\{4,\} ','W') + call search('^[A-Z?] .\|^[0-9a-f]\{4,\} \|^@','W') endfor return '.' endfunction @@ -1941,35 +2045,15 @@ function! s:StagePrevious(count) abort return 'CtrlP '.fnameescape(s:Tree()) else for i in range(a:count) - call search('^[A-Z?] .\|^[0-9a-f]\{4,\} ','Wbe') + call search('^[A-Z?] .\|^[0-9a-f]\{4,\} \|^@','Wbe') endfor return '.' endif endfunction function! s:StageReloadSeek(target,lnum1,lnum2) abort - let jump = a:target - let target = s:StageFileSection(a:lnum2 + 1) - if empty(target[0]) - let target = s:StageFileSection(a:lnum1 - 1) - endif - if empty(target[0]) - let target = a:target - endif - call s:ReloadStatus() - 1 - redraw - let lnum = 0 - while lnum < line('$') - let lnum += 1 - let file = getline(lnum)[2:-1] - if (target[0][-1:-1] ==# '/' && file[0 : len(target[0]) - 1] ==# target[0]) || - \ (file[-1:-1] ==# '/' && file ==# target[0][0 : len(file) - 1]) || - \ file ==# target[0] - exe lnum - return '' - endif - endwhile + exe s:ReloadStatus(a:lnum1) + return '' endfunction function! s:StageUndo() abort @@ -1995,28 +2079,108 @@ function! s:StageUndo() abort endif endfunction +function! s:StageInline(mode, ...) abort + let lnum1 = a:0 ? a:1 : line('.') + let lnum = lnum1 + 1 + if a:0 > 1 && a:2 == 0 + let info = s:StageInfo(lnum - 1) + if empty(info.filename) && len(info.section) + while len(getline(lnum)) + let lnum += 1 + endwhile + endif + elseif a:0 > 1 + let lnum += a:2 - 1 + endif + while lnum > lnum1 + let lnum -= 1 + while lnum > 0 && getline(lnum) =~# '^[ @+-]' + let lnum -= 1 + endwhile + let info = s:StageInfo(lnum) + if !has_key(b:fugitive_diff, info.section) + continue + endif + if getline(lnum + 1) =~# '^[ @+-]' + let lnum2 = lnum + 1 + while getline(lnum2 + 1) =~# '^[ @+-]' + let lnum2 += 1 + endwhile + if a:mode !=# 'show' + setlocal modifiable noreadonly + exe 'silent keepjumps ' . (lnum + 1) . ',' . lnum2 . 'delete _' + call remove(b:fugitive_expanded[info.section], info.filename) + setlocal nomodifiable readonly nomodified + endif + continue + endif + if !has_key(b:fugitive_diff, info.section) || info.status !~# '^[ADM]$' || a:mode ==# 'hide' + continue + endif + let mode = '' + let diff = [] + let index = 0 + let start = -1 + for line in b:fugitive_diff[info.section] + if mode ==# 'await' && line[0] ==# '@' + let mode = 'capture' + endif + if line[0] ==# 'd' + if len(diff) + break + endif + let start = index + let mode = 'head' + elseif mode ==# 'head' && line ==# '--- ' . info.filename + let mode = 'await' + elseif mode ==# 'head' && line ==# '+++ ' . info.filename + let mode = 'await' + elseif mode ==# 'capture' + call add(diff, line) + elseif line[0] ==# '@' + let mode = '' + endif + let index += 1 + endfor + if len(diff) + setlocal modifiable noreadonly + silent call append(lnum, diff) + let b:fugitive_expanded[info.section][info.filename] = [start, len(diff)] + setlocal nomodifiable readonly nomodified + endif + endwhile + return lnum +endfunction + function! s:StageDiff(diff) abort - let [filename, section] = s:StageFileSection(line('.')) - if filename ==# '' && section ==# 'Staged' + let lnum = line('.') + let info = s:StageInfo(lnum) + let prefix = info.offset > 0 ? '+' . info.offset : '' + if empty(info.filename) && info.section ==# 'Staged' return 'Git! diff --no-ext-diff --cached' - elseif filename ==# '' + elseif empty(info.filename) return 'Git! diff --no-ext-diff' - elseif filename =~# ' -> ' - let [old, new] = split(filename,' -> ') - execute 'Gedit '.s:fnameescape(':0:'.new) + elseif info.filename =~# ' -> ' + let [old, new] = split(info.filename,' -> ') + execute 'Gedit' . prefix s:fnameescape(':0:'.new) return a:diff.' HEAD:'.s:fnameescape(old) - elseif section ==# 'Staged' - execute 'Gedit '.s:fnameescape(':0:'.filename) - return a:diff.' -' + elseif info.section ==# 'Staged' && info.sigil ==# '-' + execute 'Gedit' prefix s:fnameescape('@:'.info.filename) + return a:diff.'! :0' + elseif info.section ==# 'Staged' + execute 'Gedit' prefix s:fnameescape(':0:'.info.filename) + return a:diff . (info.sigil ==# '+' ? '!' : '') . ' -' + elseif info.sigil ==# '-' + execute 'Gedit' prefix s:fnameescape(':0:'.info.filename) + return a:diff . '!' else - execute 'Gedit '.s:fnameescape(':(top)'.filename) - return a:diff + execute 'Gedit' prefix s:fnameescape(':(top)'.info.filename) + return a:diff . (info.sigil ==# '+' ? '!' : '') endif endfunction function! s:StageDiffEdit() abort let info = s:StageInfo(line('.')) - let [filename, section] = s:StageFileSection(line('.')) let arg = (empty(info.filename) ? '.' : info.filename) if info.section ==# 'Staged' return 'Git! diff --no-ext-diff --cached '.s:shellesc(arg) @@ -2029,7 +2193,7 @@ function! s:StageDiffEdit() abort call search('^Staged','W') endif else - call s:StageReloadSeek([filename, 'Staged'], line('.'), line('.')) + call s:StageReloadSeek([info.filename, 'Staged'], line('.'), line('.')) endif return '' else @@ -2107,24 +2271,21 @@ function! s:StagePatch(lnum1,lnum2) abort let reset = [] for lnum in range(a:lnum1,a:lnum2) - let [filename, section] = s:StageFileSection(lnum) - if empty(filename) && section ==# 'Staged' + let info = s:StageInfo(lnum) + if empty(info.filename) && info.section ==# 'Staged' return 'Git reset --patch' - elseif empty(filename) && section ==# 'Unstaged' + elseif empty(info.filename) && info.section ==# 'Unstaged' return 'Git add --patch' - elseif filename ==# '' + elseif info.filename ==# '' continue endif - if !exists('first_filename') - let first_filename = filename - endif execute lnum - if filename =~ ' -> ' - let reset += [split(filename,' -> ')[1]] - elseif section ==# 'Staged' - let reset += [filename] - elseif getline(lnum) !~# '^.\=\tdeleted:' - let add += [filename] + if info.filename =~ ' -> ' + let reset += [split(info.filename,' -> ')[1]] + elseif info.section ==# 'Staged' + let reset += [info.filename] + elseif info.status !~# '^D' + let add += [info.filename] endif endfor try @@ -2134,16 +2295,10 @@ function! s:StagePatch(lnum1,lnum2) abort if !empty(reset) execute "Git reset --patch -- ".join(map(reset,'s:shellesc(v:val)')) endif - if exists('first_filename') - silent! edit! - 1 - redraw - call search('^[A-Z?] \V'.first_filename.'\$','W') - endif catch /^fugitive:/ return 'echoerr v:errmsg' endtry - return 'checktime' + return s:ReloadStatus() endfunction " Section: :Gcommit @@ -3801,22 +3956,29 @@ endfunction function! s:StatusCfile(...) abort let tree = FugitiveTreeForGitDir(b:git_dir) let lead = s:cpath(tree, getcwd()) ? './' : tree . '/' + let info = s:StageInfo() let line = getline('.') - if line =~# '^\S ' - return lead . line[2:-1] - elseif line =~# '^[0-9a-f]\{4,\}\s' - return matchstr(line, '^\S\+') - elseif line =~# '^\l\+\s\+[0-9a-f]\{4,\}\s' - return matchstr(line, '^\l\+\s\+\zs\S\+') + if len(info.sigil) && len(info.section) && len(info.filename) + if info.section ==# 'Unstaged' && info.sigil !=# '-' + return [lead . info.filename, info.offset, 'normal!zv'] + elseif info.section ==# 'Staged' && info.sigil ==# '-' + return ['@:' . info.filename, info.offset, 'normal!zv'] + else + return [':0:' . info.filename, info.offset, 'normal!zv'] + endif + elseif len(info.filename) + return [lead . info.filename] + elseif len(info.commit) + return [info.commit] elseif line =~# '^\%(Head\|Merge\|Rebase\|Upstream\|Pull\|Push\): ' - return matchstr(line, ' \zs.*') + return [matchstr(line, ' \zs.*')] else - return '' + return [''] endif endfunction function! fugitive#StatusCfile() abort - let file = s:Generate(s:StatusCfile()) + let file = s:Generate(s:StatusCfile()[0]) return empty(file) ? fugitive#Cfile() : s:fnameescape(file) endfunction @@ -4003,7 +4165,7 @@ endfunction function! s:GF(mode) abort try - let results = &filetype ==# 'fugitive' ? [s:StatusCfile()] : &filetype ==# 'gitcommit' ? [s:MessageCfile()] : s:cfile() + let results = &filetype ==# 'fugitive' ? s:StatusCfile() : &filetype ==# 'gitcommit' ? [s:MessageCfile()] : s:cfile() catch /^fugitive:/ return 'echoerr v:errmsg' endtry diff --git a/doc/fugitive.txt b/doc/fugitive.txt index 3e86781..c675f95 100644 --- a/doc/fugitive.txt +++ b/doc/fugitive.txt @@ -49,6 +49,9 @@ that are part of Git repositories). cf |:Gcommit| --fixup= cs |:Gcommit| --squash= cA |:Gcommit| --edit --squash= + = toggle inline diff + < show inline diff + > hide inline diff D |:Gdiff| ds |:Gsdiff| dp |:Git!| diff (p for patch; use :Gw to apply) diff --git a/syntax/fugitive.vim b/syntax/fugitive.vim index 1562f8b..e2389b5 100644 --- a/syntax/fugitive.vim +++ b/syntax/fugitive.vim @@ -22,7 +22,7 @@ syn match FugitiveSymbolicRef /\.\@!\%(\.\.\@!\|[^[:space:][:cntrl:]\:.]\)\+\.\@ syn match fugitiveHash /^\x\{4,\}\>/ contained containedin=fugitiveSection syn match fugitiveHash /\<\x\{4,\}\>/ contained -syn region fugitiveHunk start=/^\%(@@ -\)\@=/ end=/^\%(diff --\%(git\|cc\|combined\) \|@@\|$\)\@=/ contains=@fugitiveDiff containedin=fugitiveSection fold +syn region fugitiveHunk start=/^\%(@@ -\)\@=/ end=/^\%([A-Za-z?@]\|$\)\@=/ contains=@fugitiveDiff containedin=fugitiveSection fold hi def link fugitiveHeader Label hi def link fugitiveHeading PreProc