From f3d20999d57ea1277355503961ea5b9d02fa9336 Mon Sep 17 00:00:00 2001 From: David Briscoe Date: Mon, 23 Dec 2019 15:17:36 -0800 Subject: [PATCH 1/8] Move diffexchange code to autoload --- autoload/mergetool.vim | 51 ++++++++++++++++++++++++++++++++ plugin/mergetool.vim | 66 +++++++----------------------------------- 2 files changed, 61 insertions(+), 56 deletions(-) diff --git a/autoload/mergetool.vim b/autoload/mergetool.vim index 2cb1c0a..c6d6cc9 100644 --- a/autoload/mergetool.vim +++ b/autoload/mergetool.vim @@ -223,6 +223,57 @@ endfunction " }}} " }}} +" Diff exchange {{{ + +" Do either diffget or diffput, depending on given direction +" and whether the window has adjacent window in a given direction +" h| + window on right = diffget from right win +" h| + no window on right = diffput to left win +" l| + window on left = diffget from left win +" l| + no window on left = diffput to right win +" Same logic applies for vertical directions: 'j' and 'k' + +let s:directions = { + \ 'h': 'l', + \ 'l': 'h', + \ 'j': 'k', + \ 'k': 'j' } + +function mergetool#DiffExchange(dir) + let oppdir = s:directions[a:dir] + + let winoppdir = s:FindWindowOnDir(oppdir) + if (winoppdir != -1) + execute "diffget " . winbufnr(winoppdir) + else + let windir = s:FindWindowOnDir(a:dir) + if (windir != -1) + execute "diffput " . winbufnr(windir) + else + echohl WarningMsg + echo 'Cannot exchange diff. Found only single window' + echohl None + endif + endif +endfunction + +" Finds window in given direction and returns it win number +" If no window found, returns -1 +function s:FindWindowOnDir(dir) + let oldwin = winnr() + + execute "noautocmd wincmd " . a:dir + let curwin = winnr() + if (oldwin != curwin) + noautocmd wincmd p + return curwin + else + return -1 + endif +endfunction + +" }}} + " Private functions{{{ let s:markers = { diff --git a/plugin/mergetool.vim b/plugin/mergetool.vim index 522d260..83e4b78 100644 --- a/plugin/mergetool.vim +++ b/plugin/mergetool.vim @@ -7,6 +7,7 @@ let g:loaded_mergetool = 1 let g:mergetool_in_merge_mode = 0 +" Commands and mappings for mergetool state command! -nargs=0 MergetoolStart call mergetool#start() command! -nargs=0 MergetoolStop call mergetool#stop() command! -nargs=0 MergetoolToggle call mergetool#toggle() @@ -17,64 +18,17 @@ command! -nargs=0 MergetoolPreferRemote call mergetool#prefer_revision('remote') nnoremap (MergetoolToggle) :call mergetool#toggle() -" {{{ Diff exchange -" Do either diffget or diffput, depending on given direction -" and whether the window has adjacent window in a given direction -" h| + window on right = diffget from right win -" h| + no window on right = diffput to left win -" l| + window on left = diffget from left win -" l| + no window on left = diffput to right win -" Same logic applies for vertical directions: 'j' and 'k' - -let s:directions = { - \ 'h': 'l', - \ 'l': 'h', - \ 'j': 'k', - \ 'k': 'j' } - -function s:DiffExchange(dir) - let oppdir = s:directions[a:dir] - - let winoppdir = s:FindWindowOnDir(oppdir) - if (winoppdir != -1) - execute "diffget " . winbufnr(winoppdir) - else - let windir = s:FindWindowOnDir(a:dir) - if (windir != -1) - execute "diffput " . winbufnr(windir) - else - echohl WarningMsg - echo 'Cannot exchange diff. Found only single window' - echohl None - endif - endif -endfunction - -" Finds window in given direction and returns it win number -" If no window found, returns -1 -function s:FindWindowOnDir(dir) - let oldwin = winnr() - - execute "noautocmd wincmd " . a:dir - let curwin = winnr() - if (oldwin != curwin) - noautocmd wincmd p - return curwin - else - return -1 - endif -endfunction " Commands and mappings for diff exchange commands -command! -nargs=0 MergetoolDiffExchangeLeft call s:DiffExchange('h') -command! -nargs=0 MergetoolDiffExchangeRight call s:DiffExchange('l') -command! -nargs=0 MergetoolDiffExchangeDown call s:DiffExchange('j') -command! -nargs=0 MergetoolDiffExchangeUp call s:DiffExchange('k') - -nnoremap (MergetoolDiffExchangeLeft) :call DiffExchange('h') -nnoremap (MergetoolDiffExchangeRight) :call DiffExchange('l') -nnoremap (MergetoolDiffExchangeDown) :call DiffExchange('j') -nnoremap (MergetoolDiffExchangeUp) :call DiffExchange('k') +command! -nargs=0 MergetoolDiffExchangeLeft call mergetool#DiffExchange('h') +command! -nargs=0 MergetoolDiffExchangeRight call mergetool#DiffExchange('l') +command! -nargs=0 MergetoolDiffExchangeDown call mergetool#DiffExchange('j') +command! -nargs=0 MergetoolDiffExchangeUp call mergetool#DiffExchange('k') + +nnoremap (MergetoolDiffExchangeLeft) :call mergetool#DiffExchange('h') +nnoremap (MergetoolDiffExchangeRight) :call mergetool#DiffExchange('l') +nnoremap (MergetoolDiffExchangeDown) :call mergetool#DiffExchange('j') +nnoremap (MergetoolDiffExchangeUp) :call mergetool#DiffExchange('k') " }}} From b158916205bcda928bbfe2645e0964fe4d836070 Mon Sep 17 00:00:00 2001 From: David Briscoe Date: Mon, 23 Dec 2019 15:22:24 -0800 Subject: [PATCH 2/8] Hide most commands when not merging It doesn't make sense to adjust our layout unless we're merging, so only define these commands while merge is active. This makes it easier to find the right mergetool commands. --- autoload/mergetool.vim | 18 ++++++++++++++++++ plugin/mergetool.vim | 11 ++++------- 2 files changed, 22 insertions(+), 7 deletions(-) diff --git a/autoload/mergetool.vim b/autoload/mergetool.vim index c6d6cc9..20eb4dc 100644 --- a/autoload/mergetool.vim +++ b/autoload/mergetool.vim @@ -52,8 +52,25 @@ function! mergetool#start() "{{{ call mergetool#prefer_revision(g:mergetool_prefer_revision) call mergetool#set_layout(g:mergetool_layout) + call mergetool#bind_commands() endfunction "}}} +function! mergetool#bind_commands() + command! -nargs=0 MergetoolStop call mergetool#stop() + command! -nargs=1 MergetoolSetLayout call mergetool#set_layout() + command! -nargs=1 MergetoolToggleLayout call mergetool#toggle_layout() + command! -nargs=0 MergetoolPreferLocal call mergetool#prefer_revision('local') + command! -nargs=0 MergetoolPreferRemote call mergetool#prefer_revision('remote') +endf + +function! mergetool#unbind_commands() + delcommand MergetoolStop + delcommand MergetoolSetLayout + delcommand MergetoolToggleLayout + delcommand MergetoolPreferLocal + delcommand MergetoolPreferRemote +endf + " Stop mergetool effect depends on: " - when run as 'git mergetool' " - when run from Vim directly on file with conflict markers @@ -104,6 +121,7 @@ function! mergetool#stop() " {{{ endif let g:mergetool_in_merge_mode = 0 + call mergetool#unbind_commands() tabclose endif endfunction " }}} diff --git a/plugin/mergetool.vim b/plugin/mergetool.vim index 83e4b78..f932375 100644 --- a/plugin/mergetool.vim +++ b/plugin/mergetool.vim @@ -7,20 +7,17 @@ let g:loaded_mergetool = 1 let g:mergetool_in_merge_mode = 0 -" Commands and mappings for mergetool state +" Commands and mappings for mergetool state. Additional commands +" available during merging. command! -nargs=0 MergetoolStart call mergetool#start() -command! -nargs=0 MergetoolStop call mergetool#stop() command! -nargs=0 MergetoolToggle call mergetool#toggle() -command! -nargs=1 MergetoolSetLayout call mergetool#set_layout() -command! -nargs=1 MergetoolToggleLayout call mergetool#toggle_layout() -command! -nargs=0 MergetoolPreferLocal call mergetool#prefer_revision('local') -command! -nargs=0 MergetoolPreferRemote call mergetool#prefer_revision('remote') nnoremap (MergetoolToggle) :call mergetool#toggle() -" Commands and mappings for diff exchange commands +" Commands and mappings for diff exchange commands. These can be used +" outside of merging (in any diff windows). command! -nargs=0 MergetoolDiffExchangeLeft call mergetool#DiffExchange('h') command! -nargs=0 MergetoolDiffExchangeRight call mergetool#DiffExchange('l') command! -nargs=0 MergetoolDiffExchangeDown call mergetool#DiffExchange('j') From 34565185d21e60ecede66d0425c5f6a86d57136b Mon Sep 17 00:00:00 2001 From: David Briscoe Date: Mon, 23 Dec 2019 15:30:51 -0800 Subject: [PATCH 3/8] Add autocmds for defining commands when merging Allow users to define their own Mergetool commands. The best example I have is `:MergetoolDiffWithRemote` as an alias for `:MergetoolToggleLayout rlm` --- autoload/mergetool.vim | 9 +++++++++ readme.md | 12 ++++++++++++ 2 files changed, 21 insertions(+) diff --git a/autoload/mergetool.vim b/autoload/mergetool.vim index 20eb4dc..630b28b 100644 --- a/autoload/mergetool.vim +++ b/autoload/mergetool.vim @@ -61,6 +61,7 @@ function! mergetool#bind_commands() command! -nargs=1 MergetoolToggleLayout call mergetool#toggle_layout() command! -nargs=0 MergetoolPreferLocal call mergetool#prefer_revision('local') command! -nargs=0 MergetoolPreferRemote call mergetool#prefer_revision('remote') + doautocmd User MergetoolStart endf function! mergetool#unbind_commands() @@ -69,8 +70,16 @@ function! mergetool#unbind_commands() delcommand MergetoolToggleLayout delcommand MergetoolPreferLocal delcommand MergetoolPreferRemote + doautocmd User MergetoolStop endf +" Dummy autocmds to prevent errors. +augroup mergetool_dummy + au! + autocmd User MergetoolStart let s:mergetool_dummy = 1 + autocmd User MergetoolStop let s:mergetool_dummy = 0 +augroup END + " Stop mergetool effect depends on: " - when run as 'git mergetool' " - when run from Vim directly on file with conflict markers diff --git a/readme.md b/readme.md index baa254c..9599ce9 100644 --- a/readme.md +++ b/readme.md @@ -383,6 +383,18 @@ let g:airline_section_z = airline#section#create(['_diffmerge', ...other_parts]) ![Status line indicator](./screenshots/airline_merge_indicator.png) + +You can run vimscript when mergemode begins and ends: + +```vim +augroup your_mergetool + au! + autocmd User MergetoolStart set nospell + autocmd User MergetoolStop set spell +augroup END +``` + + ### Quitting merge mode When exiting merge mode, `vim-mergetool` would prompt you whether merge was successful. If not, it will rollback changes to the buffer, will not save `MERGED` file to disk, and exit with non-zero code, when running as a `git mergetool`. From 416ed90365fb7e08cfec5fbcbf6112c65428cc61 Mon Sep 17 00:00:00 2001 From: David Briscoe Date: Wed, 15 Apr 2020 22:53:02 -0700 Subject: [PATCH 4/8] Make merged file hidden while changing layout Fix #12: We were assuming users had 'hidden' set. mergetool#start ends with two steps: prefer_revision and set_layout. prefer_revision modifies the current buffer to remove conflict markers. set_layout closes all but the first window in g:mergetool_layout and then shows the other windows in order. If a user has these combination of settings, closing the merge buffer's window will trigger an error: set nohidden let g:mergetool_layout = 'rm' We modify the merge buffer (without saving it -- so the user knows we made changes) and then we try to close it (because 'm' is not the first item in mergetool_layout), which triggers an "unsaved changes" error. Prevent the error by temporarily marking the merge buffer to hide while we're modifying the layout. Uses setbufvar (introduced to vim in 7.0) to clean up so if the user doesn't include 'm' in the layout, we don't fail to clean up. And in that case, the merge buffer is already hidden, so it doesn't cause unsaved errors until it's shown again. --- autoload/mergetool.vim | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/autoload/mergetool.vim b/autoload/mergetool.vim index 630b28b..b4797c4 100644 --- a/autoload/mergetool.vim +++ b/autoload/mergetool.vim @@ -176,6 +176,11 @@ function! mergetool#set_layout(layout) " {{{ let l:_winstate = winsaveview() endif + " Ensure merged file (which likely has unsaved conflict removal changes) can + " be hidden without error. + let bufhidden_bak = getbufvar(s:mergedfile_bufnr, '&bufhidden') + call setbufvar(s:mergedfile_bufnr, '&bufhidden', 'hide') + " Before changing layout, turn off diff mode in all visible windows windo diffoff @@ -225,6 +230,7 @@ function! mergetool#set_layout(layout) " {{{ if s:goto_win_with_merged_file() && exists('l:_winstate') call winrestview(l:_winstate) endif + call setbufvar(s:mergedfile_bufnr, '&bufhidden', bufhidden_bak) endfunction " }}} " Toggles between given and default layout From 649561f6a6eae6b8f1bcc72e067b450c90817284 Mon Sep 17 00:00:00 2001 From: David Briscoe Date: Wed, 15 Apr 2020 23:18:36 -0700 Subject: [PATCH 5/8] Support using as git-mergetool on win32 Fix buffer already exists error on win32. Cannot create a buffer called 'remote' if there's already one called 'REMOTE' because win32 is not case-sensitive. When we use as git-mergetool, there's always buffers called BASE, LOCAL, and REMOTE. So append a suffix to disambiguate. These buffers are derived by removing conflicts from the merge file, so I call them _derived. --- autoload/mergetool.vim | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/autoload/mergetool.vim b/autoload/mergetool.vim index b4797c4..f13288e 100644 --- a/autoload/mergetool.vim +++ b/autoload/mergetool.vim @@ -327,7 +327,13 @@ function! s:load_revision(revision) call s:remove_conflict_markers(a:revision) setlocal nomodifiable readonly buftype=nofile bufhidden=delete nobuflisted execute "setlocal filetype=" . s:mergedfile_filetype - execute "file " . a:revision + let bufname = a:revision + if s:run_as_git_mergetool && has('win32') + " Cannot create a buffer called 'remote' if there's already one called + " 'REMOTE' because win32 is not case-sensitive. + let bufname .= '_derived' + endif + execute "file " . bufname elseif a:revision ==# 'BASE' || a:revision ==# 'REMOTE' || a:revision ==# 'LOCAL' " First, if run as 'git mergetool', try find buffer by name: 'BASE|REMOTE|LOCAL' From b5aeee96ab59cdf454228062aaab1eff83b42175 Mon Sep 17 00:00:00 2001 From: David Briscoe Date: Wed, 15 Apr 2020 18:02:33 -0700 Subject: [PATCH 6/8] Support any scm by assigning args at startup Fix #13: add g:mergetool_args_order. Add g:mergetool_args_order to renames arguments as hidden files with the same names as git (BASE, REMOTE, LOCAL). This allows mergetool to behave as if invoked by git-mergetool for any scm. --- autoload/mergetool.vim | 43 ++++++++++++++++++++++++++++++++++++++++++ readme.md | 35 ++++++++++++++++++++++++++++++++++ 2 files changed, 78 insertions(+) diff --git a/autoload/mergetool.vim b/autoload/mergetool.vim index f13288e..70aab47 100644 --- a/autoload/mergetool.vim +++ b/autoload/mergetool.vim @@ -6,6 +6,7 @@ endfunction let g:mergetool_layout = get(g:, 'mergetool_layout', 'mr') let g:mergetool_prefer_revision = get(g:, 'mergetool_prefer_revision', 'local') let g:MergetoolSetLayoutCallback = get(g:, 'MergetoolSetLayoutCallback', function('s:noop')) +let g:mergetool_args_order = get(g:, 'mergetool_args_order', '') " {{{ Public exports @@ -38,6 +39,16 @@ function! mergetool#start() "{{{ let s:mergedfile_fileformat = &fileformat let s:mergedfile_filetype = &filetype + if !empty(g:mergetool_args_order) + let success = s:apply_args_order(s:mergedfile_bufnr, g:mergetool_args_order) + if !success + echohl WarningMsg + echo "g:mergetool_args_order didn't use the current file as MERGED. Ensure you're using the order as seen in :args." + echohl None + return + endif + endif + " Detect if we're run as 'git mergetool' by presence of BASE|LOCAL|REMOTE buf names let s:run_as_git_mergetool = bufnr('BASE') != -1 && \ bufnr('LOCAL') != -1 && @@ -144,6 +155,38 @@ function! mergetool#toggle() " {{{ endif endfunction " }}} +" Create hidden buffers that use git's special buffer names to support any +" scm. We never create a MERGED buffer. Instead, return it so we can validate +" it's as expected. +function! s:apply_args_order(merged_bufnr, arg_order) " {{{ + let abbrevs = { + \ 'M': 'MERGED', + \ 'B': 'BASE', + \ 'R': 'REMOTE', + \ 'L': 'LOCAL' } + + let i = 1 + for labbr in split(a:arg_order, '\zs') + if labbr ==# 'M' + let current_arg_bufnr = bufnr(argv(i - 1)) + if a:merged_bufnr != current_arg_bufnr + " Fail -- input merged buffer number doesn't match arg order. + return 0 + endif + else + execute 'silent' i 'argument' + execute 'silent file' abbrevs[labbr] + setlocal buftype=nofile + setlocal bufhidden=hide + endif + let i += 1 + endfor + + execute "buffer " . a:merged_bufnr + " Success + return 1 +endfunction " }}} + " Opens set of windows with merged file and various file revisions " Supported layout options: " - w, 'MERGED' revision as passed by Git, or working tree version of merged file diff --git a/readme.md b/readme.md index 9599ce9..a6bcaad 100644 --- a/readme.md +++ b/readme.md @@ -275,6 +275,41 @@ Git detects whether merge was successful or not in two ways: `vim-mergetool` supports both options. On quit, if merge was unsuccessful, it both discards any unsaved changes to buffer without touching file's `ctime` and returns non-zero exit code. +### Running as other scm mergetool + +You can set the g:mergetool_args_order variable when you start vim to tell vim-mergetool that your arguments are the files to use for merging. Setup your scm to start vim like this: + + gvim --nofork -c "let g:mergetool_args_order = 'MBRL'" -c "MergetoolStart" $MERGED $BASE $REMOTE $LOCAL + +**MERGED should be the first file** because MergetoolStart is only valid in a file with conflict markers. + +Your scm likely has its own names for these filenames. Check your documentation. + + +#### Example: Subversion + +Subversion names the files something like this: + +* MERGED --> file.vim +* BASE --> file.vim.r404217 +* REMOTE --> file.vim.r404563 +* LOCAL --> file.vim.mine + +So you'd start a merge like this: + + gvim --nofork -c "let g:mergetool_args_order = 'MBRL'" -c "MergetoolStart" file.vim file.vim.r404217 file.vim.r404563 file.vim.mine + +vim-mergetool will act like it does as a git-mergetool (no extra tab and won't try to access git to load other files). + +For TortoiseSVN, create a batchfile like this and set it as your mergetool: + + set LOCAL=%1 + set REMOTE=%2 + set BASE=%3 + set MERGED=%4 + gvim --nofork -c "let g:mergetool_args_order = 'MBLR'" -c "Merge" "%MERGED%" "%BASE%" "%LOCAL%" "%REMOTE%" + + ### Running directly from running Vim instance You can enter and exit merge mode from running Vim instance by opening a file with conflict markers, and running one of the commands: From f07273f14c9e15790edde7c96cfda42c0a6df393 Mon Sep 17 00:00:00 2001 From: David Briscoe Date: Thu, 16 Apr 2020 01:06:19 -0700 Subject: [PATCH 7/8] Convert readme into vim doc - Mostly copied verbatim. Moved some sections around to be more logical within vimdoc. - Rephrased some bits and applied the same rephrasing to readme. - Removed some sections that weren't relevant to people already using from within vim. - Lots of line wrapping and converting `` to || or other corresponding help markup. Not sure if the readme should be stripped down to avoid duplication. --- doc/mergetool.txt | 429 ++++++++++++++++++++++++++++++++++++++++++++++ readme.md | 23 ++- 2 files changed, 440 insertions(+), 12 deletions(-) create mode 100644 doc/mergetool.txt diff --git a/doc/mergetool.txt b/doc/mergetool.txt new file mode 100644 index 0000000..ab01fdb --- /dev/null +++ b/doc/mergetool.txt @@ -0,0 +1,429 @@ +*mergetool.txt* Efficient way of using Vim as a Git mergetool. + +License: MIT + +=============================================================================== +INTRO *mergetool-intro* + +mergetool processes `MERGED` file and extracts `ours`, `theirs`, or +`common` sides of a conflict by parsing conflict markers left by Git. Then it +shows 2-way diff between `ours` and `theirs` versions, with raw conflict +markers being already removed. + +Unlike simply comparing between `LOCAL` and `REMOTE` history revisions, it +takes over where automatic Git merge algorithm gives up. Diffs are present +only where Git cannot automatically resolve conflicts, and you're not +distracted with diff highlighting of already resolved hunks. + +To resolve the conflict you don't need to edit conflict markers directly - +just pick either side of a conflict using |:diffget| and |:diffput| commands. + +This plugin was initially inspired by https://github.com/whiteinge/diffconflicts. + + +=============================================================================== +REQUIREMENTS *mergetool-requirements* + + *mergetool-diff3* + +mergetool requires conflict markers in a `MERGED` file to include common +`BASE` ancestor version as well. This is called `diff3` conflict style. > + + <<<<<<< HEAD + ours/local revision + ||||||| base + common base revision + ======= + theirs/remote revision + >>>>>>> feature + + + + [merge] + conflictStyle = diff3 + + + + git checkout --conflict=diff3 {file} + +` mappings. You're free to set up key mappings +in your `vimrc` as you'd like. See plugin/mergetool.vim for the available +|| mappings. + + +=============================================================================== +MERGING *mergetool-merging* + + *:MergetoolStart* + +When in a file with conflicts, |:MergetoolStart| will show 2-way diff in a new +tab with `$MERGED` file on the left. By default, all conflicts are already +resolved by picking up `ours/LOCAL` version. You don't need to edit raw +conflict markers manually. Either leave hunk as is, or pick `theirs/REMOTE` +version with |:diffget| from the right, or edit hunk manually. + +If there's a merge in progress, |:MergetoolStart| works as usual, but Unlike +running as a `git mergetool`, `LOCAL`, `REMOTE` and `BASE` history revisions +are not passed from the outside. In this mode, mergetool extracts them from +the numbered stages of Git index. > + +$ git cat-file -p :1:{file} > {file}.base +$ git cat-file -p :2:{file} > {file}.local +$ git cat-file -p :3:{file} > {file}.remote + + + nmap mt (MergetoolToggle) +< + + *g:mergetool_prefer_revision* + +|:MergetoolStart| removes conflict markers from `MERGED` file, and picks up +`ours/local` side of a conflict by default. Use |g:mergetool_prefer_revision| +to change the preferred side of a conflict: > + + " possible values: 'local' (default), 'remote', 'base', 'unmodified' + let g:mergetool_prefer_revision = 'remote' + + + + " default behaviour + " m - for working tree version of MERGED file + " r - for 'remote' revision + " l - for 'local' revision + " b - common merge 'base' + let g:mergetool_layout = 'mr' + + + + let g:mergetool_layout = 'LmR' + +This `LmR` setup is pretty much same to what vim-fugitive |:Gdiff| +does, except that conflict markers are already removed. You can use +|g:mergetool_prefer_revision|='unmodified' to replicate vim-fugitive +completely. Indeed, mergetool is flexible enough to replicate any existing +vim+merge solution. + +Vertical splits are used by default. Use a comma to split horizontally: > + + " merged above remote + let g:mergetool_layout = 'm,r' + " base above local and remote above merged + let g:mergetool_layout = 'b,lr,m' +< + + *:MergetoolToggleLayout* + +Use |:MergetoolToggleLayout| to switch different layouts during a merge. + +For example, you can default to a 2-way diff layout: > + + " In 'vimrc', set your default layout. + let g:mergetool_layout = 'mr' + + + + " View 'base' revision on the left + :MergetoolToggleLayout bmr + + " View 'base' revision in horizontal split at the bottom + :MergetoolToggleLayout mr,b + + " View history revisions, and hide 'MERGED' file altogether + :MergetoolToggleLayout LBR + + + + nnoremap mb :call mergetool#toggle_layout('mr,b') +< + + *g:MergetoolSetLayoutCallback* + +To further tweak layout or change settings of individual splits, define the +layout callback. It is called when layout is changed. + +Example. When layout is `mr,b`, I want the `base` horizontal split to be +pulled of a diff mode and have syntax highlighting enabled. Also, I want it to +reduce its height. > + + function s:on_mergetool_set_layout(split) + if a:split["layout"] ==# 'mr,b' && a:split["split"] ==# 'b' + set nodiff + set syntax=on + + resize 15 + endif + endfunction + + let g:MergetoolSetLayoutCallback = function('s:on_mergetool_set_layout') +< + +Callback is called for each split in the layout, with a split being passed as +a callback argument. > + + { + 'layout': 'mb,r', # current layout + 'split': 'b', # current split + 'filetype': 'vim', # file type of MERGED file + 'bufnr': 2, # buffer number of current split + 'winnr': 5 # window number of current split + } + +=============================================================================== +DIFFING *mergetool-diff* + + *:MergetoolDiffExchangeLeft* + *:MergetoolDiffExchangeRight* + *:MergetoolDiffExchangeDown* + *:MergetoolDiffExchangeUp* + +Vim's |:diffget| and |:diffput| commands are convenient and unambiguous as +soon as you have only two buffers in diff mode. If you prefer 3-way diff, +you're out of lucky, as you need to explicitly tell the buffer number you want +to exchange diff with. + +mergetool comes with "DiffExchange" commands and mapping, that accepts +direction of a diff movement: "left", "right", "up", "down". You can set up +your own key mappings for diff mode only: > + + nmap &diff? '(MergetoolDiffExchangeLeft)' : '' + nmap &diff? '(MergetoolDiffExchangeRight)' : '' + nmap &diff? '(MergetoolDiffExchangeDown)' : '' + nmap &diff? '(MergetoolDiffExchangeUp)' : '' + + + + :MergetoolDiffExchangeLeft + :MergetoolDiffExchangeRight + :MergetoolDiffExchangeDown + :MergetoolDiffExchangeUp + +` would `diffget` change from the right split into the middle one. + If you imagine the diff movement - it goes from right to the left. +- `` would `diffget` change from the left split into the middle one. + If you imagine the diff movement - it goes from left to the right. + +If the rightmost split were the active one: +- `` would `diffput` change from the current split into the middle + one. As soon as there is no adjacent window on the right to get change + from, we invert `diffget` operation into `diffput`. +- `` would `diffget` change from middle split. + +Same logic applies to "up" and "down" directions. Useful if you prefer +horizontal splits. + +Conclusion~ +Despite how many splits are opened and what's the layout, you +don't need to wrap your head around `diffput` vs `diffget` semantics, and you +don't need to figure out correct buffer numbers manually. You just give +desired diff movement direction, and mergetool handles the details for you. + +Limitations~ +* DiffExchange commands work only in normal mode, and do not + support visual mode and working with line ranges. +* DiffExchange functionality is not specific to resolving merge conflicts, and + can be used for regular diffs. + +If you like `` mappings from the snippet above, you might also want +to map `` and `` keys to navigate diffs, instead of default `[c` and +`]c` mappings. They're not used anyway, since you're using `h,j,k,l` for +movements, are you? ;-) > + + nnoremap &diff ? '[c' : '' + nnoremap &diff ? ']c' : '' +< + +=============================================================================== +USER AUTOCOMMANDS *mergetool-autocmd-user* + + *MergetoolStart-autocmd* + *MergetoolStop-autocmd* + +These |User| autocommands are triggered when merge mode begins and ends. You +could use them to turn off |spell| during a merge: > + + augroup your_mergetool + au! + autocmd User MergetoolStart set nospell + autocmd User MergetoolStop set spell + augroup END + +< + +=============================================================================== +WORKING WITH OTHER PLUGINS *mergetool-other-plugins* + + *mergetool-statusline* + *g:mergetool_in_merge_mode* + +|g:mergetool_in_merge_mode| indicates whether you're in merge mode. It can be +helpful to show indicator in a status line. + +Example for vim-airline: > + + function! AirlineDiffmergePart() + if get(g:, 'mergetool_in_merge_mode', 0) + return '↸' + endif + + if &diff + return '↹' + endif + + return '' + endfunction + + call airline#parts#define_function('_diffmerge', 'AirlineDiffmergePart') + call airline#parts#define_accent('_diffmerge', 'bold') + + let g:airline_section_z = airline#section#create(['_diffmerge', ...other_parts]) +< +=============================================================================== +USING AS A MERGETOOL *mergetool-as-mergetool* + *mergetool-git-mergetool* + +mergetool can be configured to run as a git mergetool. In your `~/.gitconfig`: > + + [merge] + tool = vim_mergetool + conflictstyle = diff3 + + [mergetool "vim_mergetool"] + cmd = vim -f -c "MergetoolStart" "$MERGED" "$BASE" "$LOCAL" "$REMOTE" + trustExitCode = true + + + + gvim -f -c "let g:mergetool_args_order = 'MBRL'" -c "MergetoolStart" "$MERGED" "$BASE" "$REMOTE" "$LOCAL" + + + + set LOCAL=%1 + set REMOTE=%2 + set BASE=%3 + set MERGED=%4 + gvim --nofork -c "let g:mergetool_args_order = 'MBLR'" -c "Merge" "%MERGED%" "%BASE%" "%LOCAL%" "%REMOTE%" +< + *mergetool-exiting* + +When exiting merge mode, mergetool would prompt you whether merge was +successful. If not, it will rollback changes to the buffer, will not save +`MERGED` file to disk, and exit with non-zero code, when running as a git +mergetool. + +You can either issue `:MergetoolStop` or `:MergetoolToggle` commands, or use +dedicated mapping. + +Yet another approach, which I prefer in my personal `vimrc`, is having a +`q` key mapped to context-aware `QuitWindow()` function. It detects +whether we're in merge mode, and runs `:MergetoolStop` command, or just uses +normal "quit" command otherwise. > + + function s:QuitWindow() + + " If we're in merge mode, exit + if get(g:, 'mergetool_in_merge_mode', 0) + call mergetool#stop() + return + endif + + if &diff + " Quit diff mode intelligently... + endif + + quit + endfunction + + command! QuitWindow call s:QuitWindow() + nnoremap q :QuitWindow +< +=============================================================================== +vim:tw=78:sw=2:ts=2:ft=help:norl:nowrap: diff --git a/readme.md b/readme.md index a6bcaad..1dbf5ba 100644 --- a/readme.md +++ b/readme.md @@ -136,7 +136,7 @@ Alternatively, you can start with `local` or `unmodified` revision, and change y ### Layout -`vim-mergetool` defaults to two vertical splits layout with `MERGED` file on the left, and `remote` revision on the right. `MERGED` file is processed according to `g:mergetool_prefer_revision` setting as described above. +`vim-mergetool` defaults to two vertical splits layout with `MERGED` file on the left and `remote` revision on the right. `MERGED` file is processed according to `g:mergetool_prefer_revision` setting as described above. ```vim " (m) - for working tree version of MERGED file @@ -152,7 +152,7 @@ let g:mergetool_layout = 'bmr' ![3 way diff vertical split layout](./screenshots/bmr_3splits_layout.png) -To show usual `REMOTE`, `LOCAL`, `BASE` history revisions, use uppercase characters: +Lower case letters use files derived from the merged file (by accepting that file's view of conflicts). To use the original `REMOTE`, `LOCAL`, `BASE` files from git, use uppercase characters: ```vim let g:mergetool_layout = 'LmR' @@ -160,7 +160,7 @@ let g:mergetool_layout = 'LmR' By the way, this setup is pretty much same to what [vim-fugitive](https://github.com/tpope/vim-fugitive) `:Gdiff` does, except that conflict markers are already removed. You can use `g:mergetool_prefer_revision='unmodified'` to replicate vim-fugitive completely. Indeed, `vim-mergetool` is flexible enough to replicate any existing vim+merge solution. -Vertical splits are used by default. If you prefer working with horizontal splits: +Vertical splits are used by default. Use a comma to split horizontally: ```vim let g:mergetool_layout = 'm,r' @@ -182,8 +182,7 @@ For example, you can start with 2-way diff layout, and then temporarily toggle a ```vim -" In 'vimrc' -" Default layout +" In 'vimrc', set your default layout. let g:mergetool_layout = 'mr' " Later, during merge process: @@ -277,13 +276,13 @@ Git detects whether merge was successful or not in two ways: ### Running as other scm mergetool -You can set the g:mergetool_args_order variable when you start vim to tell vim-mergetool that your arguments are the files to use for merging. Setup your scm to start vim like this: +If your scm doesn't use files called `BASE`, `REMOTE`, `LOCAL` as merge tempfiles (like svn), you can set the g:mergetool_args_order variable to tell mergetool which argument is which file. Setup your scm to start vim like this: gvim --nofork -c "let g:mergetool_args_order = 'MBRL'" -c "MergetoolStart" $MERGED $BASE $REMOTE $LOCAL **MERGED should be the first file** because MergetoolStart is only valid in a file with conflict markers. -Your scm likely has its own names for these filenames. Check your documentation. +Your scm likely has its own variable names for these filenames. Check your documentation. #### Example: Subversion @@ -325,7 +324,7 @@ You can set up a key mapping to toggle merge mode: nmap mt (MergetoolToggle) ``` -When exiting merge mode, if merge was unsuccessful, `vim-mergetool` would discard changes to merged file and rollback to a buffer state as it were right before starting a new merge. +When exiting merge mode, if merge was unsuccessful, `vim-mergetool` discards changes to merged file and rollback to a buffer state as it were right before starting a new merge. Unlike running as a `git mergetool`, `LOCAL`, `REMOTE` and `BASE` history revisions are not passed from the outside. In this mode, `vim-mergetool` extracts them from the numbered stages of Git index. @@ -378,7 +377,7 @@ If the rightmost split were the active one: Same logic applies to "up" and "down" directions. Useful if you prefer horizontal splits. -**Conclusion**: despite how many splits are opened and what's the layout, you don't need to wrap your head around `diffput` vs `diffget` semantics, and you don't need to figure out correct buffer numbers manually. You just tell desired diff movement direction, and `vim-mergetool` handles the details for you. +**Conclusion**: despite how many splits are opened and what's the layout, you don't need to wrap your head around `diffput` vs `diffget` semantics, and you don't need to figure out correct buffer numbers manually. You just give desired diff movement direction, and `vim-mergetool` handles the details for you. **Limitation**: `DiffExchange` commands work only in normal mode, and do not support visual mode and working with line ranges. @@ -387,8 +386,8 @@ Same logic applies to "up" and "down" directions. Useful if you prefer horizonta If you like `` mappings from the snippet above, you might also want to map `` and `` keys to navigate diffs, instead of default `[c` and `]c` mappings. They're not used anyway, since you're using `h,j,k,l` for movements, are you? ;-) ```vim -nmap &diff ? '[c' : '' -nmap &diff ? ']c' : '' +nnoremap &diff ? '[c' : '' +nnoremap &diff ? ']c' : '' ``` @@ -456,7 +455,7 @@ function s:QuitWindow() quit endfunction -command QuitWindow call s:QuitWindow() +command! QuitWindow call s:QuitWindow() nnoremap q :QuitWindow ``` From c060b486c80a353ee2070b4d524bf74b3b57bd6a Mon Sep 17 00:00:00 2001 From: David Briscoe Date: Thu, 16 Apr 2020 10:26:09 -0700 Subject: [PATCH 8/8] Remove sections of readme better served in docs If the readme is "marketing" for people looking to see if they want to install the plugin, then the vimdoc is a "manual" to help them figure out how to use it once installed. These sections duplicate what's in the vimdoc and are more relevant to someone trying to configure the plugin instead of someone checking out what it can do. Also, these sections don't have images. --- readme.md | 127 ++---------------------------------------------------- 1 file changed, 3 insertions(+), 124 deletions(-) diff --git a/readme.md b/readme.md index 1dbf5ba..61c9c86 100644 --- a/readme.md +++ b/readme.md @@ -101,29 +101,11 @@ git checkout --conflict=diff3 {file} - Prompts whether merge was successful on quit. If not, rollbacks changes and report non-zero exit status code when run as a `git mergetool`. - Smart diff exchange commands. Tell direction of a window to `diffget` or `diffput` instead of specifying a buffer number. Especially handy for 3-way diffs. Not limited to merge conflict scenarios, can be used for regular diffs. - Can tell if we're in merge mode right now. Useful for showing some sort of indicator in a status line. +- Use as a mergetool for git and other source control management systems. **NOTE**: `vim-mergetool` does not set up any key mappings for you. It justs exports a handful of commands and `` mappings. You're free to set up key mappings in your `vimrc` as you'd like. -### Preferred conflict side -`vim-mergetool` removes conflict markers from `MERGED` file, and picks up `ours/local` side of a conflict by default. If you prefer another side of a conflict: - -```vim -" possible values: 'local' (default), 'remote', 'base' -let g:mergetool_prefer_revision = 'remote' -``` - -If you don't want `vim-mergetool` to process `MERGED` file and remove raw conflict markers: - -```vim -let g:mergetool_prefer_revision = 'unmodified' -``` - -Alternatively, you can start with `local` or `unmodified` revision, and change your mind later during merge process by running one of these commands: - -```vim -:MergetoolPreferLocal -:MergetoolPreferRemote -``` +See [the documentation](doc/mergetool.txt) for more commands and configuration options. ### Available revisions to compare @@ -253,90 +235,6 @@ Here's the end result: ![Layout advanced customization](./screenshots/layout_advanced_customization.png) -### Running as a `git mergetool` - -`vim-mergetool` can be configured to run as a `git mergetool`. In your `~/.gitconfig`: - -```ini -[merge] -tool = vim_mergetool -conflictstyle = diff3 - -[mergetool "vim_mergetool"] -cmd = vim -f -c "MergetoolStart" "$MERGED" "$BASE" "$LOCAL" "$REMOTE" -trustExitCode = true -``` - -Git detects whether merge was successful or not in two ways: -- When `trustExitCode = false`, checks if `MERGED` file was modified. -- When `trustExitCode = true`, checks exit code of merge tool process. - -`vim-mergetool` supports both options. On quit, if merge was unsuccessful, it both discards any unsaved changes to buffer without touching file's `ctime` and returns non-zero exit code. - - -### Running as other scm mergetool - -If your scm doesn't use files called `BASE`, `REMOTE`, `LOCAL` as merge tempfiles (like svn), you can set the g:mergetool_args_order variable to tell mergetool which argument is which file. Setup your scm to start vim like this: - - gvim --nofork -c "let g:mergetool_args_order = 'MBRL'" -c "MergetoolStart" $MERGED $BASE $REMOTE $LOCAL - -**MERGED should be the first file** because MergetoolStart is only valid in a file with conflict markers. - -Your scm likely has its own variable names for these filenames. Check your documentation. - - -#### Example: Subversion - -Subversion names the files something like this: - -* MERGED --> file.vim -* BASE --> file.vim.r404217 -* REMOTE --> file.vim.r404563 -* LOCAL --> file.vim.mine - -So you'd start a merge like this: - - gvim --nofork -c "let g:mergetool_args_order = 'MBRL'" -c "MergetoolStart" file.vim file.vim.r404217 file.vim.r404563 file.vim.mine - -vim-mergetool will act like it does as a git-mergetool (no extra tab and won't try to access git to load other files). - -For TortoiseSVN, create a batchfile like this and set it as your mergetool: - - set LOCAL=%1 - set REMOTE=%2 - set BASE=%3 - set MERGED=%4 - gvim --nofork -c "let g:mergetool_args_order = 'MBLR'" -c "Merge" "%MERGED%" "%BASE%" "%LOCAL%" "%REMOTE%" - - -### Running directly from running Vim instance -You can enter and exit merge mode from running Vim instance by opening a file with conflict markers, and running one of the commands: - -```vim -:MergetoolStart -:MergetoolStop -:MergetoolToggle -``` - -You can set up a key mapping to toggle merge mode: - -```vim -nmap mt (MergetoolToggle) -``` - -When exiting merge mode, if merge was unsuccessful, `vim-mergetool` discards changes to merged file and rollback to a buffer state as it were right before starting a new merge. - -Unlike running as a `git mergetool`, `LOCAL`, `REMOTE` and `BASE` history revisions are not passed from the outside. In this mode, `vim-mergetool` extracts them from the numbered stages of Git index. - -```bash -$ git cat-file -p :1:{file} > {file}.base -$ git cat-file -p :2:{file} > {file}.local -$ git cat-file -p :3:{file} > {file}.remote -``` - -**ASSUMPTION:** Therefore, it's assumed that a git merge is in progress, and `cwd` of running Vim instance is set to repository root dir. - - ### Smart diff exchange commands Vim's `:diffget` and `:diffput` commands are convenient and unambiguous as soon as you have only two buffers in diff mode. If you prefer 3-way diff, you're out of lucky, as you need to explicitly tell the buffer number you want to exchange diff with. @@ -394,26 +292,7 @@ nnoremap &diff ? ']c' : '' ### Merge mode detection You can detect whether you're in merge mode now, by inspecting `g:mergetool_in_merge_mode` variable. -It can be helpful to show indicator in a status line. Example for [vim-airline](https://github.com/vim-airline/vim-airline): - -```vim -function! AirlineDiffmergePart() - if get(g:, 'mergetool_in_merge_mode', 0) - return '↸' - endif - - if &diff - return '↹' - endif - - return '' -endfunction - -call airline#parts#define_function('_diffmerge', 'AirlineDiffmergePart') -call airline#parts#define_accent('_diffmerge', 'bold') - -let g:airline_section_z = airline#section#create(['_diffmerge', ...other_parts]) -``` +It can be helpful to show indicator in a status line. See mergetool-statusline in [the documentation](doc/mergetool.txt) to setup [vim-airline](https://github.com/vim-airline/vim-airline) like this: ![Status line indicator](./screenshots/airline_merge_indicator.png)