diff options
Diffstat (limited to 'vim/bundle/vim-gitgutter/autoload')
-rw-r--r-- | vim/bundle/vim-gitgutter/autoload/gitgutter.vim | 253 | ||||
-rw-r--r-- | vim/bundle/vim-gitgutter/autoload/gitgutter/async.vim | 196 | ||||
-rw-r--r-- | vim/bundle/vim-gitgutter/autoload/gitgutter/debug.vim | 119 | ||||
-rw-r--r-- | vim/bundle/vim-gitgutter/autoload/gitgutter/diff.vim | 342 | ||||
-rw-r--r-- | vim/bundle/vim-gitgutter/autoload/gitgutter/highlight.vim | 115 | ||||
-rw-r--r-- | vim/bundle/vim-gitgutter/autoload/gitgutter/hunk.vim | 137 | ||||
-rw-r--r-- | vim/bundle/vim-gitgutter/autoload/gitgutter/sign.vim | 173 | ||||
-rw-r--r-- | vim/bundle/vim-gitgutter/autoload/gitgutter/utility.vim | 199 |
8 files changed, 1534 insertions, 0 deletions
diff --git a/vim/bundle/vim-gitgutter/autoload/gitgutter.vim b/vim/bundle/vim-gitgutter/autoload/gitgutter.vim new file mode 100644 index 0000000..3b1770a --- /dev/null +++ b/vim/bundle/vim-gitgutter/autoload/gitgutter.vim @@ -0,0 +1,253 @@ +let s:nomodeline = (v:version > 703 || (v:version == 703 && has('patch442'))) ? '<nomodeline>' : '' + +" Primary functions {{{ + +function! gitgutter#all() abort + for buffer_id in tabpagebuflist() + let file = expand('#' . buffer_id . ':p') + if !empty(file) + call gitgutter#process_buffer(buffer_id, 0) + endif + endfor +endfunction + +" bufnr: (integer) the buffer to process. +" realtime: (boolean) when truthy, do a realtime diff; otherwise do a disk-based diff. +function! gitgutter#process_buffer(bufnr, realtime) abort + call gitgutter#utility#use_known_shell() + + call gitgutter#utility#set_buffer(a:bufnr) + if gitgutter#utility#is_active() + if g:gitgutter_sign_column_always + call gitgutter#sign#add_dummy_sign() + endif + try + if !a:realtime || gitgutter#utility#has_fresh_changes() + let diff = gitgutter#diff#run_diff(a:realtime || gitgutter#utility#has_unsaved_changes(), 0) + if diff != 'async' + call gitgutter#handle_diff(diff) + endif + endif + catch /diff failed/ + call gitgutter#debug#log('diff failed') + call gitgutter#hunk#reset() + endtry + execute "silent doautocmd" s:nomodeline "User GitGutter" + else + call gitgutter#hunk#reset() + endif + + call gitgutter#utility#restore_shell() +endfunction + + +function! gitgutter#handle_diff(diff) abort + call gitgutter#debug#log(a:diff) + + call setbufvar(gitgutter#utility#bufnr(), 'gitgutter_tracked', 1) + + call gitgutter#hunk#set_hunks(gitgutter#diff#parse_diff(a:diff)) + let modified_lines = gitgutter#diff#process_hunks(gitgutter#hunk#hunks()) + + if len(modified_lines) > g:gitgutter_max_signs + call gitgutter#utility#warn_once('exceeded maximum number of signs (configured by g:gitgutter_max_signs).', 'max_signs') + call gitgutter#sign#clear_signs() + return + endif + + if g:gitgutter_signs || g:gitgutter_highlight_lines + call gitgutter#sign#update_signs(modified_lines) + endif + + call gitgutter#utility#save_last_seen_change() +endfunction + +function! gitgutter#disable() abort + " get list of all buffers (across all tabs) + let buflist = [] + for i in range(tabpagenr('$')) + call extend(buflist, tabpagebuflist(i + 1)) + endfor + + for buffer_id in buflist + let file = expand('#' . buffer_id . ':p') + if !empty(file) + call gitgutter#utility#set_buffer(buffer_id) + call gitgutter#sign#clear_signs() + call gitgutter#sign#remove_dummy_sign(1) + call gitgutter#hunk#reset() + endif + endfor + + let g:gitgutter_enabled = 0 +endfunction + +function! gitgutter#enable() abort + let g:gitgutter_enabled = 1 + call gitgutter#all() +endfunction + +function! gitgutter#toggle() abort + if g:gitgutter_enabled + call gitgutter#disable() + else + call gitgutter#enable() + endif +endfunction + +" }}} + +" Line highlights {{{ + +function! gitgutter#line_highlights_disable() abort + let g:gitgutter_highlight_lines = 0 + call gitgutter#highlight#define_sign_line_highlights() + + if !g:gitgutter_signs + call gitgutter#sign#clear_signs() + call gitgutter#sign#remove_dummy_sign(0) + endif + + redraw! +endfunction + +function! gitgutter#line_highlights_enable() abort + let old_highlight_lines = g:gitgutter_highlight_lines + + let g:gitgutter_highlight_lines = 1 + call gitgutter#highlight#define_sign_line_highlights() + + if !old_highlight_lines && !g:gitgutter_signs + call gitgutter#all() + endif + + redraw! +endfunction + +function! gitgutter#line_highlights_toggle() abort + if g:gitgutter_highlight_lines + call gitgutter#line_highlights_disable() + else + call gitgutter#line_highlights_enable() + endif +endfunction + +" }}} + +" Signs {{{ + +function! gitgutter#signs_enable() abort + let old_signs = g:gitgutter_signs + + let g:gitgutter_signs = 1 + call gitgutter#highlight#define_sign_text_highlights() + + if !old_signs && !g:gitgutter_highlight_lines + call gitgutter#all() + endif +endfunction + +function! gitgutter#signs_disable() abort + let g:gitgutter_signs = 0 + call gitgutter#highlight#define_sign_text_highlights() + + if !g:gitgutter_highlight_lines + call gitgutter#sign#clear_signs() + call gitgutter#sign#remove_dummy_sign(0) + endif +endfunction + +function! gitgutter#signs_toggle() abort + if g:gitgutter_signs + call gitgutter#signs_disable() + else + call gitgutter#signs_enable() + endif +endfunction + +" }}} + +" Hunks {{{ + +function! gitgutter#stage_hunk() abort + call gitgutter#utility#use_known_shell() + if gitgutter#utility#is_active() + " Ensure the working copy of the file is up to date. + " It doesn't make sense to stage a hunk otherwise. + noautocmd silent write + let diff = gitgutter#diff#run_diff(0, 1) + call gitgutter#handle_diff(diff) + + if empty(gitgutter#hunk#current_hunk()) + call gitgutter#utility#warn('cursor is not in a hunk') + else + let diff_for_hunk = gitgutter#diff#generate_diff_for_hunk(diff, 'stage') + call gitgutter#utility#system(gitgutter#utility#command_in_directory_of_file(g:gitgutter_git_executable.' apply --cached --unidiff-zero - '), diff_for_hunk) + + " refresh gitgutter's view of buffer + silent execute "GitGutter" + endif + + silent! call repeat#set("\<Plug>GitGutterStageHunk", -1)<CR> + endif + call gitgutter#utility#restore_shell() +endfunction + +function! gitgutter#undo_hunk() abort + call gitgutter#utility#use_known_shell() + if gitgutter#utility#is_active() + " Ensure the working copy of the file is up to date. + " It doesn't make sense to stage a hunk otherwise. + noautocmd silent write + let diff = gitgutter#diff#run_diff(0, 1) + call gitgutter#handle_diff(diff) + + if empty(gitgutter#hunk#current_hunk()) + call gitgutter#utility#warn('cursor is not in a hunk') + else + let diff_for_hunk = gitgutter#diff#generate_diff_for_hunk(diff, 'undo') + call gitgutter#utility#system(gitgutter#utility#command_in_directory_of_file(g:gitgutter_git_executable.' apply --reverse --unidiff-zero - '), diff_for_hunk) + + " reload file preserving screen line position + let wl = winline() + silent edit + let offset = wl - winline() + execute "normal ".offset."\<C-Y>" + endif + + silent! call repeat#set("\<Plug>GitGutterUndoHunk", -1)<CR> + endif + call gitgutter#utility#restore_shell() +endfunction + +function! gitgutter#preview_hunk() abort + call gitgutter#utility#use_known_shell() + if gitgutter#utility#is_active() + " Ensure the working copy of the file is up to date. + " It doesn't make sense to stage a hunk otherwise. + noautocmd silent write + let diff = gitgutter#diff#run_diff(0, 1) + call gitgutter#handle_diff(diff) + + if empty(gitgutter#hunk#current_hunk()) + call gitgutter#utility#warn('cursor is not in a hunk') + else + let diff_for_hunk = gitgutter#diff#generate_diff_for_hunk(diff, 'preview') + + silent! wincmd P + if !&previewwindow + execute 'bo ' . &previewheight . ' new' + set previewwindow + endif + + setlocal noro modifiable filetype=diff buftype=nofile bufhidden=delete noswapfile + execute "%delete_" + call append(0, split(diff_for_hunk, "\n")) + + wincmd p + endif + endif + call gitgutter#utility#restore_shell() +endfunction + +" }}} diff --git a/vim/bundle/vim-gitgutter/autoload/gitgutter/async.vim b/vim/bundle/vim-gitgutter/autoload/gitgutter/async.vim new file mode 100644 index 0000000..78e725f --- /dev/null +++ b/vim/bundle/vim-gitgutter/autoload/gitgutter/async.vim @@ -0,0 +1,196 @@ +let s:jobs = {} + +" Nvim has always supported async commands. +" +" Vim introduced async in 7.4.1826. +" +" gVim didn't support aync until 7.4.1850 (though I haven't been able to +" verify this myself). +" +" MacVim-GUI didn't support async until 7.4.1832 (actually commit +" 88f4fe0 but 7.4.1832 was the first subsequent patch release). +let s:available = has('nvim') || ( + \ (has('patch-7-4-1826') && !has('gui_running')) || + \ (has('patch-7-4-1850') && has('gui_running')) || + \ (has('patch-7-4-1832') && has('gui_macvim')) + \ ) + +function! gitgutter#async#available() + return s:available +endfunction + +function! gitgutter#async#execute(cmd) abort + let bufnr = gitgutter#utility#bufnr() + + if has('nvim') + if has('unix') + let command = ["/bin/sh", "-c", a:cmd] + elseif has('win32') + let command = ["cmd.exe", "/c", a:cmd] + else + throw 'unknown os' + endif + " Make the job use a shell while avoiding (un)quoting problems. + let job_id = jobstart(command, { + \ 'buffer': bufnr, + \ 'on_stdout': function('gitgutter#async#handle_diff_job_nvim'), + \ 'on_stderr': function('gitgutter#async#handle_diff_job_nvim'), + \ 'on_exit': function('gitgutter#async#handle_diff_job_nvim') + \ }) + call gitgutter#debug#log('[nvim job: '.job_id.', buffer: '.bufnr.'] '.a:cmd) + if job_id < 1 + throw 'diff failed' + endif + + " Note that when `cmd` doesn't produce any output, i.e. the diff is empty, + " the `stdout` event is not fired on the job handler. Therefore we keep + " track of the jobs ourselves so we can spot empty diffs. + call s:job_started(job_id) + + else + " Make the job use a shell. + " + " Pass a handler for stdout but not for stderr so that errors are + " ignored (and thus signs are not updated; this assumes that an error + " only occurs when a file is not tracked by git). + + if has('unix') + let command = ["/bin/sh", "-c", a:cmd] + elseif has('win32') + " Help docs recommend {command} be a string on Windows. But I think + " they also say that will run the command directly, which I believe would + " mean the redirection and pipe stuff wouldn't work. + " let command = "cmd.exe /c ".a:cmd + let command = ["cmd.exe", "/c", a:cmd] + else + throw 'unknown os' + endif + + let job = job_start(command, { + \ 'out_cb': 'gitgutter#async#handle_diff_job_vim', + \ 'close_cb': 'gitgutter#async#handle_diff_job_vim_close' + \ }) + call gitgutter#debug#log('[vim job: '.string(job_info(job)).', buffer: '.bufnr.'] '.a:cmd) + + call s:job_started(s:channel_id(job_getchannel(job)), bufnr) + endif +endfunction + + +function! gitgutter#async#handle_diff_job_nvim(job_id, data, event) abort + call gitgutter#debug#log('job_id: '.a:job_id.', event: '.a:event.', buffer: '.self.buffer) + + let current_buffer = gitgutter#utility#bufnr() + call gitgutter#utility#set_buffer(self.buffer) + + if a:event == 'stdout' + " a:data is a list + call s:job_finished(a:job_id) + call gitgutter#handle_diff(gitgutter#utility#stringify(a:data)) + + elseif a:event == 'exit' + " If the exit event is triggered without a preceding stdout event, + " the diff was empty. + if s:is_job_started(a:job_id) + call gitgutter#handle_diff("") + call s:job_finished(a:job_id) + endif + + else " a:event is stderr + call gitgutter#hunk#reset() + call s:job_finished(a:job_id) + + endif + + call gitgutter#utility#set_buffer(current_buffer) +endfunction + + +" Channel is in NL mode. +function! gitgutter#async#handle_diff_job_vim(channel, line) abort + call gitgutter#debug#log('channel: '.a:channel.', line: '.a:line) + + call s:accumulate_job_output(s:channel_id(a:channel), a:line) +endfunction + +function! gitgutter#async#handle_diff_job_vim_close(channel) abort + call gitgutter#debug#log('channel: '.a:channel) + + let channel_id = s:channel_id(a:channel) + + let current_buffer = gitgutter#utility#bufnr() + call gitgutter#utility#set_buffer(s:job_buffer(channel_id)) + + call gitgutter#handle_diff(s:job_output(channel_id)) + call s:job_finished(channel_id) + + call gitgutter#utility#set_buffer(current_buffer) +endfunction + + +function! s:channel_id(channel) abort + " This seems to be the only way to get info about the channel once closed. + return matchstr(a:channel, '\d\+') +endfunction + + +" +" Keep track of jobs. +" +" nvim: receives all the job's output at once so we don't need to accumulate +" it ourselves. We can pass the buffer number into the job so we don't need +" to track that either. +" +" s:jobs {} -> key: job's id, value: anything truthy +" +" vim: receives the job's output line by line so we need to accumulate it. +" We also need to keep track of the buffer the job is running for. +" Vim job's don't have an id. Instead we could use the external process's id +" or the channel's id (there seems to be 1 channel per job). Arbitrarily +" choose the channel's id. +" +" s:jobs {} -> key: channel's id, value: {} key: output, value: [] job's output +" key: buffer: value: buffer number + + +" nvim: +" id: job's id +" +" vim: +" id: channel's id +" arg: buffer number +function! s:job_started(id, ...) abort + if a:0 " vim + let s:jobs[a:id] = {'output': [], 'buffer': a:1} + else " nvim + let s:jobs[a:id] = 1 + endif +endfunction + +function! s:is_job_started(id) abort + return has_key(s:jobs, a:id) +endfunction + +function! s:accumulate_job_output(id, line) abort + call add(s:jobs[a:id].output, a:line) +endfunction + +" Returns a string +function! s:job_output(id) abort + if has_key(s:jobs, a:id) + return gitgutter#utility#stringify(s:jobs[a:id].output) + else + return "" + endif +endfunction + +function! s:job_buffer(id) abort + return s:jobs[a:id].buffer +endfunction + +function! s:job_finished(id) abort + if has_key(s:jobs, a:id) + unlet s:jobs[a:id] + endif +endfunction + diff --git a/vim/bundle/vim-gitgutter/autoload/gitgutter/debug.vim b/vim/bundle/vim-gitgutter/autoload/gitgutter/debug.vim new file mode 100644 index 0000000..594f044 --- /dev/null +++ b/vim/bundle/vim-gitgutter/autoload/gitgutter/debug.vim @@ -0,0 +1,119 @@ +let s:plugin_dir = expand('<sfile>:p:h:h:h').'/' +let s:log_file = s:plugin_dir.'gitgutter.log' +let s:channel_log = s:plugin_dir.'channel.log' +let s:new_log_session = 1 + + +function! gitgutter#debug#debug() + " Open a scratch buffer + vsplit __GitGutter_Debug__ + normal! ggdG + setlocal buftype=nofile + setlocal bufhidden=delete + setlocal noswapfile + + call gitgutter#debug#vim_version() + call gitgutter#debug#separator() + + call gitgutter#debug#git_version() + call gitgutter#debug#separator() + + call gitgutter#debug#grep_version() + call gitgutter#debug#separator() + + call gitgutter#debug#option('updatetime') + call gitgutter#debug#option('shell') + call gitgutter#debug#option('shellcmdflag') + call gitgutter#debug#option('shellpipe') + call gitgutter#debug#option('shellquote') + call gitgutter#debug#option('shellredir') + call gitgutter#debug#option('shellslash') + call gitgutter#debug#option('shelltemp') + call gitgutter#debug#option('shelltype') + call gitgutter#debug#option('shellxescape') + call gitgutter#debug#option('shellxquote') +endfunction + + +function! gitgutter#debug#separator() + call gitgutter#debug#output('') +endfunction + +function! gitgutter#debug#vim_version() + redir => version_info + silent execute 'version' + redir END + call gitgutter#debug#output(split(version_info, '\n')[0:2]) +endfunction + +function! gitgutter#debug#git_version() + let v = system(g:gitgutter_git_executable.' --version') + call gitgutter#debug#output( substitute(v, '\n$', '', '') ) +endfunction + +function! gitgutter#debug#grep_version() + let v = system('grep --version') + call gitgutter#debug#output( substitute(v, '\n$', '', '') ) + + let v = system('grep --help') + call gitgutter#debug#output( substitute(v, '\%x00', '', 'g') ) +endfunction + +function! gitgutter#debug#option(name) + if exists('+' . a:name) + let v = eval('&' . a:name) + call gitgutter#debug#output(a:name . '=' . v) + " redir => output + " silent execute "verbose set " . a:name . "?" + " redir END + " call gitgutter#debug#output(a:name . '=' . output) + else + call gitgutter#debug#output(a:name . ' [n/a]') + end +endfunction + +function! gitgutter#debug#output(text) + call append(line('$'), a:text) +endfunction + +" assumes optional args are calling function's optional args +function! gitgutter#debug#log(message, ...) abort + if g:gitgutter_log + if s:new_log_session && gitgutter#async#available() + if exists('*ch_logfile') + call ch_logfile(s:channel_log, 'w') + endif + endif + + execute 'redir >> '.s:log_file + if s:new_log_session + let s:start = reltime() + silent echo "\n==== start log session ====" + endif + + let elapsed = reltimestr(reltime(s:start)).' ' + silent echo '' + " callers excluding this function + silent echo elapsed.expand('<sfile>')[:-22].':' + silent echo elapsed.s:format_for_log(a:message) + if a:0 && !empty(a:1) + for msg in a:000 + silent echo elapsed.s:format_for_log(msg) + endfor + endif + redir END + + let s:new_log_session = 0 + endif +endfunction + +function! s:format_for_log(data) abort + if type(a:data) == 1 + return join(split(a:data,'\n'),"\n") + elseif type(a:data) == 3 + return '['.join(a:data,"\n").']' + else + return a:data + endif +endfunction + diff --git a/vim/bundle/vim-gitgutter/autoload/gitgutter/diff.vim b/vim/bundle/vim-gitgutter/autoload/gitgutter/diff.vim new file mode 100644 index 0000000..170193b --- /dev/null +++ b/vim/bundle/vim-gitgutter/autoload/gitgutter/diff.vim @@ -0,0 +1,342 @@ +if exists('g:gitgutter_grep_command') + let s:grep_available = 1 + let s:grep_command = g:gitgutter_grep_command +else + let s:grep_available = executable('grep') + if s:grep_available + let s:grep_command = 'grep --color=never -e' + endif +endif +let s:hunk_re = '^@@ -\(\d\+\),\?\(\d*\) +\(\d\+\),\?\(\d*\) @@' + +let s:c_flag = gitgutter#utility#git_supports_command_line_config_override() + +let s:temp_index = tempname() +let s:temp_buffer = tempname() + +" Returns a diff of the buffer. +" +" The way to get the diff depends on whether the buffer is saved or unsaved. +" +" * Saved: the buffer contents is the same as the file on disk in the working +" tree so we simply do: +" +" git diff myfile +" +" * Unsaved: the buffer contents is not the same as the file on disk so we +" need to pass two instances of the file to git-diff: +" +" git diff myfileA myfileB +" +" The first instance is the file in the index which we obtain with: +" +" git show :myfile > myfileA +" +" The second instance is the buffer contents. Ideally we would pass this to +" git-diff on stdin via the second argument to vim's system() function. +" Unfortunately git-diff does not do CRLF conversion for input received on +" stdin, and git-show never performs CRLF conversion, so repos with CRLF +" conversion report that every line is modified due to mismatching EOLs. +" +" Instead, we write the buffer contents to a temporary file - myfileB in this +" example. Note the file extension must be preserved for the CRLF +" conversion to work. +" +" Before diffing a buffer for the first time, we check whether git knows about +" the file: +" +" git ls-files --error-unmatch myfile +" +" After running the diff we pass it through grep where available to reduce +" subsequent processing by the plugin. If grep is not available the plugin +" does the filtering instead. +function! gitgutter#diff#run_diff(realtime, preserve_full_diff) abort + " Wrap compound commands in parentheses to make Windows happy. + " bash doesn't mind the parentheses. + let cmd = '(' + + let bufnr = gitgutter#utility#bufnr() + let tracked = getbufvar(bufnr, 'gitgutter_tracked') " i.e. tracked by git + if !tracked + " Don't bother trying to realtime-diff an untracked file. + " NOTE: perhaps we should pull this guard up to the caller? + if a:realtime + throw 'diff failed' + else + let cmd .= g:gitgutter_git_executable.' ls-files --error-unmatch '.gitgutter#utility#shellescape(gitgutter#utility#filename()).' && (' + endif + endif + + if a:realtime + let blob_name = g:gitgutter_diff_base.':'.gitgutter#utility#shellescape(gitgutter#utility#file_relative_to_repo_root()) + let blob_file = s:temp_index + let buff_file = s:temp_buffer + let extension = gitgutter#utility#extension() + if !empty(extension) + let blob_file .= '.'.extension + let buff_file .= '.'.extension + endif + let cmd .= g:gitgutter_git_executable.' show '.blob_name.' > '.blob_file.' && ' + + " Writing the whole buffer resets the '[ and '] marks and also the + " 'modified' flag (if &cpoptions includes '+'). These are unwanted + " side-effects so we save and restore the values ourselves. + let modified = getbufvar(bufnr, "&mod") + let op_mark_start = getpos("'[") + let op_mark_end = getpos("']") + + execute 'keepalt noautocmd silent write!' buff_file + + call setbufvar(bufnr, "&mod", modified) + call setpos("'[", op_mark_start) + call setpos("']", op_mark_end) + endif + + let cmd .= g:gitgutter_git_executable + if s:c_flag + let cmd .= ' -c "diff.autorefreshindex=0"' + endif + let cmd .= ' diff --no-ext-diff --no-color -U0 '.g:gitgutter_diff_args.' ' + + if a:realtime + let cmd .= ' -- '.blob_file.' '.buff_file + else + let cmd .= g:gitgutter_diff_base.' -- '.gitgutter#utility#shellescape(gitgutter#utility#filename()) + endif + + if !a:preserve_full_diff && s:grep_available + let cmd .= ' | '.s:grep_command.' '.gitgutter#utility#shellescape('^@@ ') + endif + + if (!a:preserve_full_diff && s:grep_available) || a:realtime + " grep exits with 1 when no matches are found; diff exits with 1 when + " differences are found. However we want to treat non-matches and + " differences as non-erroneous behaviour; so we OR the command with one + " which always exits with success (0). + let cmd .= ' || exit 0' + endif + + let cmd .= ')' + + if !tracked + let cmd .= ')' + endif + + let cmd = gitgutter#utility#command_in_directory_of_file(cmd) + + if g:gitgutter_async && gitgutter#async#available() && !a:preserve_full_diff + call gitgutter#async#execute(cmd) + return 'async' + + else + let diff = gitgutter#utility#system(cmd) + + if gitgutter#utility#shell_error() + " A shell error indicates the file is not tracked by git (unless something bizarre is going on). + throw 'diff failed' + endif + + return diff + endif +endfunction + +function! gitgutter#diff#parse_diff(diff) abort + let hunks = [] + for line in split(a:diff, '\n') + let hunk_info = gitgutter#diff#parse_hunk(line) + if len(hunk_info) == 4 + call add(hunks, hunk_info) + endif + endfor + return hunks +endfunction + +function! gitgutter#diff#parse_hunk(line) abort + let matches = matchlist(a:line, s:hunk_re) + if len(matches) > 0 + let from_line = str2nr(matches[1]) + let from_count = (matches[2] == '') ? 1 : str2nr(matches[2]) + let to_line = str2nr(matches[3]) + let to_count = (matches[4] == '') ? 1 : str2nr(matches[4]) + return [from_line, from_count, to_line, to_count] + else + return [] + end +endfunction + +function! gitgutter#diff#process_hunks(hunks) abort + call gitgutter#hunk#reset() + let modified_lines = [] + for hunk in a:hunks + call extend(modified_lines, gitgutter#diff#process_hunk(hunk)) + endfor + return modified_lines +endfunction + +" Returns [ [<line_number (number)>, <name (string)>], ...] +function! gitgutter#diff#process_hunk(hunk) abort + let modifications = [] + let from_line = a:hunk[0] + let from_count = a:hunk[1] + let to_line = a:hunk[2] + let to_count = a:hunk[3] + + if gitgutter#diff#is_added(from_count, to_count) + call gitgutter#diff#process_added(modifications, from_count, to_count, to_line) + call gitgutter#hunk#increment_lines_added(to_count) + + elseif gitgutter#diff#is_removed(from_count, to_count) + call gitgutter#diff#process_removed(modifications, from_count, to_count, to_line) + call gitgutter#hunk#increment_lines_removed(from_count) + + elseif gitgutter#diff#is_modified(from_count, to_count) + call gitgutter#diff#process_modified(modifications, from_count, to_count, to_line) + call gitgutter#hunk#increment_lines_modified(to_count) + + elseif gitgutter#diff#is_modified_and_added(from_count, to_count) + call gitgutter#diff#process_modified_and_added(modifications, from_count, to_count, to_line) + call gitgutter#hunk#increment_lines_added(to_count - from_count) + call gitgutter#hunk#increment_lines_modified(from_count) + + elseif gitgutter#diff#is_modified_and_removed(from_count, to_count) + call gitgutter#diff#process_modified_and_removed(modifications, from_count, to_count, to_line) + call gitgutter#hunk#increment_lines_modified(to_count) + call gitgutter#hunk#increment_lines_removed(from_count - to_count) + + endif + return modifications +endfunction + +function! gitgutter#diff#is_added(from_count, to_count) abort + return a:from_count == 0 && a:to_count > 0 +endfunction + +function! gitgutter#diff#is_removed(from_count, to_count) abort + return a:from_count > 0 && a:to_count == 0 +endfunction + +function! gitgutter#diff#is_modified(from_count, to_count) abort + return a:from_count > 0 && a:to_count > 0 && a:from_count == a:to_count +endfunction + +function! gitgutter#diff#is_modified_and_added(from_count, to_count) abort + return a:from_count > 0 && a:to_count > 0 && a:from_count < a:to_count +endfunction + +function! gitgutter#diff#is_modified_and_removed(from_count, to_count) abort + return a:from_count > 0 && a:to_count > 0 && a:from_count > a:to_count +endfunction + +function! gitgutter#diff#process_added(modifications, from_count, to_count, to_line) abort + let offset = 0 + while offset < a:to_count + let line_number = a:to_line + offset + call add(a:modifications, [line_number, 'added']) + let offset += 1 + endwhile +endfunction + +function! gitgutter#diff#process_removed(modifications, from_count, to_count, to_line) abort + if a:to_line == 0 + call add(a:modifications, [1, 'removed_first_line']) + else + call add(a:modifications, [a:to_line, 'removed']) + endif +endfunction + +function! gitgutter#diff#process_modified(modifications, from_count, to_count, to_line) abort + let offset = 0 + while offset < a:to_count + let line_number = a:to_line + offset + call add(a:modifications, [line_number, 'modified']) + let offset += 1 + endwhile +endfunction + +function! gitgutter#diff#process_modified_and_added(modifications, from_count, to_count, to_line) abort + let offset = 0 + while offset < a:from_count + let line_number = a:to_line + offset + call add(a:modifications, [line_number, 'modified']) + let offset += 1 + endwhile + while offset < a:to_count + let line_number = a:to_line + offset + call add(a:modifications, [line_number, 'added']) + let offset += 1 + endwhile +endfunction + +function! gitgutter#diff#process_modified_and_removed(modifications, from_count, to_count, to_line) abort + let offset = 0 + while offset < a:to_count + let line_number = a:to_line + offset + call add(a:modifications, [line_number, 'modified']) + let offset += 1 + endwhile + let a:modifications[-1] = [a:to_line + offset - 1, 'modified_removed'] +endfunction + +" Generates a zero-context diff for the current hunk. +" +" diff - the full diff for the buffer +" type - stage | undo | preview +function! gitgutter#diff#generate_diff_for_hunk(diff, type) abort + let diff_for_hunk = gitgutter#diff#discard_hunks(a:diff, a:type == 'stage' || a:type == 'undo') + + if a:type == 'stage' || a:type == 'undo' + let diff_for_hunk = gitgutter#diff#adjust_hunk_summary(diff_for_hunk, a:type == 'stage') + endif + + return diff_for_hunk +endfunction + +" Returns the diff with all hunks discarded except the current. +" +" diff - the diff to process +" keep_header - truthy to keep the diff header and hunk summary, falsy to discard it +function! gitgutter#diff#discard_hunks(diff, keep_header) abort + let modified_diff = [] + let keep_line = a:keep_header + for line in split(a:diff, '\n') + let hunk_info = gitgutter#diff#parse_hunk(line) + if len(hunk_info) == 4 " start of new hunk + let keep_line = gitgutter#hunk#cursor_in_hunk(hunk_info) + endif + if keep_line + call add(modified_diff, line) + endif + endfor + + if a:keep_header + return gitgutter#utility#stringify(modified_diff) + else + " Discard hunk summary too. + return gitgutter#utility#stringify(modified_diff[1:]) + endif +endfunction + +" Adjust hunk summary (from's / to's line number) to ignore changes above/before this one. +" +" diff_for_hunk - a diff containing only the hunk of interest +" staging - truthy if the hunk is to be staged, falsy if it is to be undone +" +" TODO: push this down to #discard_hunks? +function! gitgutter#diff#adjust_hunk_summary(diff_for_hunk, staging) abort + let line_adjustment = gitgutter#hunk#line_adjustment_for_current_hunk() + let adj_diff = [] + for line in split(a:diff_for_hunk, '\n') + if match(line, s:hunk_re) != -1 + if a:staging + " increment 'to' line number + let line = substitute(line, '+\@<=\(\d\+\)', '\=submatch(1)+line_adjustment', '') + else + " decrement 'from' line number + let line = substitute(line, '-\@<=\(\d\+\)', '\=submatch(1)-line_adjustment', '') + endif + endif + call add(adj_diff, line) + endfor + return gitgutter#utility#stringify(adj_diff) +endfunction + diff --git a/vim/bundle/vim-gitgutter/autoload/gitgutter/highlight.vim b/vim/bundle/vim-gitgutter/autoload/gitgutter/highlight.vim new file mode 100644 index 0000000..e3b774b --- /dev/null +++ b/vim/bundle/vim-gitgutter/autoload/gitgutter/highlight.vim @@ -0,0 +1,115 @@ +function! gitgutter#highlight#define_sign_column_highlight() abort + if g:gitgutter_override_sign_column_highlight + highlight! link SignColumn LineNr + else + highlight default link SignColumn LineNr + endif +endfunction + +function! gitgutter#highlight#define_highlights() abort + let [guibg, ctermbg] = gitgutter#highlight#get_background_colors('SignColumn') + + " Highlights used by the signs. + + execute "highlight GitGutterAddDefault guifg=#009900 guibg=" . guibg . " ctermfg=2 ctermbg=" . ctermbg + execute "highlight GitGutterChangeDefault guifg=#bbbb00 guibg=" . guibg . " ctermfg=3 ctermbg=" . ctermbg + execute "highlight GitGutterDeleteDefault guifg=#ff2222 guibg=" . guibg . " ctermfg=1 ctermbg=" . ctermbg + highlight default link GitGutterChangeDeleteDefault GitGutterChangeDefault + + execute "highlight GitGutterAddInvisible guifg=bg guibg=" . guibg . " ctermfg=" . ctermbg . " ctermbg=" . ctermbg + execute "highlight GitGutterChangeInvisible guifg=bg guibg=" . guibg . " ctermfg=" . ctermbg . " ctermbg=" . ctermbg + execute "highlight GitGutterDeleteInvisible guifg=bg guibg=" . guibg . " ctermfg=" . ctermbg . " ctermbg=" . ctermbg + highlight default link GitGutterChangeDeleteInvisible GitGutterChangeInvisble + + highlight default link GitGutterAdd GitGutterAddDefault + highlight default link GitGutterChange GitGutterChangeDefault + highlight default link GitGutterDelete GitGutterDeleteDefault + highlight default link GitGutterChangeDelete GitGutterChangeDeleteDefault + + " Highlights used for the whole line. + + highlight default link GitGutterAddLine DiffAdd + highlight default link GitGutterChangeLine DiffChange + highlight default link GitGutterDeleteLine DiffDelete + highlight default link GitGutterChangeDeleteLine GitGutterChangeLine +endfunction + +function! gitgutter#highlight#define_signs() abort + sign define GitGutterLineAdded + sign define GitGutterLineModified + sign define GitGutterLineRemoved + sign define GitGutterLineRemovedFirstLine + sign define GitGutterLineModifiedRemoved + sign define GitGutterDummy + + call gitgutter#highlight#define_sign_text() + call gitgutter#highlight#define_sign_text_highlights() + call gitgutter#highlight#define_sign_line_highlights() +endfunction + +function! gitgutter#highlight#define_sign_text() abort + execute "sign define GitGutterLineAdded text=" . g:gitgutter_sign_added + execute "sign define GitGutterLineModified text=" . g:gitgutter_sign_modified + execute "sign define GitGutterLineRemoved text=" . g:gitgutter_sign_removed + execute "sign define GitGutterLineRemovedFirstLine text=" . g:gitgutter_sign_removed_first_line + execute "sign define GitGutterLineModifiedRemoved text=" . g:gitgutter_sign_modified_removed +endfunction + +function! gitgutter#highlight#define_sign_text_highlights() abort + " Once a sign's text attribute has been defined, it cannot be undefined or + " set to an empty value. So to make signs' text disappear (when toggling + " off or disabling) we make them invisible by setting their foreground colours + " to the background's. + if g:gitgutter_signs + sign define GitGutterLineAdded texthl=GitGutterAdd + sign define GitGutterLineModified texthl=GitGutterChange + sign define GitGutterLineRemoved texthl=GitGutterDelete + sign define GitGutterLineRemovedFirstLine texthl=GitGutterDelete + sign define GitGutterLineModifiedRemoved texthl=GitGutterChangeDelete + else + sign define GitGutterLineAdded texthl=GitGutterAddInvisible + sign define GitGutterLineModified texthl=GitGutterChangeInvisible + sign define GitGutterLineRemoved texthl=GitGutterDeleteInvisible + sign define GitGutterLineRemovedFirstLine texthl=GitGutterDeleteInvisible + sign define GitGutterLineModifiedRemoved texthl=GitGutterChangeDeleteInvisible + endif +endfunction + +function! gitgutter#highlight#define_sign_line_highlights() abort + if g:gitgutter_highlight_lines + sign define GitGutterLineAdded linehl=GitGutterAddLine + sign define GitGutterLineModified linehl=GitGutterChangeLine + sign define GitGutterLineRemoved linehl=GitGutterDeleteLine + sign define GitGutterLineRemovedFirstLine linehl=GitGutterDeleteLine + sign define GitGutterLineModifiedRemoved linehl=GitGutterChangeDeleteLine + else + sign define GitGutterLineAdded linehl= + sign define GitGutterLineModified linehl= + sign define GitGutterLineRemoved linehl= + sign define GitGutterLineRemovedFirstLine linehl= + sign define GitGutterLineModifiedRemoved linehl= + endif +endfunction + +function! gitgutter#highlight#get_background_colors(group) abort + redir => highlight + silent execute 'silent highlight ' . a:group + redir END + + let link_matches = matchlist(highlight, 'links to \(\S\+\)') + if len(link_matches) > 0 " follow the link + return gitgutter#highlight#get_background_colors(link_matches[1]) + endif + + let ctermbg = gitgutter#highlight#match_highlight(highlight, 'ctermbg=\([0-9A-Za-z]\+\)') + let guibg = gitgutter#highlight#match_highlight(highlight, 'guibg=\([#0-9A-Za-z]\+\)') + return [guibg, ctermbg] +endfunction + +function! gitgutter#highlight#match_highlight(highlight, pattern) abort + let matches = matchlist(a:highlight, a:pattern) + if len(matches) == 0 + return 'NONE' + endif + return matches[1] +endfunction diff --git a/vim/bundle/vim-gitgutter/autoload/gitgutter/hunk.vim b/vim/bundle/vim-gitgutter/autoload/gitgutter/hunk.vim new file mode 100644 index 0000000..0fd0246 --- /dev/null +++ b/vim/bundle/vim-gitgutter/autoload/gitgutter/hunk.vim @@ -0,0 +1,137 @@ +let s:hunks = [] + +function! gitgutter#hunk#set_hunks(hunks) abort + let s:hunks = a:hunks +endfunction + +function! gitgutter#hunk#hunks() abort + return s:hunks +endfunction + +function! gitgutter#hunk#summary(bufnr) abort + return get(getbufvar(a:bufnr,''), 'gitgutter_summary', [0,0,0]) +endfunction + +function! gitgutter#hunk#reset() abort + call setbufvar(gitgutter#utility#bufnr(), 'gitgutter_summary', [0,0,0]) +endfunction + +function! gitgutter#hunk#increment_lines_added(count) abort + let bufnr = gitgutter#utility#bufnr() + let summary = gitgutter#hunk#summary(bufnr) + let summary[0] += a:count + call setbufvar(bufnr, 'gitgutter_summary', summary) +endfunction + +function! gitgutter#hunk#increment_lines_modified(count) abort + let bufnr = gitgutter#utility#bufnr() + let summary = gitgutter#hunk#summary(bufnr) + let summary[1] += a:count + call setbufvar(bufnr, 'gitgutter_summary', summary) +endfunction + +function! gitgutter#hunk#increment_lines_removed(count) abort + let bufnr = gitgutter#utility#bufnr() + let summary = gitgutter#hunk#summary(bufnr) + let summary[2] += a:count + call setbufvar(bufnr, 'gitgutter_summary', summary) +endfunction + +function! gitgutter#hunk#next_hunk(count) abort + if gitgutter#utility#is_active() + let current_line = line('.') + let hunk_count = 0 + for hunk in s:hunks + if hunk[2] > current_line + let hunk_count += 1 + if hunk_count == a:count + execute 'normal!' hunk[2] . 'G' + return + endif + endif + endfor + call gitgutter#utility#warn('No more hunks') + endif +endfunction + +function! gitgutter#hunk#prev_hunk(count) abort + if gitgutter#utility#is_active() + let current_line = line('.') + let hunk_count = 0 + for hunk in reverse(copy(s:hunks)) + if hunk[2] < current_line + let hunk_count += 1 + if hunk_count == a:count + let target = hunk[2] == 0 ? 1 : hunk[2] + execute 'normal!' target . 'G' + return + endif + endif + endfor + call gitgutter#utility#warn('No previous hunks') + endif +endfunction + +" Returns the hunk the cursor is currently in or an empty list if the cursor +" isn't in a hunk. +function! gitgutter#hunk#current_hunk() abort + let current_hunk = [] + + for hunk in s:hunks + if gitgutter#hunk#cursor_in_hunk(hunk) + let current_hunk = hunk + break + endif + endfor + + return current_hunk +endfunction + +function! gitgutter#hunk#cursor_in_hunk(hunk) abort + let current_line = line('.') + + if current_line == 1 && a:hunk[2] == 0 + return 1 + endif + + if current_line >= a:hunk[2] && current_line < a:hunk[2] + (a:hunk[3] == 0 ? 1 : a:hunk[3]) + return 1 + endif + + return 0 +endfunction + +" Returns the number of lines the current hunk is offset from where it would +" be if any changes above it in the file didn't exist. +function! gitgutter#hunk#line_adjustment_for_current_hunk() abort + let adj = 0 + for hunk in s:hunks + if gitgutter#hunk#cursor_in_hunk(hunk) + break + else + let adj += hunk[1] - hunk[3] + endif + endfor + return adj +endfunction + +function! gitgutter#hunk#text_object(inner) abort + let hunk = gitgutter#hunk#current_hunk() + + if empty(hunk) + return + endif + + let [first_line, last_line] = [hunk[2], hunk[2] + hunk[3] - 1] + + if ! a:inner + let lnum = last_line + let eof = line('$') + while lnum < eof && empty(getline(lnum + 1)) + let lnum +=1 + endwhile + let last_line = lnum + endif + + execute 'normal! 'first_line.'GV'.last_line.'G' +endfunction diff --git a/vim/bundle/vim-gitgutter/autoload/gitgutter/sign.vim b/vim/bundle/vim-gitgutter/autoload/gitgutter/sign.vim new file mode 100644 index 0000000..6bd5efa --- /dev/null +++ b/vim/bundle/vim-gitgutter/autoload/gitgutter/sign.vim @@ -0,0 +1,173 @@ +" Vim doesn't namespace sign ids so every plugin shares the same +" namespace. Sign ids are simply integers so to avoid clashes with other +" signs we guess at a clear run. +" +" Note also we currently never reset s:next_sign_id. +let s:first_sign_id = 3000 +let s:next_sign_id = s:first_sign_id +let s:dummy_sign_id = s:first_sign_id - 1 +" Remove-all-signs optimisation requires Vim 7.3.596+. +let s:supports_star = v:version > 703 || (v:version == 703 && has("patch596")) + + +" Removes gitgutter's signs (excluding dummy sign) from the buffer being processed. +function! gitgutter#sign#clear_signs() abort + let bufnr = gitgutter#utility#bufnr() + call gitgutter#sign#find_current_signs() + + let sign_ids = map(values(getbufvar(bufnr, 'gitgutter_gitgutter_signs')), 'v:val.id') + call gitgutter#sign#remove_signs(sign_ids, 1) + call setbufvar(bufnr, 'gitgutter_gitgutter_signs', {}) +endfunction + + +" Updates gitgutter's signs in the buffer being processed. +" +" modified_lines: list of [<line_number (number)>, <name (string)>] +" where name = 'added|removed|modified|modified_removed' +function! gitgutter#sign#update_signs(modified_lines) abort + call gitgutter#sign#find_current_signs() + + let new_gitgutter_signs_line_numbers = map(copy(a:modified_lines), 'v:val[0]') + let obsolete_signs = gitgutter#sign#obsolete_gitgutter_signs_to_remove(new_gitgutter_signs_line_numbers) + + let flicker_possible = s:remove_all_old_signs && !empty(a:modified_lines) + if flicker_possible + call gitgutter#sign#add_dummy_sign() + endif + + call gitgutter#sign#remove_signs(obsolete_signs, s:remove_all_old_signs) + call gitgutter#sign#upsert_new_gitgutter_signs(a:modified_lines) + + if flicker_possible + call gitgutter#sign#remove_dummy_sign(0) + endif +endfunction + + +function! gitgutter#sign#add_dummy_sign() abort + let bufnr = gitgutter#utility#bufnr() + if !getbufvar(bufnr, 'gitgutter_dummy_sign') + execute "sign place" s:dummy_sign_id "line=" . 9999 "name=GitGutterDummy buffer=" . bufnr + call setbufvar(bufnr, 'gitgutter_dummy_sign', 1) + endif +endfunction + +function! gitgutter#sign#remove_dummy_sign(force) abort + let bufnr = gitgutter#utility#bufnr() + if getbufvar(bufnr, 'gitgutter_dummy_sign') && (a:force || !g:gitgutter_sign_column_always) + execute "sign unplace" s:dummy_sign_id "buffer=" . bufnr + call setbufvar(bufnr, 'gitgutter_dummy_sign', 0) + endif +endfunction + + +" +" Internal functions +" + + +function! gitgutter#sign#find_current_signs() abort + let bufnr = gitgutter#utility#bufnr() + let gitgutter_signs = {} " <line_number (string)>: {'id': <id (number)>, 'name': <name (string)>} + let other_signs = [] " [<line_number (number),...] + let dummy_sign_placed = 0 + + redir => signs + silent execute "sign place buffer=" . bufnr + redir END + + for sign_line in filter(split(signs, '\n')[2:], 'v:val =~# "="') + " Typical sign line: line=88 id=1234 name=GitGutterLineAdded + " We assume splitting is faster than a regexp. + let components = split(sign_line) + let name = split(components[2], '=')[1] + if name =~# 'GitGutterDummy' + let dummy_sign_placed = 1 + else + let line_number = str2nr(split(components[0], '=')[1]) + if name =~# 'GitGutter' + let id = str2nr(split(components[1], '=')[1]) + " Remove orphaned signs (signs placed on lines which have been deleted). + " (When a line is deleted its sign lingers. Subsequent lines' signs' + " line numbers are decremented appropriately.) + if has_key(gitgutter_signs, line_number) + execute "sign unplace" gitgutter_signs[line_number].id + endif + let gitgutter_signs[line_number] = {'id': id, 'name': name} + else + call add(other_signs, line_number) + endif + end + endfor + + call setbufvar(bufnr, 'gitgutter_dummy_sign', dummy_sign_placed) + call setbufvar(bufnr, 'gitgutter_gitgutter_signs', gitgutter_signs) + call setbufvar(bufnr, 'gitgutter_other_signs', other_signs) +endfunction + + +" Returns a list of [<id (number)>, ...] +" Sets `s:remove_all_old_signs` as a side-effect. +function! gitgutter#sign#obsolete_gitgutter_signs_to_remove(new_gitgutter_signs_line_numbers) abort + let bufnr = gitgutter#utility#bufnr() + let signs_to_remove = [] " list of [<id (number)>, ...] + let remove_all_signs = 1 + let old_gitgutter_signs = getbufvar(bufnr, 'gitgutter_gitgutter_signs') + for line_number in keys(old_gitgutter_signs) + if index(a:new_gitgutter_signs_line_numbers, str2nr(line_number)) == -1 + call add(signs_to_remove, old_gitgutter_signs[line_number].id) + else + let remove_all_signs = 0 + endif + endfor + let s:remove_all_old_signs = remove_all_signs + return signs_to_remove +endfunction + + +function! gitgutter#sign#remove_signs(sign_ids, all_signs) abort + let bufnr = gitgutter#utility#bufnr() + if a:all_signs && s:supports_star && empty(getbufvar(bufnr, 'gitgutter_other_signs')) + let dummy_sign_present = getbufvar(bufnr, 'gitgutter_dummy_sign') + execute "sign unplace * buffer=" . bufnr + if dummy_sign_present + execute "sign place" s:dummy_sign_id "line=" . 9999 "name=GitGutterDummy buffer=" . bufnr + endif + else + for id in a:sign_ids + execute "sign unplace" id + endfor + endif +endfunction + + +function! gitgutter#sign#upsert_new_gitgutter_signs(modified_lines) abort + let bufnr = gitgutter#utility#bufnr() + let other_signs = getbufvar(bufnr, 'gitgutter_other_signs') + let old_gitgutter_signs = getbufvar(bufnr, 'gitgutter_gitgutter_signs') + + for line in a:modified_lines + let line_number = line[0] " <number> + if index(other_signs, line_number) == -1 " don't clobber others' signs + let name = gitgutter#utility#highlight_name_for_change(line[1]) + if !has_key(old_gitgutter_signs, line_number) " insert + let id = gitgutter#sign#next_sign_id() + execute "sign place" id "line=" . line_number "name=" . name "buffer=" . bufnr + else " update if sign has changed + let old_sign = old_gitgutter_signs[line_number] + if old_sign.name !=# name + execute "sign place" old_sign.id "name=" . name "buffer=" . bufnr + end + endif + endif + endfor + " At this point b:gitgutter_gitgutter_signs is out of date. +endfunction + + +function! gitgutter#sign#next_sign_id() abort + let next_id = s:next_sign_id + let s:next_sign_id += 1 + return next_id +endfunction diff --git a/vim/bundle/vim-gitgutter/autoload/gitgutter/utility.vim b/vim/bundle/vim-gitgutter/autoload/gitgutter/utility.vim new file mode 100644 index 0000000..3135453 --- /dev/null +++ b/vim/bundle/vim-gitgutter/autoload/gitgutter/utility.vim @@ -0,0 +1,199 @@ +let s:file = '' +let s:using_xolox_shell = -1 +let s:exit_code = 0 + +function! gitgutter#utility#warn(message) abort + echohl WarningMsg + echo 'vim-gitgutter: ' . a:message + echohl None + let v:warningmsg = a:message +endfunction + +function! gitgutter#utility#warn_once(message, key) abort + if empty(getbufvar(s:bufnr, a:key)) + call setbufvar(s:bufnr, a:key, '1') + echohl WarningMsg + redraw | echo 'vim-gitgutter: ' . a:message + echohl None + let v:warningmsg = a:message + endif +endfunction + +" Returns truthy when the buffer's file should be processed; and falsey when it shouldn't. +" This function does not and should not make any system calls. +function! gitgutter#utility#is_active() abort + return g:gitgutter_enabled && + \ !pumvisible() && + \ gitgutter#utility#is_file_buffer() && + \ gitgutter#utility#exists_file() && + \ gitgutter#utility#not_git_dir() +endfunction + +function! gitgutter#utility#not_git_dir() abort + return gitgutter#utility#full_path_to_directory_of_file() !~ '[/\\]\.git\($\|[/\\]\)' +endfunction + +function! gitgutter#utility#is_file_buffer() abort + return empty(getbufvar(s:bufnr, '&buftype')) +endfunction + +" A replacement for the built-in `shellescape(arg)`. +" +" Recent versions of Vim handle shell escaping pretty well. However older +" versions aren't as good. This attempts to do the right thing. +" +" See: +" https://github.com/tpope/vim-fugitive/blob/8f0b8edfbd246c0026b7a2388e1d883d579ac7f6/plugin/fugitive.vim#L29-L37 +function! gitgutter#utility#shellescape(arg) abort + if a:arg =~ '^[A-Za-z0-9_/.-]\+$' + return a:arg + elseif &shell =~# 'cmd' || gitgutter#utility#using_xolox_shell() + return '"' . substitute(substitute(a:arg, '"', '""', 'g'), '%', '"%"', 'g') . '"' + else + return shellescape(a:arg) + endif +endfunction + +function! gitgutter#utility#set_buffer(bufnr) abort + let s:bufnr = a:bufnr + let s:file = resolve(bufname(a:bufnr)) +endfunction + +function! gitgutter#utility#bufnr() + return s:bufnr +endfunction + +function! gitgutter#utility#file() + return s:file +endfunction + +function! gitgutter#utility#filename() abort + return fnamemodify(s:file, ':t') +endfunction + +function! gitgutter#utility#extension() abort + return fnamemodify(s:file, ':e') +endfunction + +function! gitgutter#utility#full_path_to_directory_of_file() abort + return fnamemodify(s:file, ':p:h') +endfunction + +function! gitgutter#utility#directory_of_file() abort + return fnamemodify(s:file, ':h') +endfunction + +function! gitgutter#utility#exists_file() abort + return filereadable(s:file) +endfunction + +function! gitgutter#utility#has_unsaved_changes() abort + return getbufvar(s:bufnr, "&mod") +endfunction + +function! gitgutter#utility#has_fresh_changes() abort + return getbufvar(s:bufnr, 'changedtick') != getbufvar(s:bufnr, 'gitgutter_last_tick') +endfunction + +function! gitgutter#utility#save_last_seen_change() abort + call setbufvar(s:bufnr, 'gitgutter_last_tick', getbufvar(s:bufnr, 'changedtick')) +endfunction + +function! gitgutter#utility#shell_error() abort + return gitgutter#utility#using_xolox_shell() ? s:exit_code : v:shell_error +endfunction + +function! gitgutter#utility#using_xolox_shell() abort + if s:using_xolox_shell == -1 + if !g:gitgutter_avoid_cmd_prompt_on_windows + let s:using_xolox_shell = 0 + " Although xolox/vim-shell works on both windows and unix we only want to use + " it on windows. + elseif has('win32') || has('win64') || has('win32unix') + let s:using_xolox_shell = exists('g:xolox#misc#version') && exists('g:xolox#shell#version') + else + let s:using_xolox_shell = 0 + endif + endif + return s:using_xolox_shell +endfunction + +function! gitgutter#utility#system(cmd, ...) abort + call gitgutter#debug#log(a:cmd, a:000) + + if gitgutter#utility#using_xolox_shell() + let options = {'command': a:cmd, 'check': 0} + if a:0 > 0 + let options['stdin'] = a:1 + endif + let ret = xolox#misc#os#exec(options) + let output = join(ret.stdout, "\n") + let s:exit_code = ret.exit_code + else + silent let output = (a:0 == 0) ? system(a:cmd) : system(a:cmd, a:1) + endif + return output +endfunction + +function! gitgutter#utility#file_relative_to_repo_root() abort + let file_path_relative_to_repo_root = getbufvar(s:bufnr, 'gitgutter_repo_relative_path') + if empty(file_path_relative_to_repo_root) + let dir_path_relative_to_repo_root = gitgutter#utility#system(gitgutter#utility#command_in_directory_of_file(g:gitgutter_git_executable.' rev-parse --show-prefix')) + let dir_path_relative_to_repo_root = gitgutter#utility#strip_trailing_new_line(dir_path_relative_to_repo_root) + let file_path_relative_to_repo_root = dir_path_relative_to_repo_root . gitgutter#utility#filename() + call setbufvar(s:bufnr, 'gitgutter_repo_relative_path', file_path_relative_to_repo_root) + endif + return file_path_relative_to_repo_root +endfunction + +function! gitgutter#utility#command_in_directory_of_file(cmd) abort + return 'cd '.gitgutter#utility#shellescape(gitgutter#utility#directory_of_file()).' && '.a:cmd +endfunction + +function! gitgutter#utility#highlight_name_for_change(text) abort + if a:text ==# 'added' + return 'GitGutterLineAdded' + elseif a:text ==# 'removed' + return 'GitGutterLineRemoved' + elseif a:text ==# 'removed_first_line' + return 'GitGutterLineRemovedFirstLine' + elseif a:text ==# 'modified' + return 'GitGutterLineModified' + elseif a:text ==# 'modified_removed' + return 'GitGutterLineModifiedRemoved' + endif +endfunction + +function! gitgutter#utility#strip_trailing_new_line(line) abort + return substitute(a:line, '\n$', '', '') +endfunction + +function! gitgutter#utility#git_version() abort + return matchstr(system(g:gitgutter_git_executable.' --version'), '[0-9.]\+') +endfunction + +" True for git v1.7.2+. +function! gitgutter#utility#git_supports_command_line_config_override() abort + let [major, minor, patch; _] = split(gitgutter#utility#git_version(), '\.') + return major > 1 || (major == 1 && minor > 7) || (minor == 7 && patch > 1) +endfunction + +function! gitgutter#utility#stringify(list) abort + return join(a:list, "\n")."\n" +endfunction + +function! gitgutter#utility#use_known_shell() abort + if has('unix') + let s:shell = &shell + let s:shellcmdflag = &shellcmdflag + set shell=/bin/sh + set shellcmdflag=-c + endif +endfunction + +function! gitgutter#utility#restore_shell() abort + if has('unix') + let &shell = s:shell + let &shellcmdflag = s:shellcmdflag + endif +endfunction |