notify.vim 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532
  1. scriptencoding utf-8
  2. let s:is_vim = !has('nvim')
  3. let s:utf = &encoding =~# '^utf'
  4. let s:error_icon = get(g:, 'coc_notify_error_icon', s:utf ? '' : 'E')
  5. let s:warning_icon = get(g:, 'coc_notify_warning_icon', s:utf ? '⚠' : 'W')
  6. let s:info_icon = get(g:, 'coc_notify_info_icon', s:utf ? '' : 'I')
  7. let s:interval = get(g:, 'coc_notify_interval', s:is_vim ? 50 : 20)
  8. let s:phl = 'CocNotificationProgress'
  9. let s:progress_char = '─'
  10. let s:duration = 300.0
  11. let s:winids = []
  12. " Valid notify winids on current tab
  13. function! coc#notify#win_list() abort
  14. call filter(s:winids, 'coc#float#valid(v:val)')
  15. return filter(copy(s:winids), '!empty(getwinvar(v:val,"float"))')
  16. endfunction
  17. function! coc#notify#close_all() abort
  18. for winid in coc#notify#win_list()
  19. call coc#notify#close(winid)
  20. endfor
  21. endfunction
  22. " Do action for winid or first notify window with actions.
  23. function! coc#notify#do_action(...) abort
  24. let winids = a:0 > 0 ? a:000 : coc#notify#win_list()
  25. for winid in winids
  26. if coc#float#valid(winid) && getwinvar(winid, 'closing', 0) != 1
  27. let actions = getwinvar(winid, 'actions', [])
  28. if !empty(actions)
  29. let items = map(copy(actions), '(v:key + 1).". ".v:val')
  30. let msg = join(getbufline(winbufnr(winid), 1, '$'), ' ')
  31. call coc#ui#quickpick(msg, items, {err, res -> s:on_action(err, res, winid) })
  32. break
  33. endif
  34. endif
  35. endfor
  36. endfunction
  37. " Copy notification contents
  38. function! coc#notify#copy() abort
  39. let lines = []
  40. for winid in coc#notify#win_list()
  41. let key = getwinvar(winid, 'key', v:null)
  42. if type(key) == v:t_string
  43. call extend(lines, json_decode(key)['lines'])
  44. endif
  45. endfor
  46. if empty(lines)
  47. echohl WarningMsg | echon 'No content to copy' | echohl None
  48. return
  49. endif
  50. call setreg('*', join(lines, "\n"))
  51. endfunction
  52. " Show source name in window
  53. function! coc#notify#show_sources() abort
  54. if !exists('*getbufline') || !exists('*appendbufline')
  55. throw "getbufline and appendbufline functions required, please upgrade your vim."
  56. endif
  57. let winids = filter(coc#notify#win_list(), 'coc#window#get_var(v:val,"closing") != 1')
  58. for winid in winids
  59. let key = getwinvar(winid, 'key', v:null)
  60. if type(key) == v:t_string
  61. let bufnr = winbufnr(winid)
  62. let obj = json_decode(key)
  63. let sourcename = get(obj, 'source', '')
  64. let lnum = get(obj, 'kind', '') ==# 'progress' ? 1 : 0
  65. let content = get(getbufline(bufnr, lnum + 1), 0, '')
  66. if empty(sourcename) || content ==# sourcename
  67. continue
  68. endif
  69. call appendbufline(bufnr, lnum, sourcename)
  70. call coc#highlight#add_highlight(bufnr, -1, 'Title', lnum, 0, -1)
  71. call coc#float#scroll_win(winid, 0, 1)
  72. endif
  73. endfor
  74. redra
  75. endfunction
  76. function! coc#notify#close_by_source(source) abort
  77. let winids = filter(coc#notify#win_list(), 'coc#window#get_var(v:val,"closing") != 1')
  78. for winid in winids
  79. let key = getwinvar(winid, 'key', v:null)
  80. if type(key) == v:t_string
  81. let obj = json_decode(key)
  82. if get(obj, 'source', '') ==# a:source
  83. call coc#notify#close(winid)
  84. endif
  85. endif
  86. endfor
  87. endfunction
  88. " Cancel auto hide
  89. function! coc#notify#keep() abort
  90. for winid in coc#notify#win_list()
  91. call s:cancel(winid, 'close_timer')
  92. endfor
  93. endfunction
  94. " borderhighlight - border highlight [string]
  95. " maxWidth - max content width, default 60 [number]
  96. " minWidth - minimal width [number]
  97. " maxHeight - max content height, default 10 [number]
  98. " highlight - default highlight [string]
  99. " winblend - winblend [number]
  100. " timeout - auto close timeout, default 5000 [number]
  101. " title - title text
  102. " marginRight - margin right, default 10 [number]
  103. " focusable - focusable [number]
  104. " source - source name [string]
  105. " kind - kind for create icon [string]
  106. " actions - action names [string[]]
  107. function! coc#notify#create(lines, config) abort
  108. let actions = get(a:config, 'actions', [])
  109. let key = json_encode(extend({'lines': a:lines}, a:config))
  110. let winid = s:find_win(key)
  111. let kind = get(a:config, 'kind', '')
  112. let row = 0
  113. if winid != -1
  114. let row = getwinvar(winid, 'top', 0)
  115. call filter(s:winids, 'v:val != '.winid)
  116. call coc#float#close(winid)
  117. let winid = v:null
  118. endif
  119. let opts = coc#dict#pick(a:config, ['highlight', 'borderhighlight', 'focusable', 'shadow'])
  120. let border = has_key(opts, 'borderhighlight') ? [1, 1, 1, 1] : []
  121. let icon = s:get_icon(kind, get(a:config, 'highlight', 'CocFloating'))
  122. let margin = get(a:config, 'marginRight', 10)
  123. let maxWidth = min([&columns - margin - 2, get(a:config, 'maxWidth', 80)])
  124. if maxWidth <= 0
  125. throw 'No enough spaces for notification'
  126. endif
  127. let lines = map(copy(a:lines), 'tr(v:val, "\t", " ")')
  128. if has_key(a:config, 'title')
  129. if !empty(border)
  130. let opts['title'] = a:config['title']
  131. else
  132. let lines = [a:config['title']] + lines
  133. endif
  134. endif
  135. let width = max(map(copy(lines), 'strwidth(v:val)')) + (empty(icon) ? 1 : 3)
  136. if width > maxWidth
  137. let lines = coc#string#reflow(lines, maxWidth)
  138. let width = max(map(copy(lines), 'strwidth(v:val)')) + (empty(icon) ? 1 : 3)
  139. endif
  140. let highlights = []
  141. if !empty(icon)
  142. let ic = icon['text']
  143. if empty(lines)
  144. call add(lines, ic)
  145. else
  146. let lines[0] = ic.' '.lines[0]
  147. endif
  148. call add(highlights, {'lnum': 0, 'hlGroup': icon['hl'], 'colStart': 0, 'colEnd': strlen(ic)})
  149. endif
  150. let actionText = join(actions, ' ')
  151. call map(lines, 'v:key == 0 ? v:val : repeat(" ", '.(empty(icon) ? 0 : 2).').v:val')
  152. let minWidth = get(a:config, 'minWidth', kind ==# 'progress' ? 30 : 10)
  153. let width = max(extend(map(lines + [get(opts, 'title', '').' '], 'strwidth(v:val)'), [minWidth, strwidth(actionText) + 1]))
  154. let width = min([maxWidth, width])
  155. let height = min([get(a:config, 'maxHeight', 3), len(lines)])
  156. if kind ==# 'progress'
  157. let lines = [repeat(s:progress_char, width)] + lines
  158. let height = height + 1
  159. endif
  160. if !empty(actions)
  161. let before = max([width - strwidth(actionText), 0])
  162. let lines = lines + [repeat(' ', before).actionText]
  163. let height = height + 1
  164. call s:add_action_highlights(before, height - 1, highlights, actions)
  165. endif
  166. if row == 0
  167. let wintop = coc#notify#get_top()
  168. let row = wintop - height - (empty(border) ? 0 : 2) - 1
  169. if !s:is_vim && !empty(border)
  170. let row = row + 1
  171. endif
  172. endif
  173. let col = &columns - margin - width
  174. if s:is_vim && !empty(border)
  175. let col = col - 2
  176. endif
  177. let winblend = 60
  178. " Avoid animate for transparent background.
  179. if get(a:config, 'winblend', 30) == 0 && empty(synIDattr(synIDtrans(hlID(get(opts, 'highlight', 'CocFloating'))), 'bg', 'gui'))
  180. let winblend = 0
  181. endif
  182. call extend(opts, {
  183. \ 'relative': 'editor',
  184. \ 'width': width,
  185. \ 'height': height,
  186. \ 'col': col,
  187. \ 'row': row + 1,
  188. \ 'lines': lines,
  189. \ 'rounded': 1,
  190. \ 'highlights': highlights,
  191. \ 'winblend': winblend,
  192. \ 'border': border,
  193. \ })
  194. let result = coc#float#create_float_win(0, 0, opts)
  195. if empty(result)
  196. throw 'Unable to create notify window'
  197. endif
  198. let winid = result[0]
  199. let bufnr = result[1]
  200. call setwinvar(winid, 'right', 1)
  201. call setwinvar(winid, 'kind', 'notification')
  202. call setwinvar(winid, 'top', row)
  203. call setwinvar(winid, 'key', key)
  204. call setwinvar(winid, 'actions', actions)
  205. call setwinvar(winid, 'source', get(a:config, 'source', ''))
  206. call setwinvar(winid, 'border', !empty(border))
  207. call coc#float#nvim_scrollbar(winid)
  208. call add(s:winids, winid)
  209. let from = {'row': opts['row'], 'winblend': opts['winblend']}
  210. let to = {'row': row, 'winblend': get(a:config, 'winblend', 30)}
  211. call timer_start(s:interval, { -> s:animate(winid, from, to, 0)})
  212. if kind ==# 'progress'
  213. call timer_start(s:interval, { -> s:progress(winid, width, 0, -1)})
  214. endif
  215. if !s:is_vim
  216. call coc#compat#buf_add_keymap(bufnr, 'n', '<LeftRelease>', ':call coc#notify#nvim_click('.winid.')<CR>', {
  217. \ 'silent': v:true,
  218. \ 'nowait': v:true
  219. \ })
  220. endif
  221. " Enable auto close
  222. if empty(actions) && kind !=# 'progress'
  223. let timer = timer_start(get(a:config, 'timeout', 10000), { -> coc#notify#close(winid)})
  224. call setwinvar(winid, 'close_timer', timer)
  225. endif
  226. return [winid, bufnr]
  227. endfunction
  228. function! coc#notify#nvim_click(winid) abort
  229. if getwinvar(a:winid, 'closing', 0)
  230. return
  231. endif
  232. call s:cancel(a:winid, 'close_timer')
  233. let actions = getwinvar(a:winid, 'actions', [])
  234. if !empty(actions)
  235. let character = strpart(getline('.'), col('.') - 1, 1)
  236. if character =~# '^\k'
  237. let word = expand('<cword>')
  238. let idx = index(actions, word)
  239. if idx != -1
  240. call coc#rpc#notify('FloatBtnClick', [winbufnr(a:winid), idx])
  241. call coc#notify#close(a:winid)
  242. endif
  243. endif
  244. endif
  245. endfunction
  246. function! coc#notify#on_close(winid) abort
  247. if index(s:winids, a:winid) >= 0
  248. call filter(s:winids, 'v:val != '.a:winid)
  249. call coc#notify#reflow()
  250. endif
  251. endfunction
  252. function! coc#notify#get_top() abort
  253. let mintop = min(map(coc#notify#win_list(), 'coc#notify#get_win_top(v:val)'))
  254. if mintop != 0
  255. return mintop
  256. endif
  257. return &lines - &cmdheight - (&laststatus == 0 ? 0 : 1 )
  258. endfunction
  259. function! coc#notify#get_win_top(winid) abort
  260. let row = getwinvar(a:winid, 'top', 0)
  261. if row == 0
  262. return row
  263. endif
  264. return row - (s:is_vim ? 0 : getwinvar(a:winid, 'border', 0))
  265. endfunction
  266. " Close with timer
  267. function! coc#notify#close(winid) abort
  268. if !coc#float#valid(a:winid) || coc#window#get_var(a:winid, 'closing', 0) == 1
  269. return
  270. endif
  271. if !coc#window#visible(a:winid)
  272. call coc#float#close(a:winid)
  273. return
  274. endif
  275. let row = coc#window#get_var(a:winid, 'top')
  276. if type(row) != v:t_number
  277. call coc#float#close(a:winid)
  278. return
  279. endif
  280. call coc#window#set_var(a:winid, 'closing', 1)
  281. call s:cancel(a:winid)
  282. let winblend = coc#window#get_var(a:winid, 'winblend', 0)
  283. let curr = s:is_vim ? {'row': row} : {'winblend': winblend}
  284. let dest = s:is_vim ? {'row': row + 1} : {'winblend': winblend == 0 ? 0 : 60}
  285. call s:animate(a:winid, curr, dest, 0, 1)
  286. endfunction
  287. function! s:add_action_highlights(before, lnum, highlights, actions) abort
  288. let colStart = a:before
  289. for text in a:actions
  290. let w = strwidth(text)
  291. call add(a:highlights, {
  292. \ 'lnum': a:lnum,
  293. \ 'hlGroup': 'CocNotificationButton',
  294. \ 'colStart': colStart,
  295. \ 'colEnd': colStart + w
  296. \ })
  297. let colStart = colStart + w + 1
  298. endfor
  299. endfunction
  300. function! s:on_action(err, idx, winid) abort
  301. if !empty(a:err)
  302. throw a:err
  303. endif
  304. if a:idx > 0
  305. call coc#rpc#notify('FloatBtnClick', [winbufnr(a:winid), a:idx - 1])
  306. call coc#notify#close(a:winid)
  307. endif
  308. endfunction
  309. function! s:cancel(winid, ...) abort
  310. let name = get(a:, 1, 'timer')
  311. let timer = coc#window#get_var(a:winid, name)
  312. if !empty(timer)
  313. call timer_stop(timer)
  314. call coc#window#set_var(a:winid, name, v:null)
  315. endif
  316. endfunction
  317. function! s:progress(winid, total, curr, index) abort
  318. if !coc#float#valid(a:winid)
  319. return
  320. endif
  321. if coc#window#visible(a:winid)
  322. let total = a:total
  323. let idx = float2nr(a:curr/5.0)%total
  324. if idx != a:index
  325. " update percent
  326. let bufnr = winbufnr(a:winid)
  327. let percent = coc#window#get_var(a:winid, 'percent')
  328. if !empty(percent)
  329. let width = strchars(getbufline(bufnr, 1)[0])
  330. let line = repeat(s:progress_char, width - 4).printf('%4s', percent)
  331. let total = width - 4
  332. call setbufline(bufnr, 1, line)
  333. endif
  334. let message = coc#window#get_var(a:winid, 'message')
  335. if !empty(message)
  336. let linecount = coc#compat#buf_line_count(bufnr)
  337. let hasAction = !empty(coc#window#get_var(a:winid, 'actions', []))
  338. if getbufvar(bufnr, 'message', 0) == 0
  339. call appendbufline(bufnr, linecount - hasAction, message)
  340. call setbufvar(bufnr, 'message', 1)
  341. call coc#float#change_height(a:winid, 1)
  342. let tabnr = coc#window#tabnr(a:winid)
  343. call coc#notify#reflow(tabnr)
  344. else
  345. call setbufline(bufnr, linecount - hasAction, message)
  346. endif
  347. endif
  348. let bytes = strlen(s:progress_char)
  349. call coc#highlight#clear_highlight(bufnr, -1, 0, 1)
  350. let colStart = bytes * idx
  351. if idx + 4 <= total
  352. let colEnd = bytes * (idx + 4)
  353. call coc#highlight#add_highlight(bufnr, -1, s:phl, 0, colStart, colEnd)
  354. else
  355. let colEnd = bytes * total
  356. call coc#highlight#add_highlight(bufnr, -1, s:phl, 0, colStart, colEnd)
  357. call coc#highlight#add_highlight(bufnr, -1, s:phl, 0, 0, bytes * (idx + 4 - total))
  358. endif
  359. endif
  360. call timer_start(s:interval, { -> s:progress(a:winid, total, a:curr + 1, idx)})
  361. else
  362. " Not block CursorHold event
  363. call timer_start(&updatetime + 100, { -> s:progress(a:winid, a:total, a:curr, a:index)})
  364. endif
  365. endfunction
  366. " Optional row & winblend
  367. function! s:config_win(winid, props) abort
  368. let change_row = has_key(a:props, 'row')
  369. if s:is_vim
  370. if change_row
  371. call popup_move(a:winid, {'line': a:props['row'] + 1})
  372. endif
  373. else
  374. if change_row
  375. let [row, column] = nvim_win_get_position(a:winid)
  376. call nvim_win_set_config(a:winid, {
  377. \ 'row': a:props['row'],
  378. \ 'col': column,
  379. \ 'relative': 'editor',
  380. \ })
  381. call s:nvim_move_related(a:winid, a:props['row'])
  382. endif
  383. call coc#float#nvim_set_winblend(a:winid, get(a:props, 'winblend', v:null))
  384. call coc#float#nvim_refresh_scrollbar(a:winid)
  385. endif
  386. endfunction
  387. function! s:nvim_move_related(winid, row) abort
  388. let winids = coc#window#get_var(a:winid, 'related')
  389. if empty(winids)
  390. return
  391. endif
  392. for winid in winids
  393. if nvim_win_is_valid(winid)
  394. let [row, column] = nvim_win_get_position(winid)
  395. let delta = coc#window#get_var(winid, 'delta', 0)
  396. call nvim_win_set_config(winid, {
  397. \ 'row': a:row + delta,
  398. \ 'col': column,
  399. \ 'relative': 'editor',
  400. \ })
  401. endif
  402. endfor
  403. endfunction
  404. function! s:animate(winid, from, to, prev, ...) abort
  405. if !coc#float#valid(a:winid)
  406. return
  407. endif
  408. let curr = a:prev + s:interval
  409. let percent = coc#math#min(curr / s:duration, 1)
  410. let props = s:get_props(a:from, a:to, percent)
  411. call s:config_win(a:winid, props)
  412. let close = get(a:, 1, 0)
  413. if percent < 1
  414. call timer_start(s:interval, { -> s:animate(a:winid, a:from, a:to, curr, close)})
  415. elseif close
  416. call filter(s:winids, 'v:val != '.a:winid)
  417. let tabnr = coc#window#tabnr(a:winid)
  418. if tabnr != -1
  419. call coc#float#close(a:winid)
  420. call coc#notify#reflow(tabnr)
  421. endif
  422. endif
  423. endfunction
  424. function! coc#notify#reflow(...) abort
  425. let tabnr = get(a:, 1, tabpagenr())
  426. let winids = filter(copy(s:winids), 'coc#window#tabnr(v:val) == '.tabnr.' && coc#window#get_var(v:val,"closing") != 1')
  427. if empty(winids)
  428. return
  429. endif
  430. let animate = tabnr == tabpagenr()
  431. let wins = map(copy(winids), {_, val -> {
  432. \ 'winid': val,
  433. \ 'row': coc#window#get_var(val,'top',0),
  434. \ 'top': coc#window#get_var(val,'top',0) - (s:is_vim ? 0 : coc#window#get_var(val, 'border', 0)),
  435. \ 'height': coc#float#get_height(val),
  436. \ }})
  437. call sort(wins, {a, b -> b['top'] - a['top']})
  438. let bottom = &lines - &cmdheight - (&laststatus == 0 ? 0 : 1 )
  439. let moved = 0
  440. for item in wins
  441. let winid = item['winid']
  442. let delta = bottom - (item['top'] + item['height'] + 1)
  443. if delta != 0
  444. call s:cancel(winid)
  445. let dest = item['row'] + delta
  446. call coc#window#set_var(winid, 'top', dest)
  447. if animate
  448. call s:move_win_timer(winid, {'row': item['row']}, {'row': dest}, 0)
  449. else
  450. call s:config_win(winid, {'row': dest})
  451. endif
  452. let moved = moved + delta
  453. endif
  454. let bottom = item['top'] + delta
  455. endfor
  456. endfunction
  457. function! s:move_win_timer(winid, from, to, curr) abort
  458. if !coc#float#valid(a:winid)
  459. return
  460. endif
  461. if coc#window#get_var(a:winid, 'closing', 0) == 1
  462. return
  463. endif
  464. let percent = coc#math#min(a:curr / s:duration, 1)
  465. let next = a:curr + s:interval
  466. if a:curr > 0
  467. call s:config_win(a:winid, s:get_props(a:from, a:to, percent))
  468. endif
  469. if percent < 1
  470. let timer = timer_start(s:interval, { -> s:move_win_timer(a:winid, a:from, a:to, next)})
  471. call coc#window#set_var(a:winid, 'timer', timer)
  472. endif
  473. endfunction
  474. function! s:find_win(key) abort
  475. for winid in coc#notify#win_list()
  476. if getwinvar(winid, 'key', '') ==# a:key
  477. return winid
  478. endif
  479. endfor
  480. return -1
  481. endfunction
  482. function! s:get_icon(kind, bg) abort
  483. if a:kind ==# 'info'
  484. return {'text': s:info_icon, 'hl': coc#highlight#compose_hlgroup('CocInfoSign', a:bg)}
  485. endif
  486. if a:kind ==# 'warning'
  487. return {'text': s:warning_icon, 'hl': coc#highlight#compose_hlgroup('CocWarningSign', a:bg)}
  488. endif
  489. if a:kind ==# 'error'
  490. return {'text': s:error_icon, 'hl': coc#highlight#compose_hlgroup('CocErrorSign', a:bg)}
  491. endif
  492. return v:null
  493. endfunction
  494. " percent should be float
  495. function! s:get_props(from, to, percent) abort
  496. let obj = {}
  497. for key in keys(a:from)
  498. let changed = a:to[key] - a:from[key]
  499. if !s:is_vim && key ==# 'row'
  500. " Could be float
  501. let obj[key] = a:from[key] + changed * a:percent
  502. else
  503. let obj[key] = a:from[key] + float2nr(round(changed * a:percent))
  504. endif
  505. endfor
  506. return obj
  507. endfunction