Skip to content

Commit 5113cf8

Browse files
committed
Add new 'auto_format_git_diff' behavior
When you edit a file tracked in git, it may happend that some tracked lines are not well formatted. If you modify this file, and you auto_format the file, lines with bad indentation will be formatted too. You will be end up with unrelated changes in your commit. It may even appear that these 'bad formatted lines' were indented like this with a purpose. To mitigate this issue, this commit proposes to only format diff'ed lines in a file. It roughly relies on git diff, and on clang-format '-lines' option. clang_format#format function has been modified to accept a list of ranges. It adds three new user options: - g:clang_format#auto_format_git_diff to enable this new behavior - g:clang_format#auto_format_git_diff_fallback to decide what to do in case of untracked file - g:clang_format#git to define non standard git path or options
1 parent 95593b6 commit 5113cf8

File tree

3 files changed

+138
-5
lines changed

3 files changed

+138
-5
lines changed

autoload/clang_format.vim

+113-4
Original file line numberDiff line numberDiff line change
@@ -139,6 +139,14 @@ function! clang_format#is_invalid() abort
139139
let s:version = v
140140
endif
141141

142+
if g:clang_format#auto_format_git_diff &&
143+
\ !exists('s:git_available')
144+
if !executable(g:clang_format#git)
145+
return 1
146+
endif
147+
let s:git_available = 1
148+
endif
149+
142150
return 0
143151
endfunction
144152

@@ -184,6 +192,7 @@ let g:clang_format#extra_args = s:getg('clang_format#extra_args', "")
184192
if type(g:clang_format#extra_args) == type([])
185193
let g:clang_format#extra_args = join(g:clang_format#extra_args, " ")
186194
endif
195+
let g:clang_format#git = s:getg('clang_format#git', 'git')
187196

188197
let g:clang_format#code_style = s:getg('clang_format#code_style', 'google')
189198
let g:clang_format#style_options = s:getg('clang_format#style_options', {})
@@ -193,6 +202,8 @@ let g:clang_format#detect_style_file = s:getg('clang_format#detect_style_file',
193202
let g:clang_format#enable_fallback_style = s:getg('clang_format#enable_fallback_style', 1)
194203

195204
let g:clang_format#auto_format = s:getg('clang_format#auto_format', 0)
205+
let g:clang_format#auto_format_git_diff = s:getg('clang_format#auto_format_git_diff', 0)
206+
let g:clang_format#auto_format_git_diff_fallback = s:getg('clang_format#auto_format_git_diff_fallback', 'file')
196207
let g:clang_format#auto_format_on_insert_leave = s:getg('clang_format#auto_format_on_insert_leave', 0)
197208
let g:clang_format#auto_formatexpr = s:getg('clang_format#auto_formatexpr', 0)
198209
" }}}
@@ -203,8 +214,13 @@ function! s:detect_style_file() abort
203214
return findfile('.clang-format', dirname.';') != '' || findfile('_clang-format', dirname.';') != ''
204215
endfunction
205216

206-
function! clang_format#format(line1, line2) abort
207-
let args = printf(' -lines=%d:%d', a:line1, a:line2)
217+
" clang_format#format_ranges is were the magic happends.
218+
" ranges is a list of pairs, like [[start1,end1],[start2,end2]...]
219+
function! clang_format#format_ranges(ranges) abort
220+
let args = ''
221+
for range in a:ranges
222+
let args .= printf(' -lines=%d:%d', range[0], range[1])
223+
endfor
208224
if ! (g:clang_format#detect_style_file && s:detect_style_file())
209225
if g:clang_format#enable_fallback_style
210226
let args .= ' ' . s:shellescape(printf('-style=%s', s:make_style_options())) . ' '
@@ -223,14 +239,18 @@ function! clang_format#format(line1, line2) abort
223239
let source = join(getline(1, '$'), "\n")
224240
return s:system(clang_format, source)
225241
endfunction
242+
243+
function! clang_format#format(line1, line2) abort
244+
return clang_format#format_ranges([[line1, line2]])
245+
endfunction
226246
" }}}
227247

228248
" replace buffer {{{
229-
function! clang_format#replace(line1, line2, ...) abort
249+
function! clang_format#replace_ranges(ranges, ...) abort
230250
call s:verify_command()
231251

232252
let pos_save = a:0 >= 1 ? a:1 : getpos('.')
233-
let formatted = clang_format#format(a:line1, a:line2)
253+
let formatted = clang_format#format_ranges(a:ranges)
234254
if !s:success(formatted)
235255
call s:error_message(formatted)
236256
return
@@ -247,6 +267,10 @@ function! clang_format#replace(line1, line2, ...) abort
247267
call winrestview(winview)
248268
call setpos('.', pos_save)
249269
endfunction
270+
271+
function! clang_format#replace(line1, line2, ...) abort
272+
call call(function("clang_format#replace_ranges"), [[line1, line2]], a:000)
273+
endfunction
250274
" }}}
251275

252276
" auto formatting on insert leave {{{
@@ -291,6 +315,91 @@ endfunction
291315
function! clang_format#disable_auto_format() abort
292316
let g:clang_format#auto_format = 0
293317
endfunction
318+
319+
" s:strip: helper function to strip a string
320+
function! s:strip(string)
321+
return substitute(a:string, '^\s*\(.\{-}\)\s*\r\=\n\=$', '\1', '')
322+
endfunction
323+
324+
" clang_format#get_git_diff
325+
" a:file must be an absolute path to the file to be processed
326+
" this function compares the current buffer content against the
327+
" git index content of the file.
328+
" this function returns a list of pair of ranges if the file is tracked
329+
" and has changes, an empty list otherwise
330+
function! clang_format#get_git_diff(cur_file)
331+
let file_path = isdirectory(a:cur_file) ? a:cur_file :
332+
\ fnamemodify(a:cur_file, ":h")
333+
let top_dir=s:strip(system(
334+
\ g:clang_format#git." -C ".shellescape(file_path).
335+
\ " rev-parse --show-toplevel"))
336+
if v:shell_error != 0
337+
return []
338+
endif
339+
let cur_file = s:strip(s:system(
340+
\ g:clang_format#git." -C ".shellescape(top_dir).
341+
\ " ls-files --error-unmatch ".shellescape(a:cur_file)))
342+
if v:shell_error != 0
343+
return []
344+
endif
345+
let source = join(getline(1, '$'), "\n")
346+
" git show :file shows the staged content of the file:
347+
" - content in index if any (staged but not commmited)
348+
" - else content in HEAD
349+
" this solution also solves the problem for 'git mv'ed file:
350+
" - if the current buffer has been renamed by simple mv (without git
351+
" add), the file is considered as untracked
352+
" - if the renamed file has been git added or git mv, git show :file
353+
" will show the expected content.
354+
" this barbarian command does the following:
355+
" - diff --*-group-* options will return ranges (start,end) for each
356+
" diff chunk
357+
" - <(git show :file) is a process substitution, using /dev/fd/<n> as
358+
" temporary file for the output
359+
" - - is stdin, which is current buffer content in variable 'source'
360+
let diff_cmd =
361+
\ 'diff <('.g:clang_format#git.' show :'.shellescape(cur_file).') - '.
362+
\ '--old-group-format="" --unchanged-group-format="" '.
363+
\ '--new-group-format="%dF-%dL%c''\\012''" '.
364+
\ '--changed-group-format="%dF-%dL%c''\\012''"'
365+
let ranges = s:system(diff_cmd, source)
366+
if !(v:shell_error == 0 || v:shell_error == 1)
367+
throw printf("clang-format: git diff failed `%s` for ranges %s",
368+
\ diff_cmd, ranges)
369+
endif
370+
let ranges = split(ranges, '\n')
371+
" ranges is now a list of pairs [[start1, end1],[start2,end2]...]
372+
let ranges = map(ranges, "split(v:val, '-')")
373+
return ranges
374+
endfunction
375+
376+
" this function will try to format only buffer lines diffing from git index
377+
" content.
378+
" If the file is untracked (not in a git repo or not tracked in a git repo),
379+
" it returns 1.
380+
" If the format succeeds, it returns 0.
381+
function! clang_format#do_auto_format_git_diff()
382+
let cur_file = expand("%:p")
383+
let ranges = clang_format#get_git_diff(cur_file)
384+
if !empty(ranges)
385+
call clang_format#replace_ranges(ranges)
386+
return 0
387+
else
388+
return 1
389+
endif
390+
endfunction
391+
392+
function! clang_format#do_auto_format()
393+
if g:clang_format#auto_format_git_diff
394+
let ret = clang_format#do_auto_format_git_diff()
395+
if ret == 0 ||
396+
\ g:clang_format#auto_format_git_diff_fallback != 'file'
397+
return
398+
endif
399+
endif
400+
call clang_format#replace_ranges([[1, line('$')]])
401+
endfunction
402+
294403
" }}}
295404
let &cpo = s:save_cpo
296405
unlet s:save_cpo

doc/clang-format.txt

+24
Original file line numberDiff line numberDiff line change
@@ -127,6 +127,11 @@ g:clang_format#command *g:clang_format#command*
127127
Name of clang-format command.
128128
The default value is "clang-format".
129129

130+
g:clang_format#git *g:clang_format#git*
131+
132+
Name of the git command.
133+
The default value is "git".
134+
130135
g:clang_format#code_style *g:clang_format#code_style*
131136

132137
Base coding style for formatting. Available coding styles are "llvm",
@@ -186,6 +191,25 @@ g:clang_format#auto_format *g:clang_format#auto_format*
186191
buffer on saving the buffer. Formatting is executed at |BufWritePre| event.
187192
The default value is 0.
188193

194+
g:clang_format#auto_format_git_diff *g:clang_format#auto_format_git_diff*
195+
196+
When this value is 1, and when g:clang_format#auto_format is 1, the auto
197+
format only formats modified lines is the file is tracked in git.
198+
If the file is not tracked, or even not in a git project, fallback
199+
behavior depends on |g:clang_format#auto_format_git_diff_fallback|.
200+
WARNING: this option should not be used with
201+
|g:clang_format#auto_format_on_insert_leave|.
202+
The default value is 0.
203+
204+
g:clang_format#auto_format_git_diff_fallback
205+
*g:clang_format#auto_format_git_diff_fallback*
206+
207+
Fallback behavior when |g:clang_format#auto_format_git_diff| is 1 and the
208+
current file is not tracked. The value can be:
209+
- 'file': the whole file is formatted (which is the default
210+
|g:clang_format#auto_format| behavior).
211+
- 'pass': the file is not formatted.
212+
189213
g:clang_format#auto_format_on_insert_leave
190214
*g:clang_format#auto_format_on_insert_leave*
191215

plugin/clang_format.vim

+1-1
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ augroup plugin-clang-format-auto-format
2222
\ if &ft =~# '^\%(c\|cpp\|objc\|java\|javascript\|typescript\|proto\|arduino\)$' &&
2323
\ g:clang_format#auto_format &&
2424
\ !clang_format#is_invalid() |
25-
\ call clang_format#replace(1, line('$')) |
25+
\ call clang_format#do_auto_format() |
2626
\ endif
2727
autocmd FileType c,cpp,objc,java,javascript,typescript,proto,arduino
2828
\ if g:clang_format#auto_format_on_insert_leave &&

0 commit comments

Comments
 (0)