1" Runs the specified healthchecks.
2" Runs all discovered healthchecks if a:plugin_names is empty.
3function! health#check(plugin_names) abort
4  let healthchecks = empty(a:plugin_names)
5        \ ? s:discover_healthchecks()
6        \ : s:get_healthcheck(a:plugin_names)
7
8  " create scratch-buffer
9  execute 'tab sbuffer' nvim_create_buf(v:true, v:true)
10  setfiletype checkhealth
11
12  if empty(healthchecks)
13    call setline(1, 'ERROR: No healthchecks found.')
14  else
15    redraw|echo 'Running healthchecks...'
16    for name in sort(keys(healthchecks))
17      let [func, type] = healthchecks[name]
18      let s:output = []
19      try
20        if func == ''
21          throw 'healthcheck_not_found'
22        endif
23        eval type == 'v' ? call(func, []) : luaeval(func)
24      catch
25        let s:output = []  " Clear the output
26        if v:exception =~# 'healthcheck_not_found'
27          call health#report_error('No healthcheck found for "'.name.'" plugin.')
28        else
29          call health#report_error(printf(
30                \ "Failed to run healthcheck for \"%s\" plugin. Exception:\n%s\n%s",
31                \ name, v:throwpoint, v:exception))
32        endif
33      endtry
34      let header = [name. ': ' . func, repeat('=', 72)]
35      " remove empty line after header from report_start
36      let s:output = s:output[0] == '' ? s:output[1:] : s:output
37      let s:output = header + s:output + ['']
38      call append('$', s:output)
39      redraw
40    endfor
41  endif
42
43  " needed for plasticboy/vim-markdown, because it uses fdm=expr
44  normal! zR
45  redraw|echo ''
46endfunction
47
48function! s:collect_output(output)
49  let s:output += split(a:output, "\n", 1)
50endfunction
51
52" Starts a new report.
53function! health#report_start(name) abort
54  call s:collect_output("\n## " . a:name)
55endfunction
56
57" Indents lines *except* line 1 of a string if it contains newlines.
58function! s:indent_after_line1(s, columns) abort
59  let lines = split(a:s, "\n", 0)
60  if len(lines) < 2  " We do not indent line 1, so nothing to do.
61    return a:s
62  endif
63  for i in range(1, len(lines)-1)  " Indent lines after the first.
64    let lines[i] = substitute(lines[i], '^\s*', repeat(' ', a:columns), 'g')
65  endfor
66  return join(lines, "\n")
67endfunction
68
69" Changes ':h clipboard' to ':help |clipboard|'.
70function! s:help_to_link(s) abort
71  return substitute(a:s, '\v:h%[elp] ([^|][^"\r\n ]+)', ':help |\1|', 'g')
72endfunction
73
74" Format a message for a specific report item.
75" a:1: Optional advice (string or list)
76function! s:format_report_message(status, msg, ...) abort " {{{
77  let output = '  - ' . a:status . ': ' . s:indent_after_line1(a:msg, 4)
78
79  " Optional parameters
80  if a:0 > 0
81    let advice = type(a:1) == type('') ? [a:1] : a:1
82    if type(advice) != type([])
83      throw 'a:1: expected String or List'
84    endif
85
86    " Report each suggestion
87    if !empty(advice)
88      let output .= "\n    - ADVICE:"
89      for suggestion in advice
90        let output .= "\n      - " . s:indent_after_line1(suggestion, 10)
91      endfor
92    endif
93  endif
94
95  return s:help_to_link(output)
96endfunction " }}}
97
98" Use {msg} to report information in the current section
99function! health#report_info(msg) abort " {{{
100  call s:collect_output(s:format_report_message('INFO', a:msg))
101endfunction " }}}
102
103" Reports a successful healthcheck.
104function! health#report_ok(msg) abort " {{{
105  call s:collect_output(s:format_report_message('OK', a:msg))
106endfunction " }}}
107
108" Reports a health warning.
109" a:1: Optional advice (string or list)
110function! health#report_warn(msg, ...) abort " {{{
111  if a:0 > 0
112    call s:collect_output(s:format_report_message('WARNING', a:msg, a:1))
113  else
114    call s:collect_output(s:format_report_message('WARNING', a:msg))
115  endif
116endfunction " }}}
117
118" Reports a failed healthcheck.
119" a:1: Optional advice (string or list)
120function! health#report_error(msg, ...) abort " {{{
121  if a:0 > 0
122    call s:collect_output(s:format_report_message('ERROR', a:msg, a:1))
123  else
124    call s:collect_output(s:format_report_message('ERROR', a:msg))
125  endif
126endfunction " }}}
127
128" From a path return a list [{name}, {func}, {type}] representing a healthcheck
129function! s:filepath_to_healthcheck(path) abort
130  if a:path =~# 'vim$'
131    let name =  matchstr(a:path, '\zs[^\/]*\ze\.vim$')
132    let func = 'health#'.name.'#check'
133    let type = 'v'
134  else
135   let base_path = substitute(a:path,
136         \ '.*lua[\/]\(.\{-}\)[\/]health\([\/]init\)\?\.lua$',
137         \ '\1', '')
138   let name = substitute(base_path, '[\/]', '.', 'g')
139   let func = 'require("'.name.'.health").check()'
140   let type = 'l'
141 endif
142  return [name, func, type]
143endfunction
144
145function! s:discover_healthchecks() abort
146  return s:get_healthcheck('*')
147endfunction
148
149" Returns Dictionary {name: [func, type], ..} representing healthchecks
150function! s:get_healthcheck(plugin_names) abort
151  let health_list = s:get_healthcheck_list(a:plugin_names)
152  let healthchecks = {}
153  for c in health_list
154    let normalized_name = substitute(c[0], '-', '_', 'g')
155    let existent = get(healthchecks, normalized_name, [])
156    " Prefer Lua over vim entries
157    if existent != [] && existent[2] == 'l'
158      continue
159    else
160      let healthchecks[normalized_name] = c
161    endif
162  endfor
163  let output = {}
164  for v in values(healthchecks)
165    let output[v[0]] = v[1:]
166  endfor
167  return output
168endfunction
169
170" Returns list of lists [ [{name}, {func}, {type}] ] representing healthchecks
171function! s:get_healthcheck_list(plugin_names) abort
172  let healthchecks = []
173  let plugin_names = type('') == type(a:plugin_names)
174        \ ? split(a:plugin_names, ' ', v:false)
175        \ : a:plugin_names
176  for p in plugin_names
177    " support vim/lsp/health{/init/}.lua as :checkhealth vim.lsp
178    let p = substitute(p, '\.', '/', 'g')
179    let p = substitute(p, '*$', '**', 'g')  " find all submodule e.g vim*
180    let paths = nvim_get_runtime_file('autoload/health/'.p.'.vim', v:true)
181          \ + nvim_get_runtime_file('lua/**/'.p.'/health/init.lua', v:true)
182          \ + nvim_get_runtime_file('lua/**/'.p.'/health.lua', v:true)
183    if len(paths) == 0
184      let healthchecks += [[p, '', '']]  " healthchek not found
185    else
186      let healthchecks += map(uniq(sort(paths)),
187            \'<SID>filepath_to_healthcheck(v:val)')
188    end
189  endfor
190  return healthchecks
191endfunction
192