Merge pull request #15259 from muniter/muniter/checkhealth-from-lua

feat(checkhealth): support Lua healthchecks
This commit is contained in:
Matthieu Coudron
2021-10-05 11:27:07 +02:00
committed by GitHub
9 changed files with 445 additions and 135 deletions

View File

@@ -26,8 +26,8 @@ endfunction
" Runs all discovered healthchecks if a:plugin_names is empty.
function! health#check(plugin_names) abort
let healthchecks = empty(a:plugin_names)
\ ? s:discover_health_checks()
\ : s:to_fn_names(a:plugin_names)
\ ? s:discover_healthchecks()
\ : s:get_healthcheck(a:plugin_names)
tabnew
setlocal wrap breakindent linebreak
@@ -42,24 +42,28 @@ function! health#check(plugin_names) abort
else
redraw|echo 'Running healthchecks...'
for c in healthchecks
let output = ''
call append('$', split(printf("\n%s\n%s", c, repeat('=',72)), "\n"))
let [name, func, type] = c
let s:output = []
try
let output = "\n\n".execute('call '.c.'()')
if func == ''
throw 'healthcheck_not_found'
endif
eval type == 'v' ? call(func, []) : luaeval(func)
catch
if v:exception =~# '^Vim\%((\a\+)\)\=:E117.*\V'.c
let output = execute(
\ 'call health#report_error(''No healthcheck found for "'
\ .s:to_plugin_name(c)
\ .'" plugin.'')')
let s:output = [] " Clear the output
if v:exception =~# 'healthcheck_not_found'
call health#report_error('No healthcheck found for "'.name.'" plugin.')
else
let output = execute(
\ 'call health#report_error(''Failed to run healthcheck for "'
\ .s:to_plugin_name(c)
\ .'" plugin. Exception:''."\n".v:throwpoint."\n".v:exception)')
call health#report_error(printf(
\ "Failed to run healthcheck for \"%s\" plugin. Exception:\n%s\n%s",
\ name, v:throwpoint, v:exception))
endif
endtry
call append('$', split(output, "\n") + [''])
let header = [name. ': ' . func, repeat('=', 72)]
" remove empty line after header from report_start
let s:output = s:output[0] == '' ? s:output[1:] : s:output
let s:output = header + s:output + ['']
call append('$', s:output)
redraw
endfor
endif
@@ -71,9 +75,13 @@ function! health#check(plugin_names) abort
redraw|echo ''
endfunction
function! s:collect_output(output)
let s:output += split(a:output, "\n", 1)
endfunction
" Starts a new report.
function! health#report_start(name) abort
echo "\n## " . a:name
call s:collect_output("\n## " . a:name)
endfunction
" Indents lines *except* line 1 of a string if it contains newlines.
@@ -119,21 +127,21 @@ endfunction " }}}
" Use {msg} to report information in the current section
function! health#report_info(msg) abort " {{{
echo s:format_report_message('INFO', a:msg)
call s:collect_output(s:format_report_message('INFO', a:msg))
endfunction " }}}
" Reports a successful healthcheck.
function! health#report_ok(msg) abort " {{{
echo s:format_report_message('OK', a:msg)
call s:collect_output(s:format_report_message('OK', a:msg))
endfunction " }}}
" Reports a health warning.
" a:1: Optional advice (string or list)
function! health#report_warn(msg, ...) abort " {{{
if a:0 > 0
echo s:format_report_message('WARNING', a:msg, a:1)
call s:collect_output(s:format_report_message('WARNING', a:msg, a:1))
else
echo s:format_report_message('WARNING', a:msg)
call s:collect_output(s:format_report_message('WARNING', a:msg))
endif
endfunction " }}}
@@ -141,37 +149,52 @@ endfunction " }}}
" a:1: Optional advice (string or list)
function! health#report_error(msg, ...) abort " {{{
if a:0 > 0
echo s:format_report_message('ERROR', a:msg, a:1)
call s:collect_output(s:format_report_message('ERROR', a:msg, a:1))
else
echo s:format_report_message('ERROR', a:msg)
call s:collect_output(s:format_report_message('ERROR', a:msg))
endif
endfunction " }}}
function! s:filepath_to_function(name) abort
return substitute(substitute(substitute(a:name, '.*autoload[\/]', '', ''),
\ '\.vim', '#check', ''), '[\/]', '#', 'g')
" From a path return a list [{name}, {func}, {type}] representing a healthcheck
function! s:filepath_to_healthcheck(path) abort
if a:path =~# 'vim$'
let name = matchstr(a:path, '\zs[^\/]*\ze\.vim$')
let func = 'health#'.name.'#check'
let type = 'v'
else
let base_path = substitute(a:path,
\ '.*lua[\/]\(.\{-}\)[\/]health\([\/]init\)\?\.lua$',
\ '\1', '')
let name = substitute(base_path, '[\/]', '.', 'g')
let func = 'require("'.name.'.health").check()'
let type = 'l'
endif
return [name, func, type]
endfunction
function! s:discover_health_checks() abort
let healthchecks = globpath(&runtimepath, 'autoload/health/*.vim', 1, 1)
let healthchecks = map(healthchecks, '<SID>filepath_to_function(v:val)')
return healthchecks
function! s:discover_healthchecks() abort
return s:get_healthcheck('*')
endfunction
" Translates a list of plugin names to healthcheck function names.
function! s:to_fn_names(plugin_names) abort
" Returns list of lists [ [{name}, {func}, {type}] ] representing healthchecks
function! s:get_healthcheck(plugin_names) abort
let healthchecks = []
let plugin_names = type('') ==# type(a:plugin_names)
\ ? split(a:plugin_names, '', v:false)
let plugin_names = type('') == type(a:plugin_names)
\ ? split(a:plugin_names, ' ', v:false)
\ : a:plugin_names
for p in plugin_names
call add(healthchecks, 'health#'.p.'#check')
" support vim/lsp/health{/init/}.lua as :checkhealth vim.lsp
let p = substitute(p, '\.', '/', 'g')
let p = substitute(p, '*$', '**', 'g') " find all submodule e.g vim*
let paths = nvim_get_runtime_file('autoload/health/'.p.'.vim', v:true)
\ + nvim_get_runtime_file('lua/**/'.p.'/health/init.lua', v:true)
\ + nvim_get_runtime_file('lua/**/'.p.'/health.lua', v:true)
if len(paths) == 0
let healthchecks += [[p, '', '']] " healthchek not found
else
let healthchecks += map(uniq(sort(paths)),
\'<SID>filepath_to_healthcheck(v:val)')
end
endfor
return healthchecks
endfunction
" Extracts 'foo' from 'health#foo#check'.
function! s:to_plugin_name(fn_name) abort
return substitute(a:fn_name,
\ '\v.*health\#(.+)\#check.*', '\1', '')
endfunction

