neovim

Neovim text editor
git clone https://git.dasho.dev/neovim.git
Log | Files | Refs | README

health.lua (8502B)


      1 local M = {}
      2 
      3 local health = vim.health
      4 
      5 local function get_lockfile_path()
      6  return vim.fs.joinpath(vim.fn.stdpath('config'), 'nvim-pack-lock.json')
      7 end
      8 
      9 local function get_plug_dir()
     10  return vim.fs.joinpath(vim.fn.stdpath('data'), 'site', 'pack', 'core', 'opt')
     11 end
     12 
     13 local function git_cmd(cmd, cwd)
     14  cmd = vim.list_extend({ 'git', '-c', 'gc.auto=0' }, cmd)
     15  local env = vim.fn.environ() --- @type table<string,string>
     16  env.GIT_DIR, env.GIT_WORK_TREE = nil, nil
     17  local sys_opts = { cwd = cwd, text = true, env = env, clear_env = true }
     18  local out = vim.system(cmd, sys_opts):wait() --- @type vim.SystemCompleted
     19  if out.code ~= 0 then
     20    return false, ((out.stderr or ''):gsub('\n+$', ''))
     21  end
     22  return true, ((out.stdout or ''):gsub('\n+$', ''))
     23 end
     24 
     25 local function check_basics()
     26  health.start('vim.pack: basics')
     27 
     28  -- Requirements
     29  if vim.fn.executable('git') == 0 then
     30    health.warn('`git` executable is required. Install it using your package manager')
     31    return false, false
     32  end
     33 
     34  -- Detect if not used
     35  local lockfile_path = get_lockfile_path()
     36  local has_lockfile = vim.fn.filereadable(lockfile_path) == 1
     37  local plug_dir = get_plug_dir()
     38  local has_plug_dir = vim.fn.isdirectory(plug_dir) == 1
     39  if not has_lockfile and not has_plug_dir then
     40    health.ok('`vim.pack` is not used')
     41    return false, false
     42  end
     43 
     44  -- General info
     45  local git = vim.fn.exepath('git')
     46  local _, version = git_cmd({ 'version' }, vim.uv.cwd())
     47  health.info(('Git: %s (%s)'):format(version:gsub('^git%s*', ''), git))
     48  health.info('Lockfile: ' .. lockfile_path)
     49  health.info('Plugin directory: ' .. plug_dir)
     50 
     51  if has_lockfile and has_plug_dir then
     52    health.ok('')
     53  else
     54    local lockfile_absent = has_lockfile and 'present' or 'absent'
     55    local plug_dir_absent = has_plug_dir and 'present' or 'absent'
     56    local msg = ('Lockfile is %s, plugin directory is %s.'):format(lockfile_absent, plug_dir_absent)
     57      .. ' Restart Nvim and run `vim.pack.add({})` to '
     58      .. (has_lockfile and 'install plugins from the lockfile' or 'regenerate the lockfile')
     59    health.warn(msg)
     60  end
     61 
     62  return has_lockfile, has_plug_dir
     63 end
     64 
     65 local function is_version(x)
     66  return type(x) == 'string' or (type(x) == 'table' and pcall(x.has, x, '1'))
     67 end
     68 
     69 local function failed_git_cmd(plug_name, plug_path)
     70  local msg = ('Failed Git command inside plugin %s.'):format(vim.inspect(plug_name))
     71    .. ' This is unexpected and should not happen.'
     72    .. (' Manually delete directory %s and reinstall plugin'):format(plug_path)
     73  health.error(msg)
     74  return false
     75 end
     76 
     77 --- @return boolean Whether a check is successful
     78 local function check_plugin_lock_data(plug_name, lock_data)
     79  local name_str = vim.inspect(plug_name)
     80  local error_with_del_advice = function(reason)
     81    local msg = ('%s %s.'):format(name_str, reason)
     82      .. (' Delete %s entry (do not create trailing comma) and '):format(name_str)
     83      .. 'restart Nvim to regenerate lockfile data'
     84    health.error(msg)
     85    return false
     86  end
     87 
     88  -- Types
     89  if type(plug_name) ~= 'string' then
     90    return error_with_del_advice('is not a valid plugin name')
     91  end
     92  if type(lock_data) ~= 'table' then
     93    return error_with_del_advice('entry is not a valid type')
     94  end
     95  if type(lock_data.rev) ~= 'string' then
     96    local reason = '`rev` entry is ' .. (lock_data.rev and 'not a valid type' or 'missing')
     97    return error_with_del_advice(reason)
     98  end
     99  if type(lock_data.src) ~= 'string' then
    100    local reason = '`src` entry is ' .. (lock_data.src and 'not a valid type' or 'missing')
    101    return error_with_del_advice(reason)
    102  end
    103  if lock_data.version and not is_version(lock_data.version) then
    104    return error_with_del_advice('`version` entry is not a valid type')
    105  end
    106 
    107  -- Alignment with what is actually present on disk
    108  local plug_path = vim.fs.joinpath(get_plug_dir(), plug_name)
    109  if vim.fn.isdirectory(plug_path) ~= 1 then
    110    health.warn(
    111      ('Plugin %s is not installed but present in the lockfile.'):format(name_str)
    112        .. ' Restart Nvim and run `vim.pack.add({})` to autoinstall.'
    113        .. (' To fully delete, run `vim.pack.del({ %s }, { force = true })`'):format(name_str)
    114    )
    115    return false
    116  end
    117 
    118  -- NOTE: `vim.pack` currently only supports Git repos as plugins
    119  if not git_cmd({ 'rev-parse', '--git-dir' }, plug_path) then
    120    return true
    121  end
    122 
    123  local has_head, head = git_cmd({ 'rev-list', '-1', 'HEAD' }, plug_path)
    124  if not has_head then
    125    return failed_git_cmd(plug_name, plug_path)
    126  elseif lock_data.rev ~= head then
    127    health.error(
    128      ('Plugin %s is not at expected revision\n'):format(name_str)
    129        .. ('Expected: %s\nActual:   %s\n'):format(lock_data.rev, head)
    130        .. 'To synchronize, restart Nvim and run '
    131        .. ('`vim.pack.update({ %s }, { offline = true })`'):format(name_str)
    132    )
    133    return false
    134  end
    135 
    136  local has_origin, origin = git_cmd({ 'remote', 'get-url', 'origin' }, plug_path)
    137  if not has_origin then
    138    return failed_git_cmd(plug_name, plug_path)
    139  elseif lock_data.src ~= origin then
    140    health.error(
    141      ('Plugin %s has not expected source\n'):format(name_str)
    142        .. ('Expected: %s\nActual:   %s\n'):format(lock_data.src, origin)
    143        .. 'Delete `src` lockfile entry (do not create trailing comma) and '
    144        .. 'restart Nvim to regenerate lockfile data'
    145    )
    146    return false
    147  end
    148 
    149  return true
    150 end
    151 
    152 local function check_lockfile()
    153  health.start('vim.pack: lockfile')
    154 
    155  local can_read, text = pcall(vim.fn.readblob, get_lockfile_path())
    156  if not can_read then
    157    health.error('Could not read lockfile. Delete it and restart Nvim.')
    158    return
    159  end
    160 
    161  local can_parse, data = pcall(vim.json.decode, text)
    162  if not can_parse then
    163    health.error(('Could not parse lockfile: %s\nDelete it and restart Nvim'):format(data))
    164    return
    165  end
    166 
    167  if type(data.plugins) ~= 'table' then
    168    health.error('Field `plugins` is not proper type. Delete lockfile and restart Nvim')
    169    return
    170  end
    171 
    172  local is_good = true
    173  --- @cast data { plugins: table<string,table> }
    174  for plug_name, lock_data in pairs(data.plugins) do
    175    is_good = check_plugin_lock_data(plug_name, lock_data) and is_good
    176  end
    177 
    178  if is_good then
    179    health.ok('')
    180  end
    181 end
    182 
    183 --- @return boolean Whether a check is successful
    184 local function check_installed_plugin(plug_name)
    185  local name_str = vim.inspect(plug_name)
    186  local plug_path = vim.fs.joinpath(get_plug_dir(), plug_name)
    187 
    188  if vim.fn.isdirectory(plug_path) ~= 1 then
    189    health.error(('%s is not a directory. Delete it'):format(plug_name))
    190    return false
    191  end
    192 
    193  if not git_cmd({ 'rev-parse', '--git-dir' }, plug_path) then
    194    health.error(
    195      ('%s is not a Git repository.'):format(name_str)
    196        .. ' It was not installed by `vim.pack` and should not be present in the plugin directory.'
    197        .. ' If installed manually, use dedicated `:h packages`'
    198    )
    199    return false
    200  end
    201 
    202  -- Detached HEAD is a sign that plugin is managed by `vim.pack`
    203  local has_head_ref, head_ref = git_cmd({ 'rev-parse', '--abbrev-ref', 'HEAD' }, plug_path)
    204  if not has_head_ref then
    205    return failed_git_cmd(plug_name, plug_path)
    206  elseif head_ref ~= 'HEAD' then
    207    health.warn(
    208      ('Plugin %s is not at state which is a result of `vim.pack` operation.\n'):format(name_str)
    209        .. 'If it was intentional, make sure you know what you are doing.\n'
    210        .. 'Otherwise, restart Nvim and run '
    211        .. ('`vim.pack.update({ %s }, { offline = true })`.\n'):format(name_str)
    212        .. 'If nothing is updated, plugin is at correct revision and will be managed as expected'
    213    )
    214    return false
    215  end
    216 
    217  -- Usage data
    218  local has_pack_info, info = pcall(vim.pack.get, { plug_name })
    219  if not has_pack_info then
    220    health.error('Could not get `vim.pack` usage information for plugin ' .. name_str)
    221    return false
    222  end
    223 
    224  if not info[1].active then
    225    health.info(
    226      ('Plugin %s is not active.'):format(name_str)
    227        .. ' Is it lazy loaded or did you forget to run `vim.pack.del()`?'
    228    )
    229  end
    230 
    231  return true
    232 end
    233 
    234 local function check_plug_dir()
    235  health.start('vim.pack: plugin directory')
    236 
    237  local is_good = true
    238  local plug_dir = get_plug_dir()
    239  for plug_name, _ in vim.fs.dir(plug_dir) do
    240    is_good = check_installed_plugin(plug_name) and is_good
    241  end
    242 
    243  if is_good then
    244    health.ok('')
    245  end
    246 end
    247 
    248 function M.check()
    249  local has_lockfile, has_plug_dir = check_basics()
    250  if has_lockfile then
    251    check_lockfile()
    252  end
    253  if has_plug_dir then
    254    check_plug_dir()
    255  end
    256 end
    257 
    258 return M