diff --git a/ROADMAP.md b/ROADMAP.md new file mode 100644 index 00000000..e3e8a7c7 --- /dev/null +++ b/ROADMAP.md @@ -0,0 +1,61 @@ +# Roadmap for Packer v2 + +`packer` has become bloated, compilation (at least as it currently stands) has proven confusing and probably not necessary, and the codebase has become messy and hard to maintain. + +As such, I'm proposing/working on (very slowly, as my OSS time is quite limited in my current job) an incremental rewrite of `packer`. +The below is a high-level overview of this plan. +Comments and additions are welcome and encouraged! + +## General principles + +`packer`'s code currently suffers from some poor software development practices largely stemming from getting "discovered" before it had time to become polished. +We want to avoid these pitfalls in the rewrite: + +- Document all functions with emmylua-style comments: `packer` has very sparse documentation in its codebase. New/updated functions need to be appropriately documented. +- Add tests where possible: testing a package manager can be a pain because of requirements to interact with the file system, etc. However, `packer`'s current tests do not cover much, and new code should at least strongly consider adding unit tests. +- Avoid giant functions: `packer`'s logic contains some monstrous functions (e.g., `manage`, the main compilation logic, etc.) that are difficult to work with. New code should strive to use more, simpler functions in a more modular design. +- Prioritize clean, performant code over total flexibility: `packer`'s design allows for a very flexible range of input formats. Although we don't want to completely lose this property, supporting all of these formats (and other use cases) has contributed to code bloat. If there's a choice to be made between losing some flexibility and significantly increasing the complexity of the code, seriously consider removing the complexity. +- Reduce redundancy: `packer` currently has an annoying amount of redundant/overlapping functionality (the worst offenders are the `requires`/`wants`/`after` family of keywords, but there's also duplication of logic in the utilities, etc.). The rewrite should aim to reduce this. + +## Stage 1: Remove compilation and clean up main interface. Breaking changes + +We want the rewrite process to be as unintrusive as possible while still allowing for significant, potentially breaking changes. +As such, the first stage should contain all of the significant breaking changes to reduce the number of times users need to adapt. +The goals of the first stage are: + +- [ ] Mostly remove compilation: this step involves moving to dynamic handlers for plugin specs and only "compiling" (really, caching) information that we can automatically recompile as needed, like additional filepaths to source, etc. + - [x] Make main module lightweight: if `packer` is no longer relying on the compiled file being lightweight, its main module must become cheaper to `require`, as this will be necessary at all startups. This is mostly a question of reducing the amount that `packer` pulls in at the top level (e.g., moving things like management operations more fully into their own modules, reducing the reliance on utils modules, etc.). + - [x] Implement fast runtime handler framework: To replace the compiled file, we will introduce a notion of "keyword handlers": simple functions which are responsible for implementing, at runtime, the behavior of a single previously-compiled keyword. See `lua/packer/handlers.lua` for the current spec and implementation of this idea. + - [ ] Implement compiled keywords as handlers: Work in progress/current state of the rewrite. All the existing keywords from `packer/compile.lua` need to be ported to handlers. This involves dealing with `compile.lua`'s tricky logic in some places, and is a key point for simplification. + - [ ] Implement `load` function to process and execute handlers: The final step of moving away from compilation is to write a function that runs the handlers on the plugin specs to generate lazy-loaders, run `setup` and `config` functions, etc. The trick here will be making this sufficiently fast. + - [ ] Potentially implement path caching, etc.: Once the bulk of compilation is gone, we may wish to investigate adding a little bit back by seeing where the runtime system spends time during startup. If this includes a significant amount of time checking information that only changes when a plugin is installed or updated (e.g., searching for runtime paths), then we may want to start caching this information in a file that gets updated on updates to save startup time. +- [ ] Simplify `requires`/`wants`/etc. + - [ ] Unify `requires`/`wants` into `depends_on`: `requires` and `wants` currently duplicate functionality. I propose merging the two into a single keyword `depends_on`, which will (1) ensure that dependencies are installed and (2) ensure that dependencies are loaded before the dependent plugin. + - [ ] Make interface for `depends_on` and `after` consistent: the correct way to specify dependent plugins/sequential load plugins is confusing, since it's based on a plugin's short name in some cases but the full name in others (I think?). We should define and implement a clear way of referring to plugins in these settings. +- [ ] Clean up interface + - [ ] Use new Neovim v0.7.0 functions instead of `vim.cmd`: the codebase makes use of a lot of `vim.cmd` with string building for things like generating keymaps, commands, etc. Neovim v0.7.0 introduced the ability to create these directly from Lua, with direct Lua callbacks. `packer` should move to use these functions. + - [ ] Make management operations sequenceable steps: To clean up and simplify the code for management operations (e.g., installs, updates, cleans, etc.), we want to redesign the operation interface into sequenceable steps (e.g., "check current FS state", "clone missing plugins", "pull installed plugins", etc.) so that the actual operations become a sequence of calls to functions implementing these steps, rather than the current highly-duplicated logic. + +## Stage 2: Simplify internal git functions and use of async + +The nastiest part of the codebase is the `git` logic. +We would like to simplify this as much as possible. +Also, a significant contributor to the difficulty of working on `packer` is its aggressive use of `async` everywhere. +We should investigate how to minimize the async surface area to allow more flexibility and require less of the codebase to need to think about `async`. + +## Stage 3: Issue triage + +`packer` has accumulated a large number of outstanding issues. +At this point, we should go through and try to reproduce issues with the updated codebase. +During this process, issues should either be fixed or closed as "Wontfix" or "No longer relevant" unless they are feature requests or questions. +The goal of this stage is to more or less close out the open issues. + +## Stage 4: Gradual porting to Teal + +Teal offers significant benefits for maintainability and has improved since the last time I took a look at porting `packer`. +After `packer`'s code is at a cleaner, more reliable state, it makes sense to start porting modules incrementally to Teal. + +## Stage 5 and onward: new features, Luarocks improvements + +Finally, we can start thinking about new features and improvements. +Things like the proposed unified plugin specification format, improvements to Luarocks (e.g., updating rocks, putting binary rocks on the Neovim `PATH`, etc.) diff --git a/lua/packer.lua b/lua/packer.lua index 4d43a9a4..35b34165 100644 --- a/lua/packer.lua +++ b/lua/packer.lua @@ -682,6 +682,12 @@ packer.compile = function(raw_args, move_plugins) local a = require 'packer.async' local async = a.sync local await = a.wait + -- TODO: check if the user wants any compilation at all via a new config variable + -- TODO: remove a bunch of complexity from the compile module/maybe here once we don't need to + -- compile functions or sequencing + -- TODO: implement, for the compiled file, logic to check its values against the current config. + -- We can do this by computing an efficient hash of the lazy-loader strings and storing this in + -- the compiled file manage_all_plugins() async(function() @@ -994,4 +1000,11 @@ packer.startup = function(spec) return packer end +packer.load = function() + -- TODO: This will be responsible for (1) sourcing the new, lighter, compiled file and (2) + -- handling things like running setup and config functions, sequencing, etc. 90% of this logic + -- already exists in the load module; the main thing to add is loading the new compiled file. This + -- function needs to be called at the end of startup() +end + return packer diff --git a/lua/packer/config.lua b/lua/packer/config.lua new file mode 100644 index 00000000..4dfc4d3d --- /dev/null +++ b/lua/packer/config.lua @@ -0,0 +1,86 @@ +--- Configuration + +local path_utils = require 'packer.path' +local stdpath = vim.fn.stdpath +local join_paths = path_utils.join_paths +local path_separator = path_utils.path_separator + +local defaults = { + package_root = join_paths(stdpath 'data', 'site', 'pack'), + compile_path = join_paths(stdpath 'config', 'plugin', 'packer_compiled.lua'), + plugin_package = 'packer', + max_jobs = nil, + auto_clean = true, + compile_on_sync = true, + disable_commands = false, + opt_default = false, + transitive_opt = true, + transitive_disable = true, + auto_reload_compiled = true, + git = { + mark_breaking_changes = true, + cmd = 'git', + subcommands = { + update = 'pull --ff-only --progress --rebase=false', + install = 'clone --depth %i --no-single-branch --progress', + fetch = 'fetch --depth 999999 --progress', + checkout = 'checkout %s --', + update_branch = 'merge --ff-only @{u}', + current_branch = 'rev-parse --abbrev-ref HEAD', + diff = 'log --color=never --pretty=format:FMT --no-show-signature HEAD@{1}...HEAD', + diff_fmt = '%%h %%s (%%cr)', + git_diff_fmt = 'show --no-color --pretty=medium %s', + get_rev = 'rev-parse --short HEAD', + get_header = 'log --color=never --pretty=format:FMT --no-show-signature HEAD -n 1', + get_bodies = 'log --color=never --pretty=format:"===COMMIT_START===%h%n%s===BODY_START===%b" --no-show-signature HEAD@{1}...HEAD', + submodules = 'submodule update --init --recursive --progress', + revert = 'reset --hard HEAD@{1}', + }, + depth = 1, + clone_timeout = 60, + default_url_format = 'https://github.com/%s.git', + }, + display = { + non_interactive = false, + open_fn = nil, + open_cmd = '65vnew', + working_sym = '⟳', + error_sym = '✗', + done_sym = '✓', + removed_sym = '-', + moved_sym = '→', + header_sym = '━', + header_lines = 2, + title = 'packer.nvim', + show_all_info = true, + prompt_border = 'double', + keybindings = { quit = 'q', toggle_info = '', diff = 'd', prompt_revert = 'r' }, + }, + luarocks = { python_cmd = 'python' }, + log = { level = 'warn' }, + profile = { enable = false, threshold = nil }, +} + +local M = { _hooks = {} } +M.config = {} +function M.configure(user_config) + user_config = user_config or {} + local config = M.config + vim.tbl_deep_extend('force', defaults, user_config) + config.package_root = string.gsub(vim.fn.fnamemodify(config.package_root, ':p'), path_separator .. '$', '', 1) + config.pack_dir = join_paths(config.package_root, config.plugin_package) + config.opt_dir = join_paths(config.pack_dir, 'opt') + config.start_dir = join_paths(config.pack_dir, 'start') + config.display.non_interactive = #vim.api.nvim_list_uis() == 0 + for i = 1, #M._hooks do + M._hooks[i](config) + end + + return M.config +end + +function M.register_hook(hook) + M._hooks[#M._hooks + 1] = hook +end + +return M diff --git a/lua/packer/handlers.lua b/lua/packer/handlers.lua index 63a09064..85ce1d13 100644 --- a/lua/packer/handlers.lua +++ b/lua/packer/handlers.lua @@ -1,11 +1,307 @@ -local config = nil +--- Handlers: +--- A handler is a table minimally defining the following keys: +--- name: a string uniquely naming the handler +--- startup: a Boolean indicating if the handler needs to be applied at startup or only when plugins are being fully managed +--- process(self, spec): a function which precomputes any information necessary to apply the handler for the given plugin spec +--- apply(self): a function which evaluates the handler's effect +--- reset(self): a function which clears all precomputed state from the handler -local function cfg(_config) - config = _config +local M = {} + +local function make_default_handler(spec) + local state_table_name = spec.name .. 's' + local handler = { name = spec.name, startup = spec.startup } + handler[state_table_name] = {} + if spec.reset then + handler.reset = spec.reset + else + handler.reset = function(_) + handler[state_table_name] = {} + end + end + + if spec.process then + handler.process = spec.process + else + handler.process = function(_, plugin) + if not plugin[spec.name] then + return + end + + if spec.set_loaders ~= false then + plugin.loaders = plugin.loaders or { _n = 0 } + plugin.loaders[spec.name] = true + plugin.loaders._n = plugin.loaders._n + 1 + end + + if type(plugin[spec.name]) == 'string' then + plugin[spec.name] = { plugin[spec.name] } + end + + if spec.collect then + local key_tbl = plugin[spec.name] + for i = 1, #key_tbl do + local val = key_tbl[i] + handler[state_table_name][val] = handler[state_table_name][val] or {} + handler[state_table_name][val][#handler[state_table_name][val] + 1] = plugin + end + else + handler[state_table_name][#handler[state_table_name] + 1] = plugin + end + end + end + + handler.apply = spec.apply + return handler +end + +-- Default handlers are defined here rather than in their own modules to avoid needing to load a +-- bunch of files on every start +-- TODO: Investigate if having handlers in their own modules and loading them as-needed would lead +-- to performance improvements. Seems likely +local handler_names = { + [module] = true, + cmd = true, + cond = true, + config = true, + disable = true, + event = true, + fn = true, + ft = true, + keys = true, + load_after = true, + requires = true, + rtp = true, + -- TODO: handle ftdetect stuff with caching? + setup = true, +} + +-- Default handler implementations + +local profile = require 'packer.profile' +local timed_run = profile.timed_run +local timed_packadd = profile.timed_packadd +local timed_load = profile.timed_load + +-- Handler for the 'setup' key +local setup_handler = make_default_handler { + name = 'setup', + startup = true, + apply = function(self) + local setups = self.setups + for i = 1, #setups do + local plugin = setups[i] + timed_run(plugin.setup, 'setup for ' .. plugin.short_name, plugin.short_name, plugin) + -- Check for only setup + if plugin.loaders._n == 1 then + timed_packadd(plugin.short_name) + end + end + end, +} + +-- Handler for the 'config' key +local config_handler = make_default_handler { + name = 'config', + startup = true, + set_loaders = false, + apply = function(self) + local configs = self.configs + for i = 1, #configs do + local plugin = configs[i] + timed_run(plugin.config, 'config for ' .. plugin.short_name, plugin.short_name, plugin) + end + end, +} + +-- Handler for the 'module' and 'module_pattern' keys +local module_handler = { name = 'module', startup = true, modules = {}, lazy_load_called = { ['packer.load'] = true } } +function module_handler.reset() + module_handler.modules = {} + module_handler.lazy_load_called = { ['packer.load'] = true } +end + +local function lazy_load_module(module_name) + local to_load = {} + if module_handler.lazy_load_called[module_name] then + return nil + end + + module_handler.lazy_load_called[module_name] = true + for module_pat, plugin in pairs(module_handler.modules) do + if not plugin.loaded and module_name:match(module_pat) then + to_load[#to_load + 1] = plugin.short_name + end + end + + if #to_load > 0 then + timed_load(to_load, { module = module_name }) + local loaded_mod = package.loaded[module_name] + if loaded_mod then + return function(_) + return loaded_mod + end + end + end +end + +function module_handler.process(_, plugin) + if plugin.module or plugin.module_pattern then + plugin.loaders = plugin.loaders or { _n = 0 } + plugin.loaders.module = true + plugin.loaders._n = plugin.loaders._n + 1 + if plugin.module then + if type(plugin.module) == 'string' then + plugin.module = { plugin.module } + end + + for i = 1, #plugin.module do + module_handler.modules['^' .. vim.pesc(plugin.module[i])] = plugin + end + end + + if plugin.module_pattern then + if type(plugin.module_pattern) == 'string' then + plugin.module_pattern = { plugin.module_pattern } + end + + for i = 1, #plugin.module_pattern do + module_handler.modules[plugin.module_pattern[i]] = plugin + end + end + end +end + +local packer_custom_loader_enabled = false +function module_handler.apply(_) + if #module_handler.modules > 0 then + if packer_custom_loader_enabled then + package.loaders[1] = lazy_load_module + else + table.insert(package.loaders, 1, lazy_load_module) + packer_custom_loader_enabled = true + end + end end -local handlers = { - cfg = cfg, +-- Handler for the cmd key +local cmd_handler = make_default_handler { + name = 'cmd', + startup = true, + collect = true, + apply = function(self) + local cmds = self.cmds + local create_command = vim.api.nvim_create_user_command + for cmd, plugins in pairs(cmds) do + if string.match(cmd, '^%w+$') then + create_command(cmd, function(args) + args.cmd = cmd + timed_load(plugins, args) + end, { bang = true, nargs = [[*]], range = true, complete = 'file' }) + end + end + end, } -return handlers +-- Handler for the cond key +local cond_handler = make_default_handler { + name = 'cond', + startup = true, + collect = true, + apply = function(self) + local conds = self.conds + for cond, plugins in pairs(conds) do + if type(cond) == 'string' then + cond = loadstring('return ' .. cond) + end + + if cond() then + timed_load(plugins) + end + end + end, +} + +local startup_handlers = { + module_handler, + cmd_handler, + cond_handler, + config_handler, + disable = true, + event = true, + fn = true, + ft = true, + keys = true, + load_after = true, + rtp = true, + setup_handler, +} + +local num_startup = #startup_handlers +local deferred_handlers = { + requires = true, +} + +local num_deferred = #deferred_handlers + +function M.add(handler) + if handler_names[handler.name] ~= nil then + require('packer.log').warn('Duplicate handler "' .. handler.name .. '" added. Ignoring!') + elseif handler.startup then + num_startup = num_startup + 1 + startup_handlers[num_startup] = handler + handler_names[handler.name] = true + else + num_deferred = num_deferred + 1 + deferred_handlers[num_deferred] = handler + handler_names[handler.name] = true + end +end + +function M.process_startup(plugin) + for i = 1, num_startup do + startup_handlers[i].process(startup_handlers[i], plugin) + end +end + +function M.apply_startup() + for i = 1, num_startup do + startup_handlers[i].apply(startup_handlers[i]) + end +end + +function M.process_deferred(plugin) + for i = 1, num_deferred do + deferred_handlers[i].process(deferred_handlers[i], plugin) + end +end + +function M.apply_deferred() + for i = 1, num_deferred do + deferred_handlers[i].apply(deferred_handlers[i]) + end +end + +function M.get_startup() + return startup_handlers +end + +function M.get_deferred() + return deferred_handlers +end + +function M.get_all() + return vim.tbl_extend('error', {}, startup_handlers, deferred_handlers) +end + +function M.get_handler(name) + if startup_handlers[name] then + return startup_handlers[name] + end + + if deferred_handlers[name] then + return deferred_handlers[name] + end +end + +return M diff --git a/lua/packer/init.lua b/lua/packer/init.lua new file mode 100644 index 00000000..a3e8ccd4 --- /dev/null +++ b/lua/packer/init.lua @@ -0,0 +1,409 @@ +--- Main packer module +local M = {} + +-- TODO: Add design for hooks after operations finish + +-- TODO: Investigate whether using FFI structs for the elements of these tables would be useful +-- and/or faster for operations +local plugins, plugin_specifications, rocks, config + +local handlers = require 'packer.handlers' + +local function ensure_dir(path) + local path_info = vim.loop.fs_stat(path) + if path_info == nil or path_info.type ~= 'directory' then + -- TODO: Investigate if rolling our own mkdir -p with vim.loop.fs_mkdir is faster + if vim.fn.mkdir(path, 'p') ~= 1 then + require('packer.log').warn("Couldn't create path: " .. path) + end + end +end + +--- Configure and reset packer +function M.init(user_config) + if vim.fn.has 'nvim-0.7' ~= 1 then + require('packer.log').error 'Invalid Neovim version for packer.nvim!' + error 'Tried to use packer.nvim with Neovim 0 do + name = name_segments[idx] + idx = idx - 1 + end + + return name, expanded_path +end + +--- Utility function to get the canonical names of top-level plugins from a table of specifications +---@param specs table of plugin specs +local function get_plugin_names(specs) + local names = {} + for i = 1, #specs do + names[#names + 1] = get_plugin_short_name(specs[i]) + end + + return names +end + +--- Utility function to recursively flatten a potentially nested list of plugin specifications. Used by flatten_specification +---@param specs table of (potentially nested) plugin specifications +---@param result table modified in place with the flattened list of specs +local function flatten(specs, from_requires, result) + local num_specs = #specs + for i = 1, num_specs do + local spec = specs[i] + spec.from_requires = from_requires + result[#result + 1] = spec + if spec.requires then + ensure_table(spec.requires) + flatten(spec.requires, true) + spec.requires = get_plugin_names(spec.requires) + end + end +end + +--- Recursively flatten a potentially nested list of plugin specifications +---@param plugin_specification string or full plugin specification or list of plugin specifications +local function flatten_specification(plugin_specification) + plugin_specification = ensure_table(plugin_specification) + local result = {} + flatten(plugin_specification, false, result) + return result +end + +--- Utility function responsible for consistently setting plugin metadata that may be used by +--- handlers +local function set_plugin_metadata(plugin) + local name, path = get_plugin_short_name(plugin) + -- Check name/alias validity + if name == '' then + require('packer.log').warn([["]] .. plugin[1] .. [[" is an invalid plugin name!]]) + return false + end + + if plugins[name] and not plugins[name].from_requires then + require('packer.log').warn([[Plugin "]] .. name .. [[" is used twice! (line ]] .. plugin.line .. [[)]]) + return false + end + + if plugin.as and plugins[plugin.as] then + require('packer.log').error( + string.format( + [[The alias %s specified for %s at line %d is already used as a plugin name!]], + plugin.as, + path, + plugin.line + ) + ) + return false + end + + -- Set plugin name and path + -- TODO: Eliminate use of plugin.name in the rest of the code and call plugin.short_name plugin.name + plugin.short_name = name + plugin.name = path + plugin.path = path + + -- Set manual optness + if plugin.opt then + plugin.manual_opt = true + elseif plugin.opt == nil and config.opt_default then + plugin.manual_opt = true + plugin.opt = true + end + + return true +end + +local getinfo = debug.getinfo +--- Add one or more plugin specifications to the managed set +---@param plugin_specification string, full plugin specification, or list of plugin specifications +--- See main packer documentation for expected format +function M.use(plugin_specification) + local current_line = getinfo(2, 'l').currentline + local flattened_specification = flatten_specification(plugin_specification) + local num_specs = #flattened_specification + for i = 1, num_specs do + local plugin = flattened_specification[i] + if not plugin[1] then + require('packer.log').warn('No plugin name provided for spec at line ' .. current_line) + else + plugin_specifications[#plugin_specifications + 1] = { + spec = plugin, + line = current_line, + plugin_index = i, + } + + set_plugin_metadata(plugin) + handlers.process_startup(plugin) + plugins[plugin.short_name] = plugin + end + end +end + +--- Convenience function for simple setup +-- Can be invoked as follows: +-- spec can be a function: +-- packer.startup(function() use 'tjdevries/colorbuddy.vim' end) +-- +-- spec can be a table with a function as its first element and config overrides as another +-- element: +-- packer.startup({function() use 'tjdevries/colorbuddy.vim' end, config = { ... }}) +-- +-- spec can be a table with a table of plugin specifications as its first element and config +-- overrides as another element: +-- packer.startup({{'tjdevries/colorbuddy.vim'}, config = { ... }}) +function M.startup(spec) + local user_func, user_config, user_plugins + if type(spec) == 'function' then + user_func = spec + elseif type(spec) == 'table' then + if type(spec[1]) == 'function' then + user_func = spec[1] + elseif type(spec[1]) == 'table' then + user_plugins = spec[1] + else + require('packer.log').error 'You must provide a function or table of specifications as the first element of the argument to startup!' + return + end + + user_config = spec.config + end + + M.init(user_config) + if user_func then + if user_func then + setfenv(user_func, vim.tbl_extend('force', getfenv(), { use = M.use, use_rocks = M.use_rocks })) + local status, err = pcall(user_func, M.use, M.use_rocks) + if not status then + require('packer.log').error('Failure running setup function: ' .. vim.inspect(err)) + error(err) + end + else + M.use(user_plugins) + end + + M.load() + return M + end +end + +--- Generate lazy-loaders and run plugin config/setup functions to finish the startup stage +-- TODO: Not sure this is the right name to pick +function M.load() + error 'Not implemented!' +end + +--- Hook to fire events after completion of packer operations +M.on_complete = vim.schedule_wrap(function() + vim.api.nvim_exec_autocmds('User', { pattern = 'PackerComplete' }) +end) + +--- Clean operation +--- Finds and removes plugins which are installed but not managed +function M.clean() + require('packer.operations').clean() +end + +--- Install operation +--- Takes varargs for plugin names to install, or nothing to install all managed plugins +function M.install(...) + require('packer.operations').install(...) +end + +--- Update operation +--- Takes varargs for plugin names to update, or nothing to update all managed plugins +function M.update(...) + require('packer.operations').update(...) +end + +--- Sync operation +--- Takes varargs for plugin names to sync, or nothing to sync all managed plugins +function M.sync(...) + require('packer.operations').sync(...) +end + +--- Show the current status of the managed plugins +function M.status() + require('packer.status').status() +end + +--- Show the output, if any exists, of packer's profiler +function M.profile_output() + local profiles = require('packer.profile').get_profile_data() + if profiles then + require('packer.display').display_profile_output(profiles) + else + local log = require 'packer.log' + log.warn 'No profile output to display! Set config.profile.enable = true and restart' + end +end + +--- Manually load plugins +--- Takes varargs giving plugin names to load, as either a string of space separated names or a list +--- of names as independent strings +function M.activate_plugins(...) + local plugin_names = { ... } + local force = plugin_names[#plugin_names] == true + if type(plugin_names[#plugin_names]) == 'boolean' then + plugin_names[#plugin_names] = nil + end + + -- We make a new table here because it's more convenient than expanding a space-separated string + -- into the existing plugin_names + local plugin_list = {} + for _, plugin_name in ipairs(plugin_names) do + vim.list_extend( + plugin_list, + vim.tbl_filter(function(name) + return #name > 0 + end, vim.split(plugin_name, ' ')) + ) + end + + require 'packer.load'(plugin_list, {}, plugins, force) +end + +--- Completion for not-yet-loaded plugin names +--- Used by PackerLoad command +local function complete_loadable_plugin_names(lead, _, _) + local completion_list = {} + for name, plugin in pairs(plugins) do + if vim.startswith(name, lead) and not plugin.loaded then + table.insert(completion_list, name) + end + end + table.sort(completion_list) + return completion_list +end + +--- Completion for managed plugin names +--- Used by PackerInstall/Update/Sync commands +local function complete_plugin_names(lead, _, _) + local completion_list = vim.tbl_filter(function(name) + return vim.startswith(name, lead) + end, vim.tbl_keys(plugins)) + table.sort(completion_list) + return completion_list +end + +--- packer's predefined commands +local commands = { + { + name = [[PackerInstall]], + command = function(args) + M.install(unpack(args.fargs)) + end, + opts = { + nargs = [[*]], + complete = complete_plugin_names, + }, + }, + { + name = [[PackerUpdate]], + command = function(args) + M.update(unpack(args.fargs)) + end, + opts = { + nargs = [[*]], + complete = complete_plugin_names, + }, + }, + { + name = [[PackerSync]], + command = function(args) + M.sync(unpack(args.fargs)) + end, + opts = { + nargs = [[*]], + complete = complete_plugin_names, + }, + }, + { name = [[PackerClean]], command = M.clean }, + { name = [[PackerStatus]], command = M.status }, + { name = [[PackerProfileOutput]], command = M.profile_output }, + { + name = [[PackerLoad]], + command = function(args) + M.activate_plugins(unpack(args.fargs), args.bang) + end, + opts = { + bang = true, + nargs = [[+]], + complete = complete_loadable_plugin_names, + }, + }, +} + +--- Ensure the existence of packer's standard commands +function M.make_commands() + local create_command = vim.api.nvim_create_user_command + for i = 1, #commands do + local cmd = commands[i] + create_command(cmd.name, cmd.command, cmd.opts) + end +end + +return M diff --git a/lua/packer/path.lua b/lua/packer/path.lua new file mode 100644 index 00000000..76cb5034 --- /dev/null +++ b/lua/packer/path.lua @@ -0,0 +1,8 @@ +--- Minimal platform-aware path manipulation utilities for the parts of packer that load on every start +local is_windows = vim.loop.os_uname().sysname:lower():find 'windows' ~= nil +local M = { path_separator = (is_windows and [[\]]) or [[/]] } +function M.join_paths(...) + return table.concat({ ... }, M.path_separator) +end + +return M diff --git a/lua/packer/plugin_utils.lua b/lua/packer/plugin_utils.lua index c495c935..18191cca 100644 --- a/lua/packer/plugin_utils.lua +++ b/lua/packer/plugin_utils.lua @@ -6,11 +6,8 @@ local log = require 'packer.log' local await = a.wait -local config = nil +local config = require('packer.config').config local plugin_utils = {} -plugin_utils.cfg = function(_config) - config = _config -end plugin_utils.custom_plugin_type = 'custom' plugin_utils.local_plugin_type = 'local' diff --git a/lua/packer/profile.lua b/lua/packer/profile.lua new file mode 100644 index 00000000..e1332c4a --- /dev/null +++ b/lua/packer/profile.lua @@ -0,0 +1,76 @@ +--- Support for fine-grained profiling of startup steps +local M = { results = {} } + +local function time(name, is_start) end + +local profile_data = {} +local hrtime = vim.loop.hrtime +local threshold = nil +require('packer.config').register_hook(function(config) + threshold = config.profile.threshold + if config.profile.enable then + time = function(name, is_start) + if is_start then + profile_data[name] = hrtime() + else + profile_data[name] = (hrtime() - profile_data[name]) / 1e6 + end + end + end +end) + +function M.save_profiles() + M.results = {} + local sorted_times = {} + for chunk_name, time_taken in pairs(profile_data) do + sorted_times[#sorted_times + 1] = { chunk_name, time_taken } + end + + table.sort(sorted_times, function(a, b) + return a[2] > b[2] + end) + + for i, elem in ipairs(sorted_times) do + if not threshold or threshold and elem[2] > threshold then + M.results[i] = elem[1] .. ' took ' .. elem[2] .. 'ms' + end + end +end + +function M.get_profile_data() + if #M.results == 0 then + M.save_profiles() + end + + return M.results +end + +function M.timed_run(fn, name, ...) + time(name, true) + local success, result = pcall(fn, ...) + time(name, false) + if not success then + vim.schedule(function() + require('packer.log').error('Failed running ' .. name .. ': ' .. result) + end) + end + + return result +end + +function M.timed_packadd(name) + local packadd_cmd = 'packadd ' .. name + time(packadd_cmd, true) + vim.cmd(packadd_cmd) + time(packadd_cmd, false) +end + +function M.timed_load(plugins, args) + local load = require 'packer.load' + local load_name = 'Loading ' .. vim.inspect(plugins) + time(load_name, true) + load(plugins, args) + time(load_name, false) +end + +return M diff --git a/lua/packer/util.lua b/lua/packer/util.lua index b0668209..592366e0 100644 --- a/lua/packer/util.lua +++ b/lua/packer/util.lua @@ -150,4 +150,8 @@ util.float = function(opts) return true, win, buf end +function util.ensure_table(obj) + return (type(obj) == 'table' and obj) or { obj } +end + return util