View File

@@ -23,27 +23,102 @@ Commands *health-commands*
*:checkhealth* *:CheckHealth*
:checkhealth Run all healthchecks.
*E5009*
Nvim depends on |$VIMRUNTIME| and 'runtimepath' to find
the standard "runtime files" for syntax highlighting,
filetype-specific behavior, and standard plugins
(including :checkhealth). If the runtime files cannot
be found then those features will not work.
Nvim depends on |$VIMRUNTIME|, 'runtimepath' and 'packpath' to
find the standard "runtime files" for syntax highlighting,
filetype-specific behavior, and standard plugins (including
:checkhealth). If the runtime files cannot be found then
those features will not work.
:checkhealth {plugins}
Run healthcheck(s) for one or more plugins. E.g. to run
only the standard Nvim healthcheck: >
Run healthcheck(s) for one or more plugins. E.g. to run only
the standard Nvim healthcheck: >
:checkhealth nvim
< To run the healthchecks for the "foo" and "bar" plugins
(assuming these plugins are on your 'runtimepath' and
they have implemented health#foo#check() and
health#bar#check(), respectively): >
<
To run the healthchecks for the "foo" and "bar" plugins
(assuming these plugins are on 'runtimepath' or 'packpath' and
they have implemented the Lua or Vimscript interface
require("foo.health").check() and health#bar#check(),
respectively): >
:checkhealth foo bar
<
To run healthchecks for lua submodules, use dot notation or
"*" to refer to all submodules. For example nvim provides
`vim.lsp` and `vim.treesitter` >
:checkhealth vim.lsp vim.treesitter
:checkhealth vim*
<
==============================================================================
Functions *health-functions*
Lua Functions *health-functions-lua* *health-lua*
health.vim functions are for creating new healthchecks. They mostly just do
some layout and formatting, to give users a consistent presentation.
The Lua "health" module can be used to create new healthchecks (see also
|health-functions-vim|). To get started, simply use: >
local health = require('health')
<
health.report_start({name}) *health.report_start()*
Starts a new report. Most plugins should call this only once, but if
you want different sections to appear in your report, call this once
per section.
health.report_info({msg}) *health.report_info()*
Reports an informational message.
health.report_ok({msg}) *health.report_ok()*
Reports a "success" message.
health.report_warn({msg} [, {advice}]) *health.report_warn()*
Reports a warning. {advice} is an optional List of suggestions.
health.report_error({msg} [, {advice}]) *health.report_error()*
Reports an error. {advice} is an optional List of suggestions.
==============================================================================
Create a Lua healthcheck *health-dev-lua*
Healthchecks are functions that check the user environment, configuration,
etc. Nvim has built-in healthchecks in $VIMRUNTIME/autoload/health/.
To add a new healthcheck for your own plugin, simply define a Lua module in
your plugin that returns a table with a "check()" function. |:checkhealth|
will automatically find and invoke this function.
If your plugin is named "foo", then its healthcheck module should be a file in
one of these locations on 'runtimepath' or 'packpath':
- `lua/foo/health/init.lua`
- `lua/foo/health.lua`
If your plugin provides a submodule named "bar" for which you want a separate
healthcheck, define the healthcheck at one of these locations on 'runtimepath'
or 'packpath':
- `lua/foo/bar/health/init.lua`
- `lua/foo/bar/health.lua`
All submodules should return a Lua table containing the method `check()`.
Copy this sample code into `lua/foo/health/init.lua` or `lua/foo/health.lua`,
replacing "foo" in the path with your plugin name: >
local M = {}
local health = require("health")
M.check = function()
health.report_start("my_plugin report")
-- make sure setup function parameters are ok
if check_setup() then
health.report_ok("Setup function is correct")
else
health.report_error("Setup function is incorrect")
end
-- do some more checking
-- ...
end
return M
==============================================================================
Vimscript Functions *health-functions-vimscript* *health-vimscript*
health.vim functions are for creating new healthchecks. (See also
|health-functions-lua|)
health#report_start({name}) *health#report_start*
Starts a new report. Most plugins should call this only once, but if
@@ -56,10 +131,10 @@ health#report_info({msg}) *health#report_info*
health#report_ok({msg}) *health#report_ok*
Reports a "success" message.
health#report_warn({msg}, [{advice}]) *health#report_warn*
health#report_warn({msg} [, {advice}]) *health#report_warn*
Reports a warning. {advice} is an optional List of suggestions.
health#report_error({msg}, [{advice}]) *health#report_error*
health#report_error({msg} [, {advice}]) *health#report_error*
Reports an error. {advice} is an optional List of suggestions.
health#{plugin}#check() *health.user_checker*
@@ -71,12 +146,8 @@ health#{plugin}#check() *health.user_checker*
silent call s:check_python_configuration()
endfunction
<
All output will be captured from the healthcheck. Use the
health#report_* functions so that your healthcheck has a format
consistent with the standard healthchecks.
==============================================================================
Create a healthcheck *health-dev*
Create a healthcheck *health-dev-vim*
Healthchecks are functions that check the user environment, configuration,
etc. Nvim has built-in healthchecks in $VIMRUNTIME/autoload/health/.
@@ -88,7 +159,7 @@ health#{plugin}#check() function in autoload/health/{plugin}.vim.
If your plugin is named "foo", then its healthcheck function must be >
health#foo#check()
defined in this file on 'runtimepath': >
defined in this file on 'runtimepath' or 'packpath': >
autoload/health/foo.vim
Copy this sample code into autoload/health/foo.vim and replace "foo" with your
@@ -106,6 +177,4 @@ plugin name: >
endif
endfunction
==============================================================================
vim:tw=78:ts=8:ft=help:fdm=marker
vim:noet tw=78:ts=8:ft=help:fdm=marker

23
runtime/lua/health.lua Normal file
View File

@@ -0,0 +1,23 @@
local M = {}
function M.report_start(msg)
vim.fn['health#report_start'](msg)
end
function M.report_info(msg)
vim.fn['health#report_info'](msg)
end
function M.report_ok(msg)
vim.fn['health#report_ok'](msg)
end
function M.report_warn(msg, ...)
vim.fn['health#report_warn'](msg, ...)
end
function M.report_error(msg, ...)
vim.fn['health#report_error'](msg, ...)
end
return M

View File

@@ -230,7 +230,22 @@ static int compl_match_arraysize;
static int compl_startcol;
static int compl_selected;
/// |:checkhealth| completion items
///
/// Regenerates on every new command line prompt, to accomodate changes on the
/// runtime files.
typedef struct {
garray_T names; // healthcheck names
unsigned last_gen; // last_prompt_id where names were generated
} CheckhealthComp;
/// Cookie used when converting filepath to name
struct healthchecks_cookie {
garray_T *names; // global healthchecks
bool is_lua; // true if the current entry is a Lua healthcheck
};
static CheckhealthComp healthchecks = { GA_INIT(sizeof(char_u *), 10), 0 };
#ifdef INCLUDE_GENERATED_DECLARATIONS
# include "ex_getln.c.generated.h"
@@ -273,6 +288,68 @@ static void init_incsearch_state(incsearch_state_T *s)
save_viewstate(&s->old_viewstate);
}
/// Completion for |:checkhealth| command.
///
/// Given to ExpandGeneric() to obtain all available heathcheck names.
/// @param[in] idx Index of the healthcheck item.
/// @param[in] xp Not used.
static char_u *get_healthcheck_names(expand_T *xp, int idx)
{
// Generate the first time or on new prompt.
if (healthchecks.last_gen == 0 || healthchecks.last_gen != last_prompt_id) {
ga_clear_strings(&healthchecks.names);
char *patterns[3] = { "autoload/health/**.vim", "lua/**/**/health/init.lua", // NOLINT
"lua/**/**/health.lua" }; // NOLINT
for (int i = 0; i < 3; i++) {
struct healthchecks_cookie hcookie = { .names = &healthchecks.names, .is_lua = i != 0 };
do_in_runtimepath((char_u *)patterns[i], DIP_ALL, get_healthcheck_cb, &hcookie);
if (healthchecks.names.ga_len > 0) {
ga_remove_duplicate_strings(&healthchecks.names);
}
}
// Tracked to regenerate items on next prompt.
healthchecks.last_gen = last_prompt_id;
}
return idx <
(int)healthchecks.names.ga_len ? ((char_u **)(healthchecks.names.ga_data))[idx] : NULL;
}
/// Transform healthcheck file path into it's name.
///
/// Used as a callback for do_in_runtimepath
/// @param[in] path Expanded path to a possible healthcheck.
/// @param[out] cookie Array where names will be inserted.
static void get_healthcheck_cb(char_u *path, void *cookie)
{
if (path != NULL) {
struct healthchecks_cookie *hcookie = (struct healthchecks_cookie *)cookie;
char *pattern;
char *sub = "\\1";
char_u *res;
if (hcookie->is_lua) {
// Lua: transform "../lua/vim/lsp/health.lua" into "vim.lsp"
pattern = ".*lua[\\/]\\(.\\{-}\\)[\\/]health\\([\\/]init\\)\\?\\.lua$";
} else {
// Vim: transform "../autoload/health/provider.vim" into "provider"
pattern = ".*[\\/]\\([^\\/]*\\)\\.vim$";
}
res = do_string_sub(path, (char_u *)pattern, (char_u *)sub, NULL, (char_u *)"g");
if (hcookie->is_lua && res != NULL) {
// Replace slashes with dots as represented by the healthcheck plugin.
char_u *ares = do_string_sub(res, (char_u *)"[\\/]", (char_u *)".", NULL, (char_u *)"g");
xfree(res);
res = ares;
}
if (res != NULL) {
GA_APPEND(char_u *, hcookie->names, res);
}
}
}
// Return true when 'incsearch' highlighting is to be done.
// Sets search_first_line and search_last_line to the address range.
static bool do_incsearch_highlighting(int firstc, int *search_delim, incsearch_state_T *s,
@@ -4902,10 +4979,6 @@ static int ExpandFromContext(expand_T *xp, char_u *pat, int *num_file, char_u **
char *directories[] = { "syntax", "indent", "ftplugin", NULL };
return ExpandRTDir(pat, DIP_LUA, num_file, file, directories);
}
if (xp->xp_context == EXPAND_CHECKHEALTH) {
char *directories[] = { "autoload/health", NULL };
return ExpandRTDir(pat, 0, num_file, file, directories);
}
if (xp->xp_context == EXPAND_USER_LIST) {
return ExpandUserList(xp, num_file, file);
}
@@ -4982,6 +5055,7 @@ static int ExpandFromContext(expand_T *xp, char_u *pat, int *num_file, char_u **
{ EXPAND_ENV_VARS, get_env_name, true, true },
{ EXPAND_USER, get_users, true, false },
{ EXPAND_ARGLIST, get_arglist_name, true, false },
{ EXPAND_CHECKHEALTH, get_healthcheck_names, true, false },
};
int i;

View File

@@ -0,0 +1,8 @@
function! health#full_render#check()
call health#report_start("report 1")
call health#report_ok("life is fine")
call health#report_warn("no what installed", ["pip what", "make what"])
call health#report_start("report 2")
call health#report_info("stuff is stable")
call health#report_error("why no hardcopy", [":h :hardcopy", ":h :TOhtml"])
endfunction

View File

@@ -0,0 +1,11 @@
local M = {}
local health = require("health")
M.check = function()
health.report_start("report 1")
health.report_ok("everything is fine")
health.report_start("report 2")
health.report_ok("nothing to see here")
end
return M

View File

@@ -0,0 +1,11 @@
local M = {}
local health = require("health")
M.check = function()
health.report_start("report 1")
health.report_ok("everything is fine")
health.report_start("report 2")
health.report_ok("nothing to see here")
end
return M

View File

@@ -0,0 +1,12 @@
local M = {}
local health = require("health")
M.check = function()
health.report_start("report 1")
health.report_ok("everything is fine")
health.report_warn("About to add a number to nil")
local a = nil + 2
return a
end
return M

View File

@@ -1,4 +1,5 @@
local helpers = require('test.functional.helpers')(after_each)
local global_helpers = require('test.helpers')
local Screen = require('test.functional.ui.screen')
local clear = helpers.clear
@@ -35,6 +36,7 @@ describe(':checkhealth', function()
clear()
eq('nvim', getcompletion('nvim', 'checkhealth')[1])
eq('provider', getcompletion('prov', 'checkhealth')[1])
eq('vim.lsp', getcompletion('vim.ls', 'checkhealth')[1])
end)
end)
@@ -48,42 +50,34 @@ describe('health.vim', function()
command("set runtimepath+=test/functional/fixtures")
end)
it("health#report_*()", function()
helpers.source([[
let g:health_report = execute([
\ "call health#report_start('Check Bar')",
\ "call health#report_ok('Bar status')",
\ "call health#report_ok('Other Bar status')",
\ "call health#report_warn('Zub')",
\ "call health#report_start('Baz')",
\ "call health#report_warn('Zim', ['suggestion 1', 'suggestion 2'])"
\ ])
]])
local result = helpers.eval("g:health_report")
helpers.eq(helpers.dedent([[
## Check Bar
- OK: Bar status
- OK: Other Bar status
- WARNING: Zub
## Baz
- WARNING: Zim
- ADVICE:
- suggestion 1
- suggestion 2]]),
result)
end)
describe(":checkhealth", function()
it("concatenates multiple reports", function()
command("checkhealth success1 success2")
it("functions health#report_*() render correctly", function()
command("checkhealth full_render")
helpers.expect([[
health#success1#check
full_render: health#full_render#check
========================================================================
## report 1
- OK: life is fine
- WARNING: no what installed
- ADVICE:
- pip what
- make what
## report 2
- INFO: stuff is stable
- ERROR: why no hardcopy
- ADVICE:
- :help |:hardcopy|
- :help |:TOhtml|
]])
end)
it("concatenates multiple reports", function()
command("checkhealth success1 success2 test_plug")
helpers.expect([[
success1: health#success1#check
========================================================================
## report 1
- OK: everything is fine
@@ -91,25 +85,109 @@ describe('health.vim', function()
## report 2
- OK: nothing to see here
health#success2#check
success2: health#success2#check
========================================================================
## another 1
- OK: ok
test_plug: require("test_plug.health").check()
========================================================================
## report 1
- OK: everything is fine
## report 2
- OK: nothing to see here
]])
end)
it("lua plugins", function()
command("checkhealth test_plug")
helpers.expect([[
test_plug: require("test_plug.health").check()
========================================================================
## report 1
- OK: everything is fine
## report 2
- OK: nothing to see here
]])
end)
it("lua plugins submodules", function()
command("checkhealth test_plug.submodule")
helpers.expect([[
test_plug.submodule: require("test_plug.submodule.health").check()
========================================================================
## report 1
- OK: everything is fine
## report 2
- OK: nothing to see here
]])
end)
it("lua plugins submodules with expression '*'", function()
command("checkhealth test_plug*")
local buf_lines = helpers.curbuf('get_lines', 0, -1, true)
-- avoid dealing with path separators
local received = table.concat(buf_lines, '\n', 1, #buf_lines - 2)
local expected = helpers.dedent([[
test_plug: require("test_plug.health").check()
========================================================================
## report 1
- OK: everything is fine
## report 2
- OK: nothing to see here
test_plug.submodule: require("test_plug.submodule.health").check()
========================================================================
## report 1
- OK: everything is fine
## report 2
- OK: nothing to see here
test_plug.submodule_failed: require("test_plug.submodule_failed.health").check()
========================================================================
- ERROR: Failed to run healthcheck for "test_plug.submodule_failed" plugin. Exception:
function health#check, line 24]])
eq(expected, received)
end)
it("gracefully handles broken healthcheck", function()
command("checkhealth broken")
helpers.expect([[
health#broken#check
broken: health#broken#check
========================================================================
- ERROR: Failed to run healthcheck for "broken" plugin. Exception:
function health#check[21]..health#broken#check, line 1
function health#check[24]..health#broken#check, line 1
caused an error
]])
end)
it("gracefully handles broken lua healthcheck", function()
command("checkhealth test_plug.submodule_failed")
local buf_lines = helpers.curbuf('get_lines', 0, -1, true)
local received = table.concat(buf_lines, '\n', 1, #buf_lines - 2)
-- avoid dealing with path separators
local lua_err = "attempt to perform arithmetic on a nil value"
local last_line = buf_lines[#buf_lines - 1]
assert(string.find(last_line, lua_err) ~= nil, "Lua error not present")
local expected = global_helpers.dedent([[
test_plug.submodule_failed: require("test_plug.submodule_failed.health").check()
========================================================================
- ERROR: Failed to run healthcheck for "test_plug.submodule_failed" plugin. Exception:
function health#check, line 24]])
eq(expected, received)
end)
it("highlights OK, ERROR", function()
local screen = Screen.new(72, 10)
screen:attach()
@@ -126,11 +204,11 @@ describe('health.vim', function()
command("set laststatus=0")
screen:expect{grid=[[
^ |
{Heading:health#foo#check} |
{Heading:foo: } |
{Bar:========================================================================}|
{Bullet: -} {Error:ERROR:} No healthcheck found for "foo" plugin. |
|
{Heading:health#success1#check} |
{Heading:success1: health#success1#check} |
{Bar:========================================================================}|
{Heading2:##}{Heading: report 1} |
{Bullet: -} {Ok:OK:} everything is fine |
@@ -140,9 +218,10 @@ describe('health.vim', function()
it("gracefully handles invalid healthcheck", function()
command("checkhealth non_existent_healthcheck")
-- luacheck: ignore 613
helpers.expect([[
health#non_existent_healthcheck#check
non_existent_healthcheck:
========================================================================
- ERROR: No healthcheck found for "non_existent_healthcheck" plugin.
]])