hunk.vim 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613
  1. let s:winid = 0
  2. let s:preview_bufnr = 0
  3. let s:nomodeline = (v:version > 703 || (v:version == 703 && has('patch442'))) ? '<nomodeline>' : ''
  4. function! gitgutter#hunk#set_hunks(bufnr, hunks) abort
  5. call gitgutter#utility#setbufvar(a:bufnr, 'hunks', a:hunks)
  6. call s:reset_summary(a:bufnr)
  7. endfunction
  8. function! gitgutter#hunk#hunks(bufnr) abort
  9. return gitgutter#utility#getbufvar(a:bufnr, 'hunks', [])
  10. endfunction
  11. function! gitgutter#hunk#reset(bufnr) abort
  12. call gitgutter#utility#setbufvar(a:bufnr, 'hunks', [])
  13. call s:reset_summary(a:bufnr)
  14. endfunction
  15. function! gitgutter#hunk#summary(bufnr) abort
  16. return gitgutter#utility#getbufvar(a:bufnr, 'summary', [0,0,0])
  17. endfunction
  18. function! s:reset_summary(bufnr) abort
  19. call gitgutter#utility#setbufvar(a:bufnr, 'summary', [0,0,0])
  20. endfunction
  21. function! gitgutter#hunk#increment_lines_added(bufnr, count) abort
  22. let summary = gitgutter#hunk#summary(a:bufnr)
  23. let summary[0] += a:count
  24. call gitgutter#utility#setbufvar(a:bufnr, 'summary', summary)
  25. endfunction
  26. function! gitgutter#hunk#increment_lines_modified(bufnr, count) abort
  27. let summary = gitgutter#hunk#summary(a:bufnr)
  28. let summary[1] += a:count
  29. call gitgutter#utility#setbufvar(a:bufnr, 'summary', summary)
  30. endfunction
  31. function! gitgutter#hunk#increment_lines_removed(bufnr, count) abort
  32. let summary = gitgutter#hunk#summary(a:bufnr)
  33. let summary[2] += a:count
  34. call gitgutter#utility#setbufvar(a:bufnr, 'summary', summary)
  35. endfunction
  36. function! gitgutter#hunk#next_hunk(count) abort
  37. let bufnr = bufnr('')
  38. if !gitgutter#utility#is_active(bufnr) | return | endif
  39. let hunks = gitgutter#hunk#hunks(bufnr)
  40. if empty(hunks)
  41. call gitgutter#utility#warn('No hunks in file')
  42. return
  43. endif
  44. let current_line = line('.')
  45. let hunk_count = 0
  46. for hunk in hunks
  47. if hunk[2] > current_line
  48. let hunk_count += 1
  49. if hunk_count == a:count
  50. execute 'normal!' hunk[2] . 'Gzv'
  51. if g:gitgutter_show_msg_on_hunk_jumping
  52. redraw | echo printf('Hunk %d of %d', index(hunks, hunk) + 1, len(hunks))
  53. endif
  54. if gitgutter#hunk#is_preview_window_open()
  55. call gitgutter#hunk#preview()
  56. endif
  57. return
  58. endif
  59. endif
  60. endfor
  61. call gitgutter#utility#warn('No more hunks')
  62. endfunction
  63. function! gitgutter#hunk#prev_hunk(count) abort
  64. let bufnr = bufnr('')
  65. if !gitgutter#utility#is_active(bufnr) | return | endif
  66. let hunks = gitgutter#hunk#hunks(bufnr)
  67. if empty(hunks)
  68. call gitgutter#utility#warn('No hunks in file')
  69. return
  70. endif
  71. let current_line = line('.')
  72. let hunk_count = 0
  73. for hunk in reverse(copy(hunks))
  74. if hunk[2] < current_line
  75. let hunk_count += 1
  76. if hunk_count == a:count
  77. let target = hunk[2] == 0 ? 1 : hunk[2]
  78. execute 'normal!' target . 'Gzv'
  79. if g:gitgutter_show_msg_on_hunk_jumping
  80. redraw | echo printf('Hunk %d of %d', index(hunks, hunk) + 1, len(hunks))
  81. endif
  82. if gitgutter#hunk#is_preview_window_open()
  83. call gitgutter#hunk#preview()
  84. endif
  85. return
  86. endif
  87. endif
  88. endfor
  89. call gitgutter#utility#warn('No previous hunks')
  90. endfunction
  91. " Returns the hunk the cursor is currently in or an empty list if the cursor
  92. " isn't in a hunk.
  93. function! s:current_hunk() abort
  94. let bufnr = bufnr('')
  95. let current_hunk = []
  96. for hunk in gitgutter#hunk#hunks(bufnr)
  97. if gitgutter#hunk#cursor_in_hunk(hunk)
  98. let current_hunk = hunk
  99. break
  100. endif
  101. endfor
  102. return current_hunk
  103. endfunction
  104. " Returns truthy if the cursor is in two hunks (which can only happen if the
  105. " cursor is on the first line and lines above have been deleted and lines
  106. " immediately below have been deleted) or falsey otherwise.
  107. function! s:cursor_in_two_hunks()
  108. let hunks = gitgutter#hunk#hunks(bufnr(''))
  109. if line('.') == 1 && len(hunks) > 1 && hunks[0][2:3] == [0, 0] && hunks[1][2:3] == [1, 0]
  110. return 1
  111. endif
  112. return 0
  113. endfunction
  114. " A line can be in 0 or 1 hunks, with the following exception: when the first
  115. " line(s) of a file has been deleted, and the new second line (and
  116. " optionally below) has been deleted, the new first line is in two hunks.
  117. function! gitgutter#hunk#cursor_in_hunk(hunk) abort
  118. let current_line = line('.')
  119. if current_line == 1 && a:hunk[2] == 0
  120. return 1
  121. endif
  122. if current_line >= a:hunk[2] && current_line < a:hunk[2] + (a:hunk[3] == 0 ? 1 : a:hunk[3])
  123. return 1
  124. endif
  125. return 0
  126. endfunction
  127. function! gitgutter#hunk#in_hunk(lnum)
  128. " Hunks are sorted in the order they appear in the buffer.
  129. for hunk in gitgutter#hunk#hunks(bufnr(''))
  130. " if in a hunk on first line of buffer
  131. if a:lnum == 1 && hunk[2] == 0
  132. return 1
  133. endif
  134. " if in a hunk generally
  135. if a:lnum >= hunk[2] && a:lnum < hunk[2] + (hunk[3] == 0 ? 1 : hunk[3])
  136. return 1
  137. endif
  138. " if hunk starts after the given line
  139. if a:lnum < hunk[2]
  140. return 0
  141. endif
  142. endfor
  143. return 0
  144. endfunction
  145. function! gitgutter#hunk#text_object(inner) abort
  146. let hunk = s:current_hunk()
  147. if empty(hunk)
  148. return
  149. endif
  150. let [first_line, last_line] = [hunk[2], hunk[2] + hunk[3] - 1]
  151. if ! a:inner
  152. let lnum = last_line
  153. let eof = line('$')
  154. while lnum < eof && empty(getline(lnum + 1))
  155. let lnum +=1
  156. endwhile
  157. let last_line = lnum
  158. endif
  159. execute 'normal! 'first_line.'GV'.last_line.'G'
  160. endfunction
  161. function! gitgutter#hunk#stage(...) abort
  162. if !s:in_hunk_preview_window() && !gitgutter#utility#has_repo_path(bufnr('')) | return | endif
  163. if a:0 && (a:1 != 1 || a:2 != line('$'))
  164. call s:hunk_op(function('s:stage'), a:1, a:2)
  165. else
  166. call s:hunk_op(function('s:stage'))
  167. endif
  168. silent! call repeat#set("\<Plug>(GitGutterStageHunk)", -1)
  169. endfunction
  170. function! gitgutter#hunk#undo() abort
  171. if !gitgutter#utility#has_repo_path(bufnr('')) | return | endif
  172. call s:hunk_op(function('s:undo'))
  173. silent! call repeat#set("\<Plug>(GitGutterUndoHunk)", -1)
  174. endfunction
  175. function! gitgutter#hunk#preview() abort
  176. if !gitgutter#utility#has_repo_path(bufnr('')) | return | endif
  177. call s:hunk_op(function('s:preview'))
  178. silent! call repeat#set("\<Plug>(GitGutterPreviewHunk)", -1)
  179. endfunction
  180. function! s:hunk_op(op, ...)
  181. let bufnr = bufnr('')
  182. if s:in_hunk_preview_window()
  183. if string(a:op) =~ '_stage'
  184. " combine hunk-body in preview window with updated hunk-header
  185. let hunk_body = getline(1, '$')
  186. let [removed, added] = [0, 0]
  187. for line in hunk_body
  188. if line[0] == '-'
  189. let removed += 1
  190. elseif line[0] == '+'
  191. let added += 1
  192. endif
  193. endfor
  194. let hunk_header = b:hunk_header
  195. " from count
  196. let hunk_header[4] = substitute(hunk_header[4], '\(-\d\+\)\(,\d\+\)\?', '\=submatch(1).",".removed', '')
  197. " to count
  198. let hunk_header[4] = substitute(hunk_header[4], '\(+\d\+\)\(,\d\+\)\?', '\=submatch(1).",".added', '')
  199. let hunk_diff = join(hunk_header + hunk_body, "\n")."\n"
  200. call s:goto_original_window()
  201. call gitgutter#hunk#close_hunk_preview_window()
  202. call s:stage(hunk_diff)
  203. endif
  204. return
  205. endif
  206. if gitgutter#utility#is_active(bufnr)
  207. " Get a (synchronous) diff.
  208. let [async, g:gitgutter_async] = [g:gitgutter_async, 0]
  209. let diff = gitgutter#diff#run_diff(bufnr, g:gitgutter_diff_relative_to, 1)
  210. let g:gitgutter_async = async
  211. call gitgutter#hunk#set_hunks(bufnr, gitgutter#diff#parse_diff(diff))
  212. call gitgutter#diff#process_hunks(bufnr, gitgutter#hunk#hunks(bufnr)) " so the hunk summary is updated
  213. if empty(s:current_hunk())
  214. call gitgutter#utility#warn('Cursor is not in a hunk')
  215. elseif s:cursor_in_two_hunks()
  216. let choice = input('Choose hunk: upper or lower (u/l)? ')
  217. " Clear input
  218. normal! :<ESC>
  219. if choice =~ 'u'
  220. call a:op(gitgutter#diff#hunk_diff(bufnr, diff, 0))
  221. elseif choice =~ 'l'
  222. call a:op(gitgutter#diff#hunk_diff(bufnr, diff, 1))
  223. else
  224. call gitgutter#utility#warn('Did not recognise your choice')
  225. endif
  226. else
  227. let hunk_diff = gitgutter#diff#hunk_diff(bufnr, diff)
  228. if a:0
  229. let hunk_first_line = s:current_hunk()[2]
  230. let hunk_diff = s:part_of_diff(hunk_diff, a:1-hunk_first_line, a:2-hunk_first_line)
  231. endif
  232. call a:op(hunk_diff)
  233. endif
  234. endif
  235. endfunction
  236. function! s:stage(hunk_diff)
  237. let bufnr = bufnr('')
  238. let diff = s:adjust_header(bufnr, a:hunk_diff)
  239. " Apply patch to index.
  240. call gitgutter#utility#system(
  241. \ gitgutter#utility#cd_cmd(bufnr, g:gitgutter_git_executable.' '.g:gitgutter_git_args.' apply --cached --unidiff-zero - '),
  242. \ diff)
  243. if v:shell_error
  244. call gitgutter#utility#warn('Patch does not apply')
  245. else
  246. if exists('#User#GitGutterStage')
  247. execute 'doautocmd' s:nomodeline 'User GitGutterStage'
  248. endif
  249. endif
  250. " Refresh gitgutter's view of buffer.
  251. call gitgutter#process_buffer(bufnr, 1)
  252. endfunction
  253. function! s:undo(hunk_diff)
  254. " Apply reverse patch to buffer.
  255. let hunk = gitgutter#diff#parse_hunk(split(a:hunk_diff, '\n')[4])
  256. let lines = map(split(a:hunk_diff, '\r\?\n')[5:], 'v:val[1:]')
  257. let lnum = hunk[2]
  258. let added_only = hunk[1] == 0 && hunk[3] > 0
  259. let removed_only = hunk[1] > 0 && hunk[3] == 0
  260. if removed_only
  261. call append(lnum, lines)
  262. elseif added_only
  263. execute lnum .','. (lnum+len(lines)-1) .'d _'
  264. else
  265. call append(lnum-1, lines[0:hunk[1]])
  266. execute (lnum+hunk[1]) .','. (lnum+hunk[1]+hunk[3]) .'d _'
  267. endif
  268. endfunction
  269. function! s:preview(hunk_diff)
  270. let lines = split(a:hunk_diff, '\r\?\n')
  271. let header = lines[0:4]
  272. let body = lines[5:]
  273. call s:open_hunk_preview_window()
  274. call s:populate_hunk_preview_window(header, body)
  275. call s:enable_staging_from_hunk_preview_window()
  276. if &previewwindow
  277. call s:goto_original_window()
  278. endif
  279. endfunction
  280. " Returns a new hunk diff using the specified lines from the given one.
  281. " Assumes all lines are additions.
  282. " a:first, a:last - 0-based indexes into the body of the hunk.
  283. function! s:part_of_diff(hunk_diff, first, last)
  284. let diff_lines = split(a:hunk_diff, '\n', 1)
  285. " adjust 'to' line count in header
  286. let diff_lines[4] = substitute(diff_lines[4], '\(+\d\+\)\(,\d\+\)\?', '\=submatch(1).",".(a:last-a:first+1)', '')
  287. return join(diff_lines[0:4] + diff_lines[5+a:first:5+a:last], "\n")."\n"
  288. endfunction
  289. function! s:adjust_header(bufnr, hunk_diff)
  290. let filepath = gitgutter#utility#repo_path(a:bufnr, 0)
  291. return s:adjust_hunk_summary(s:fix_file_references(filepath, a:hunk_diff))
  292. endfunction
  293. " Replaces references to temp files with the actual file.
  294. function! s:fix_file_references(filepath, hunk_diff)
  295. let lines = split(a:hunk_diff, '\n')
  296. let left_prefix = matchstr(lines[2], '[abciow12]').'/'
  297. let right_prefix = matchstr(lines[3], '[abciow12]').'/'
  298. let quote = lines[0][11] == '"' ? '"' : ''
  299. let left_file = quote.left_prefix.a:filepath.quote
  300. let right_file = quote.right_prefix.a:filepath.quote
  301. let lines[0] = 'diff --git '.left_file.' '.right_file
  302. let lines[2] = '--- '.left_file
  303. let lines[3] = '+++ '.right_file
  304. return join(lines, "\n")."\n"
  305. endfunction
  306. function! s:adjust_hunk_summary(hunk_diff) abort
  307. let line_adjustment = s:line_adjustment_for_current_hunk()
  308. let diff = split(a:hunk_diff, '\n', 1)
  309. let diff[4] = substitute(diff[4], '+\zs\(\d\+\)', '\=submatch(1)+line_adjustment', '')
  310. return join(diff, "\n")
  311. endfunction
  312. " Returns the number of lines the current hunk is offset from where it would
  313. " be if any changes above it in the file didn't exist.
  314. function! s:line_adjustment_for_current_hunk() abort
  315. let bufnr = bufnr('')
  316. let adj = 0
  317. for hunk in gitgutter#hunk#hunks(bufnr)
  318. if gitgutter#hunk#cursor_in_hunk(hunk)
  319. break
  320. else
  321. let adj += hunk[1] - hunk[3]
  322. endif
  323. endfor
  324. return adj
  325. endfunction
  326. function! s:in_hunk_preview_window()
  327. if g:gitgutter_preview_win_floating
  328. return win_id2win(s:winid) == winnr()
  329. else
  330. return &previewwindow
  331. endif
  332. endfunction
  333. " Floating window: does not move cursor to floating window.
  334. " Preview window: moves cursor to preview window.
  335. function! s:open_hunk_preview_window()
  336. if g:gitgutter_preview_win_floating
  337. if exists('*nvim_open_win')
  338. call gitgutter#hunk#close_hunk_preview_window()
  339. let buf = nvim_create_buf(v:false, v:false)
  340. " Set default width and height for now.
  341. let s:winid = nvim_open_win(buf, v:false, g:gitgutter_floating_window_options)
  342. call nvim_buf_set_option(buf, 'filetype', 'diff')
  343. call nvim_buf_set_option(buf, 'buftype', 'acwrite')
  344. call nvim_buf_set_option(buf, 'bufhidden', 'delete')
  345. call nvim_buf_set_option(buf, 'swapfile', v:false)
  346. call nvim_buf_set_name(buf, 'gitgutter://hunk-preview')
  347. " Assumes cursor is in original window.
  348. autocmd CursorMoved <buffer> ++once call gitgutter#hunk#close_hunk_preview_window()
  349. if g:gitgutter_close_preview_on_escape
  350. " Map <Esc> to close the floating preview.
  351. nnoremap <buffer> <silent> <Esc> :<C-U>call gitgutter#hunk#close_hunk_preview_window()<CR>
  352. " Ensure that when the preview window is closed, the map is removed.
  353. autocmd User GitGutterPreviewClosed silent! nunmap <buffer> <Esc>
  354. autocmd CursorMoved <buffer> ++once silent! nunmap <buffer> <Esc>
  355. execute "autocmd WinClosed <buffer=".winbufnr(s:winid)."> doautocmd" s:nomodeline "User GitGutterPreviewClosed"
  356. endif
  357. return
  358. endif
  359. if exists('*popup_create')
  360. if g:gitgutter_close_preview_on_escape
  361. let g:gitgutter_floating_window_options.filter = function('s:close_popup_on_escape')
  362. endif
  363. let s:winid = popup_create('', g:gitgutter_floating_window_options)
  364. call setbufvar(winbufnr(s:winid), '&filetype', 'diff')
  365. return
  366. endif
  367. endif
  368. if exists('&previewpopup')
  369. let [previewpopup, &previewpopup] = [&previewpopup, '']
  370. endif
  371. " Specifying where to open the preview window can lead to the cursor going
  372. " to an unexpected window when the preview window is closed (#769).
  373. silent! noautocmd execute g:gitgutter_preview_win_location 'pedit gitgutter://hunk-preview'
  374. silent! wincmd P
  375. setlocal statusline=%{''}
  376. doautocmd WinEnter
  377. if exists('*win_getid')
  378. let s:winid = win_getid()
  379. else
  380. let s:preview_bufnr = bufnr('')
  381. endif
  382. setlocal filetype=diff buftype=acwrite bufhidden=delete
  383. " Reset some defaults in case someone else has changed them.
  384. setlocal noreadonly modifiable noswapfile
  385. if g:gitgutter_close_preview_on_escape
  386. " Ensure cursor goes to the expected window.
  387. nnoremap <buffer> <silent> <Esc> :<C-U>wincmd p<Bar>pclose<CR>
  388. endif
  389. if exists('&previewpopup')
  390. let &previewpopup=previewpopup
  391. endif
  392. endfunction
  393. function! s:close_popup_on_escape(winid, key)
  394. if a:key == "\<Esc>"
  395. call popup_close(a:winid)
  396. return 1
  397. endif
  398. return 0
  399. endfunction
  400. " Floating window: does not care where cursor is.
  401. " Preview window: assumes cursor is in preview window.
  402. function! s:populate_hunk_preview_window(header, body)
  403. let body_length = len(a:body)
  404. if g:gitgutter_preview_win_floating
  405. if exists('*nvim_open_win')
  406. let height = min([body_length, g:gitgutter_floating_window_options.height])
  407. " Assumes cursor is not in previewing window.
  408. call nvim_buf_set_var(winbufnr(s:winid), 'hunk_header', a:header)
  409. let [_scrolloff, &scrolloff] = [&scrolloff, 0]
  410. let width = max(map(copy(a:body), 'strdisplaywidth(v:val)'))
  411. call nvim_win_set_width(s:winid, width)
  412. call nvim_win_set_height(s:winid, height)
  413. let &scrolloff=_scrolloff
  414. call nvim_buf_set_lines(winbufnr(s:winid), 0, -1, v:false, [])
  415. call nvim_buf_set_lines(winbufnr(s:winid), 0, -1, v:false, a:body)
  416. call nvim_buf_set_option(winbufnr(s:winid), 'modified', v:false)
  417. let ns_id = nvim_create_namespace('GitGutter')
  418. call nvim_buf_clear_namespace(winbufnr(s:winid), ns_id, 0, -1)
  419. for region in gitgutter#diff_highlight#process(a:body)
  420. let group = region[1] == '+' ? 'GitGutterAddIntraLine' : 'GitGutterDeleteIntraLine'
  421. call nvim_buf_add_highlight(winbufnr(s:winid), ns_id, group, region[0]-1, region[2]-1, region[3])
  422. endfor
  423. call nvim_win_set_cursor(s:winid, [1,0])
  424. endif
  425. if exists('*popup_create')
  426. call popup_settext(s:winid, a:body)
  427. for region in gitgutter#diff_highlight#process(a:body)
  428. let group = region[1] == '+' ? 'GitGutterAddIntraLine' : 'GitGutterDeleteIntraLine'
  429. call win_execute(s:winid, "call matchaddpos('".group."', [[".region[0].", ".region[2].", ".(region[3]-region[2]+1)."]])")
  430. endfor
  431. endif
  432. else
  433. let b:hunk_header = a:header
  434. %delete _
  435. call setline(1, a:body)
  436. setlocal nomodified
  437. normal! G$
  438. let hunk_height = max([body_length, winline()])
  439. let height = min([hunk_height, &previewheight])
  440. execute 'resize' height
  441. 1
  442. call clearmatches()
  443. for region in gitgutter#diff_highlight#process(a:body)
  444. let group = region[1] == '+' ? 'GitGutterAddIntraLine' : 'GitGutterDeleteIntraLine'
  445. call matchaddpos(group, [[region[0], region[2], region[3]-region[2]+1]])
  446. endfor
  447. 1
  448. endif
  449. endfunction
  450. function! s:enable_staging_from_hunk_preview_window()
  451. augroup gitgutter_hunk_preview
  452. autocmd!
  453. let bufnr = s:winid != 0 ? winbufnr(s:winid) : s:preview_bufnr
  454. execute 'autocmd BufWriteCmd <buffer='.bufnr.'> GitGutterStageHunk'
  455. augroup END
  456. endfunction
  457. function! s:goto_original_window()
  458. noautocmd wincmd p
  459. doautocmd WinEnter
  460. endfunction
  461. function! gitgutter#hunk#close_hunk_preview_window()
  462. let bufnr = s:winid != 0 ? winbufnr(s:winid) : s:preview_bufnr
  463. call setbufvar(bufnr, '&modified', 0)
  464. if g:gitgutter_preview_win_floating
  465. if win_id2win(s:winid) > 0
  466. execute win_id2win(s:winid).'wincmd c'
  467. endif
  468. else
  469. pclose
  470. endif
  471. let s:winid = 0
  472. let s:preview_bufnr = 0
  473. endfunction
  474. function gitgutter#hunk#is_preview_window_open()
  475. if g:gitgutter_preview_win_floating
  476. if win_id2win(s:winid) > 0
  477. execute win_id2win(s:winid).'wincmd c'
  478. endif
  479. else
  480. for i in range(1, winnr('$'))
  481. if getwinvar(i, '&previewwindow')
  482. return 1
  483. endif
  484. endfor
  485. endif
  486. return 0
  487. endfunction